You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							549 lines
						
					
					
						
							21 KiB
						
					
					
				
			
		
		
	
	
							549 lines
						
					
					
						
							21 KiB
						
					
					
				<!DOCTYPE html>
 | 
						|
<html lang="en">
 | 
						|
<head>
 | 
						|
    <meta charset="UTF-8">
 | 
						|
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
						|
    <title>Flymaster GPS Client</title>
 | 
						|
    <!-- Vue.js -->
 | 
						|
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
 | 
						|
    <!-- Flymaster Client -->
 | 
						|
    <script src="flymaster-client.js"></script>
 | 
						|
    <!-- Leaflet CSS -->
 | 
						|
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
 | 
						|
          integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
 | 
						|
          crossorigin=""/>
 | 
						|
    <!-- Leaflet JavaScript -->
 | 
						|
    <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
 | 
						|
            integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
 | 
						|
            crossorigin=""></script>
 | 
						|
    <style>
 | 
						|
        body {
 | 
						|
            font-family: Arial, sans-serif;
 | 
						|
            max-width: 800px;
 | 
						|
            margin: 0 auto;
 | 
						|
            padding: 20px;
 | 
						|
        }
 | 
						|
        .container {
 | 
						|
            border: 1px solid #ddd;
 | 
						|
            border-radius: 5px;
 | 
						|
            padding: 20px;
 | 
						|
            margin-bottom: 20px;
 | 
						|
        }
 | 
						|
        h1, h2 {
 | 
						|
            color: #333;
 | 
						|
        }
 | 
						|
        button {
 | 
						|
            background-color: #4CAF50;
 | 
						|
            border: none;
 | 
						|
            color: white;
 | 
						|
            padding: 10px 15px;
 | 
						|
            text-align: center;
 | 
						|
            text-decoration: none;
 | 
						|
            display: inline-block;
 | 
						|
            font-size: 14px;
 | 
						|
            margin: 4px 2px;
 | 
						|
            cursor: pointer;
 | 
						|
            border-radius: 4px;
 | 
						|
        }
 | 
						|
        button:disabled {
 | 
						|
            background-color: #cccccc;
 | 
						|
            cursor: not-allowed;
 | 
						|
        }
 | 
						|
        .download-button {
 | 
						|
            background-color: #2196F3;
 | 
						|
            margin-top: 10px;
 | 
						|
            margin-bottom: 15px;
 | 
						|
        }
 | 
						|
        .status {
 | 
						|
            margin: 10px 0;
 | 
						|
            padding: 10px;
 | 
						|
            border-radius: 4px;
 | 
						|
        }
 | 
						|
        .status.success {
 | 
						|
            background-color: #dff0d8;
 | 
						|
            color: #3c763d;
 | 
						|
        }
 | 
						|
        .status.error {
 | 
						|
            background-color: #f2dede;
 | 
						|
            color: #a94442;
 | 
						|
        }
 | 
						|
        .status.info {
 | 
						|
            background-color: #d9edf7;
 | 
						|
            color: #31708f;
 | 
						|
        }
 | 
						|
        table {
 | 
						|
            width: 100%;
 | 
						|
            border-collapse: collapse;
 | 
						|
            margin: 15px 0;
 | 
						|
        }
 | 
						|
        table, th, td {
 | 
						|
            border: 1px solid #ddd;
 | 
						|
        }
 | 
						|
        th, td {
 | 
						|
            padding: 8px;
 | 
						|
            text-align: left;
 | 
						|
        }
 | 
						|
        th {
 | 
						|
            background-color: #f2f2f2;
 | 
						|
        }
 | 
						|
        .flight-info-table th {
 | 
						|
            width: 200px;
 | 
						|
        }
 | 
						|
        tr:nth-child(even) {
 | 
						|
            background-color: #f9f9f9;
 | 
						|
        }
 | 
						|
        .progress-container {
 | 
						|
            width: 100%;
 | 
						|
            background-color: #f1f1f1;
 | 
						|
            border-radius: 4px;
 | 
						|
            margin: 10px 0;
 | 
						|
        }
 | 
						|
        .progress-bar {
 | 
						|
            height: 20px;
 | 
						|
            background-color: #4CAF50;
 | 
						|
            border-radius: 4px;
 | 
						|
            text-align: center;
 | 
						|
            color: white;
 | 
						|
            line-height: 20px;
 | 
						|
        }
 | 
						|
        .track-list {
 | 
						|
            max-height: 300px;
 | 
						|
            overflow-y: auto;
 | 
						|
        }
 | 
						|
        #map-container {
 | 
						|
            height: 400px;
 | 
						|
            width: 100%;
 | 
						|
            margin-top: 15px;
 | 
						|
        }
 | 
						|
        .action-button {
 | 
						|
            margin: 2px;
 | 
						|
            padding: 5px 10px;
 | 
						|
            font-size: 12px;
 | 
						|
        }
 | 
						|
        .view-button {
 | 
						|
            background-color: #2196F3;
 | 
						|
        }
 | 
						|
        .save-button {
 | 
						|
            background-color: #4CAF50;
 | 
						|
        }
 | 
						|
        .not-set {
 | 
						|
            color: #999;
 | 
						|
            font-style: italic;
 | 
						|
        }
 | 
						|
    </style>
 | 
						|
</head>
 | 
						|
<body>
 | 
						|
    <div id="app">
 | 
						|
        <h1>Flymaster GPS Client</h1>
 | 
						|
 | 
						|
        <div class="container">
 | 
						|
            <h2>Connection</h2>
 | 
						|
            <button @click="connect" :disabled="isConnected">Connect</button>
 | 
						|
            <button @click="disconnect" :disabled="!isConnected">Disconnect</button>
 | 
						|
 | 
						|
            <div v-if="statusMessage" :class="['status', statusType]">
 | 
						|
                {{ statusMessage }}
 | 
						|
            </div>
 | 
						|
 | 
						|
            <div v-if="deviceInfo">
 | 
						|
                <h3>Device Information</h3>
 | 
						|
                <p><strong>Name:</strong> {{ deviceInfo.name }}</p>
 | 
						|
                <p><strong>Unit ID:</strong> {{ deviceInfo.unitId }}</p>
 | 
						|
            </div>
 | 
						|
        </div>
 | 
						|
 | 
						|
        <div class="container" v-if="isConnected">
 | 
						|
            <h2>Tracks</h2>
 | 
						|
            <button @click="refreshTracks" :disabled="isLoading">Refresh Track List</button>
 | 
						|
 | 
						|
            <div v-if="isLoading" class="status info">
 | 
						|
                Loading tracks...
 | 
						|
            </div>
 | 
						|
 | 
						|
            <div v-if="tracks.length > 0" class="track-list">
 | 
						|
                <table>
 | 
						|
                    <thead>
 | 
						|
                        <tr>
 | 
						|
                            <th>#</th>
 | 
						|
                            <th>Start Date</th>
 | 
						|
                            <th>End Date</th>
 | 
						|
                            <th>Duration</th>
 | 
						|
                            <th>Actions</th>
 | 
						|
                        </tr>
 | 
						|
                    </thead>
 | 
						|
                    <tbody>
 | 
						|
                        <tr v-for="track in tracks" :key="track.index">
 | 
						|
                            <td>{{ track.index + 1 }}</td>
 | 
						|
                            <td>{{ formatDate(track.startDate) }}</td>
 | 
						|
                            <td>{{ formatDate(track.endDate) }}</td>
 | 
						|
                            <td>{{ formatDuration(track.duration) }}</td>
 | 
						|
                            <td>
 | 
						|
                                <button @click="downloadTrack(track.index, false)" :disabled="isDownloading" class="action-button view-button">
 | 
						|
                                    View
 | 
						|
                                </button>
 | 
						|
                                <button @click="downloadTrack(track.index, true)" :disabled="isDownloading" class="action-button save-button">
 | 
						|
                                    Save
 | 
						|
                                </button>
 | 
						|
                            </td>
 | 
						|
                        </tr>
 | 
						|
                    </tbody>
 | 
						|
                </table>
 | 
						|
            </div>
 | 
						|
 | 
						|
            <div v-if="isDownloading">
 | 
						|
                <h3>Download Progress</h3>
 | 
						|
                <div class="progress-container">
 | 
						|
                    <div class="progress-bar" :style="{ width: downloadProgress + '%' }">
 | 
						|
                        {{ downloadProgress }}%
 | 
						|
                    </div>
 | 
						|
                </div>
 | 
						|
            </div>
 | 
						|
        </div>
 | 
						|
 | 
						|
 | 
						|
        <div class="container" v-if="trackPoints.length > 0">
 | 
						|
            <h2>Downloaded Track Data</h2>
 | 
						|
            <p>Total points: {{ trackPoints.length }}</p>
 | 
						|
 | 
						|
            <button @click="saveTrackToFile" class="download-button">Save Track to File</button>
 | 
						|
 | 
						|
            <div v-if="flightInfo">
 | 
						|
                <h3>Flight Information</h3>
 | 
						|
                <table class="flight-info-table">
 | 
						|
                    <tbody>
 | 
						|
                        <tr>
 | 
						|
                            <th>Software Version</th>
 | 
						|
                            <td>{{ flightInfo.sw_version }}</td>
 | 
						|
                        </tr>
 | 
						|
                        <tr>
 | 
						|
                            <th>Hardware Version</th>
 | 
						|
                            <td>{{ flightInfo.hw_version }}</td>
 | 
						|
                        </tr>
 | 
						|
                        <tr>
 | 
						|
                            <th>Serial Number</th>
 | 
						|
                            <td>{{ flightInfo.serial }}</td>
 | 
						|
                        </tr>
 | 
						|
                        <tr>
 | 
						|
                            <th>Competition Number</th>
 | 
						|
                            <td :class="{ 'not-set': !flightInfo.compnum }">{{ flightInfo.compnum || 'Not set' }}</td>
 | 
						|
                        </tr>
 | 
						|
                        <tr>
 | 
						|
                            <th>Pilot Name</th>
 | 
						|
                            <td :class="{ 'not-set': !flightInfo.pilotname }">{{ flightInfo.pilotname || 'Not set' }}</td>
 | 
						|
                        </tr>
 | 
						|
                        <tr>
 | 
						|
                            <th>Glider Brand</th>
 | 
						|
                            <td :class="{ 'not-set': !flightInfo.gliderbrand }">{{ flightInfo.gliderbrand || 'Not set' }}</td>
 | 
						|
                        </tr>
 | 
						|
                        <tr>
 | 
						|
                            <th>Glider Model</th>
 | 
						|
                            <td :class="{ 'not-set': !flightInfo.glidermodel }">{{ flightInfo.glidermodel || 'Not set' }}</td>
 | 
						|
                        </tr>
 | 
						|
                    </tbody>
 | 
						|
                </table>
 | 
						|
            </div>
 | 
						|
 | 
						|
            <div v-if="trackPoints.length > 0">
 | 
						|
                <h3>Sample Points</h3>
 | 
						|
                <table>
 | 
						|
                    <thead>
 | 
						|
                        <tr>
 | 
						|
                            <th>#</th>
 | 
						|
                            <th>Latitude</th>
 | 
						|
                            <th>Longitude</th>
 | 
						|
                            <th>GPS Alt</th>
 | 
						|
                            <th>Baro Alt</th>
 | 
						|
                            <th>Time</th>
 | 
						|
                        </tr>
 | 
						|
                    </thead>
 | 
						|
                    <tbody>
 | 
						|
                        <tr v-for="(point, index) in samplePoints" :key="index">
 | 
						|
                            <td>{{ index + 1 }}</td>
 | 
						|
                            <td>{{ point.lat.toFixed(6) }}</td>
 | 
						|
                            <td>{{ point.lon.toFixed(6) }}</td>
 | 
						|
                            <td>{{ point.gpsalt }}m</td>
 | 
						|
                            <td>{{ point.baroalt.toFixed(1) }}m</td>
 | 
						|
                            <td>{{ formatDate(new Date(point.time * 1000)) }}</td>
 | 
						|
                        </tr>
 | 
						|
                    </tbody>
 | 
						|
                </table>
 | 
						|
            </div>
 | 
						|
 | 
						|
            <h3>Track Map</h3>
 | 
						|
            <div id="map-container" ref="mapContainer"></div>
 | 
						|
        </div>
 | 
						|
    </div>
 | 
						|
 | 
						|
    <script>
 | 
						|
        new Vue({
 | 
						|
            el: '#app',
 | 
						|
            data: {
 | 
						|
                client: null,
 | 
						|
                isConnected: false,
 | 
						|
                isLoading: false,
 | 
						|
                isDownloading: false,
 | 
						|
                statusMessage: '',
 | 
						|
                statusType: 'info',
 | 
						|
                deviceInfo: null,
 | 
						|
                tracks: [],
 | 
						|
                trackPoints: [],
 | 
						|
                downloadProgress: 0,
 | 
						|
                map: null,
 | 
						|
                flightInfo: null
 | 
						|
            },
 | 
						|
            computed: {
 | 
						|
                samplePoints() {
 | 
						|
                    // Return a sample of points (first 10)
 | 
						|
                    return this.trackPoints.slice(0, 10);
 | 
						|
                }
 | 
						|
            },
 | 
						|
            methods: {
 | 
						|
 | 
						|
                async connect() {
 | 
						|
                    try {
 | 
						|
                        this.statusMessage = 'Connecting to device...';
 | 
						|
                        this.statusType = 'info';
 | 
						|
 | 
						|
                        this.client = new FlymasterClient();
 | 
						|
 | 
						|
                        const connected = await this.client.connect();
 | 
						|
 | 
						|
                        if (connected) {
 | 
						|
                            this.isConnected = true;
 | 
						|
                            this.statusMessage = 'Connected to device successfully!';
 | 
						|
                            this.statusType = 'success';
 | 
						|
 | 
						|
                            // Initialize GPS
 | 
						|
                            await this.initializeGps();
 | 
						|
                        } else {
 | 
						|
                            this.statusMessage = 'Failed to connect to device.';
 | 
						|
                            this.statusType = 'error';
 | 
						|
                        }
 | 
						|
                    } catch (error) {
 | 
						|
                        console.error('Connection error:', error);
 | 
						|
                        this.statusMessage = 'Error connecting to device: ' + error.message;
 | 
						|
                        this.statusType = 'error';
 | 
						|
                    }
 | 
						|
                },
 | 
						|
 | 
						|
                async disconnect() {
 | 
						|
                    try {
 | 
						|
                        if (this.client) {
 | 
						|
                            await this.client.disconnect();
 | 
						|
                            this.isConnected = false;
 | 
						|
                            this.statusMessage = 'Disconnected from device.';
 | 
						|
                            this.statusType = 'info';
 | 
						|
                            this.deviceInfo = null;
 | 
						|
                            this.flightInfo = null;
 | 
						|
                        }
 | 
						|
                    } catch (error) {
 | 
						|
                        console.error('Disconnection error:', error);
 | 
						|
                        this.statusMessage = 'Error disconnecting from device: ' + error.message;
 | 
						|
                        this.statusType = 'error';
 | 
						|
                    }
 | 
						|
                },
 | 
						|
 | 
						|
                async initializeGps() {
 | 
						|
                    try {
 | 
						|
                        this.isLoading = true;
 | 
						|
                        this.statusMessage = 'Initializing GPS...';
 | 
						|
                        this.statusType = 'info';
 | 
						|
 | 
						|
                        await this.client.initGps();
 | 
						|
 | 
						|
                        this.deviceInfo = {
 | 
						|
                            name: this.client.gpsname,
 | 
						|
                            unitId: this.client.gpsunitid
 | 
						|
                        };
 | 
						|
 | 
						|
                        this.refreshTracks();
 | 
						|
 | 
						|
                        this.statusMessage = 'GPS initialized successfully!';
 | 
						|
                        this.statusType = 'success';
 | 
						|
                    } catch (error) {
 | 
						|
                        console.error('GPS initialization error:', error);
 | 
						|
                        this.statusMessage = 'Error initializing GPS: ' + error.message;
 | 
						|
                        this.statusType = 'error';
 | 
						|
                    } finally {
 | 
						|
                        this.isLoading = false;
 | 
						|
                    }
 | 
						|
                },
 | 
						|
 | 
						|
                async refreshTracks() {
 | 
						|
                    try {
 | 
						|
                        this.isLoading = true;
 | 
						|
                        this.statusMessage = 'Loading tracks...';
 | 
						|
                        this.statusType = 'info';
 | 
						|
 | 
						|
                        this.tracks = this.client.getTrackList();
 | 
						|
 | 
						|
                        this.statusMessage = `Loaded ${this.tracks.length} tracks.`;
 | 
						|
                        this.statusType = 'success';
 | 
						|
                    } catch (error) {
 | 
						|
                        console.error('Track loading error:', error);
 | 
						|
                        this.statusMessage = 'Error loading tracks: ' + error.message;
 | 
						|
                        this.statusType = 'error';
 | 
						|
                    } finally {
 | 
						|
                        this.isLoading = false;
 | 
						|
                    }
 | 
						|
                },
 | 
						|
 | 
						|
                async downloadTrack(trackIndex, saveToFile) {
 | 
						|
                    try {
 | 
						|
                        this.isDownloading = true;
 | 
						|
                        this.downloadProgress = 0;
 | 
						|
                        this.statusMessage = 'Downloading track...';
 | 
						|
                        this.statusType = 'info';
 | 
						|
 | 
						|
                        // Reset flight info before downloading
 | 
						|
                        this.flightInfo = null;
 | 
						|
 | 
						|
                        // Download the single track
 | 
						|
                        this.trackPoints = await this.client.downloadStrack(trackIndex, (current, total) => {
 | 
						|
                            this.downloadProgress = Math.floor((current / total) * 100);
 | 
						|
                            return true; // Continue download
 | 
						|
                        });
 | 
						|
 | 
						|
                        // Get flight info if available
 | 
						|
                        this.flightInfo = this.client.getFlightInfo();
 | 
						|
 | 
						|
                        this.statusMessage = `Downloaded ${this.trackPoints.length} points successfully!`;
 | 
						|
                        this.statusType = 'success';
 | 
						|
 | 
						|
                        if (saveToFile) {
 | 
						|
                            // Save to file immediately
 | 
						|
                            this.saveTrackToFile();
 | 
						|
                        } else {
 | 
						|
                            // Initialize and update the map with track points
 | 
						|
                            this.$nextTick(() => {
 | 
						|
                                this.initMap();
 | 
						|
                                this.displayTrackOnMap();
 | 
						|
                            });
 | 
						|
                        }
 | 
						|
                    } catch (error) {
 | 
						|
                        console.error('Download error:', error);
 | 
						|
                        this.statusMessage = 'Error downloading track: ' + error.message;
 | 
						|
                        this.statusType = 'error';
 | 
						|
                    } finally {
 | 
						|
                        this.isDownloading = false;
 | 
						|
                    }
 | 
						|
                },
 | 
						|
 | 
						|
                initMap() {
 | 
						|
                    // If map already exists, remove it and create a new one
 | 
						|
                    if (this.map) {
 | 
						|
                        this.map.remove();
 | 
						|
                    }
 | 
						|
 | 
						|
                    // Create a new map instance
 | 
						|
                    this.map = L.map(this.$refs.mapContainer).setView([0, 0], 2);
 | 
						|
 | 
						|
                    // Add OpenStreetMap tile layer
 | 
						|
                    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
 | 
						|
                        attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
 | 
						|
                    }).addTo(this.map);
 | 
						|
                },
 | 
						|
 | 
						|
                displayTrackOnMap() {
 | 
						|
                    if (!this.map || this.trackPoints.length === 0) return;
 | 
						|
 | 
						|
                    // Create an array of [lat, lng] points for the track
 | 
						|
                    const trackLatLngs = this.trackPoints.map(point => [point.lat, point.lon]);
 | 
						|
 | 
						|
                    // Create a polyline from the track points
 | 
						|
                    const trackLine = L.polyline(trackLatLngs, {
 | 
						|
                        color: 'blue',
 | 
						|
                        weight: 3,
 | 
						|
                        opacity: 0.7
 | 
						|
                    }).addTo(this.map);
 | 
						|
 | 
						|
                    // Add markers for start and end points
 | 
						|
                    const startPoint = this.trackPoints[0];
 | 
						|
                    const endPoint = this.trackPoints[this.trackPoints.length - 1];
 | 
						|
 | 
						|
                    L.marker([startPoint.lat, startPoint.lon])
 | 
						|
                        .bindPopup('Start: ' + this.formatDate(new Date(startPoint.time * 1000)))
 | 
						|
                        .addTo(this.map);
 | 
						|
 | 
						|
                    L.marker([endPoint.lat, endPoint.lon])
 | 
						|
                        .bindPopup('End: ' + this.formatDate(new Date(endPoint.time * 1000)))
 | 
						|
                        .addTo(this.map);
 | 
						|
 | 
						|
                    // Fit the map to the track bounds
 | 
						|
                    this.map.fitBounds(trackLine.getBounds());
 | 
						|
                },
 | 
						|
 | 
						|
                formatDate(date) {
 | 
						|
                    return new Date(date).toLocaleString();
 | 
						|
                },
 | 
						|
 | 
						|
                formatDuration(seconds) {
 | 
						|
                    const hours = Math.floor(seconds / 3600);
 | 
						|
                    const minutes = Math.floor((seconds % 3600) / 60);
 | 
						|
                    const secs = seconds % 60;
 | 
						|
 | 
						|
                    return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
 | 
						|
                },
 | 
						|
 | 
						|
                saveTrackToFile() {
 | 
						|
                    if (this.trackPoints.length === 0) {
 | 
						|
                        this.statusMessage = 'No track data to save.';
 | 
						|
                        this.statusType = 'error';
 | 
						|
                        return;
 | 
						|
                    }
 | 
						|
 | 
						|
                    try {
 | 
						|
                        // Create GPX format
 | 
						|
                        let gpx = '<?xml version="1.0" encoding="UTF-8"?>\n';
 | 
						|
                        gpx += '<gpx version="1.1" creator="Flymaster GPS Client" xmlns="http://www.topografix.com/GPX/1/1">\n';
 | 
						|
                        gpx += '  <metadata>\n';
 | 
						|
                        gpx += `    <name>Track from ${this.deviceInfo.name}</name>\n`;
 | 
						|
                        gpx += `    <time>${new Date().toISOString()}</time>\n`;
 | 
						|
                        gpx += '  </metadata>\n';
 | 
						|
                        gpx += '  <trk>\n';
 | 
						|
                        gpx += '    <name>Flymaster Track</name>\n';
 | 
						|
                        gpx += '    <trkseg>\n';
 | 
						|
 | 
						|
                        // Add track points
 | 
						|
                        this.trackPoints.forEach(point => {
 | 
						|
                            const time = new Date(point.time * 1000).toISOString();
 | 
						|
                            gpx += `      <trkpt lat="${point.lat}" lon="${point.lon}">\n`;
 | 
						|
                            gpx += `        <ele>${point.gpsalt}</ele>\n`;
 | 
						|
                            gpx += `        <time>${time}</time>\n`;
 | 
						|
                            gpx += '      </trkpt>\n';
 | 
						|
                        });
 | 
						|
 | 
						|
                        gpx += '    </trkseg>\n';
 | 
						|
                        gpx += '  </trk>\n';
 | 
						|
                        gpx += '</gpx>';
 | 
						|
 | 
						|
                        // Create a blob and download link
 | 
						|
                        const blob = new Blob([gpx], { type: 'application/gpx+xml' });
 | 
						|
                        const url = URL.createObjectURL(blob);
 | 
						|
 | 
						|
                        // Create a temporary link element and trigger download
 | 
						|
                        const a = document.createElement('a');
 | 
						|
                        a.href = url;
 | 
						|
                        a.download = `flymaster_track_${new Date().toISOString().slice(0, 10)}.gpx`;
 | 
						|
                        document.body.appendChild(a);
 | 
						|
                        a.click();
 | 
						|
 | 
						|
                        // Clean up
 | 
						|
                        setTimeout(() => {
 | 
						|
                            document.body.removeChild(a);
 | 
						|
                            URL.revokeObjectURL(url);
 | 
						|
                        }, 100);
 | 
						|
 | 
						|
                        this.statusMessage = 'Track saved successfully!';
 | 
						|
                        this.statusType = 'success';
 | 
						|
                    } catch (error) {
 | 
						|
                        console.error('Error saving track:', error);
 | 
						|
                        this.statusMessage = 'Error saving track: ' + error.message;
 | 
						|
                        this.statusType = 'error';
 | 
						|
                    }
 | 
						|
                }
 | 
						|
            }
 | 
						|
        });
 | 
						|
    </script>
 | 
						|
</body>
 | 
						|
</html>
 | 
						|
 |