diff --git a/go_backend/tidal.go b/go_backend/tidal.go index 7eb390f8..8551e4ae 100644 --- a/go_backend/tidal.go +++ b/go_backend/tidal.go @@ -693,9 +693,19 @@ func (t *TidalDownloader) downloadFromManifest(manifestB64, outputPath string) e Timeout: 120 * time.Second, } - // If we have a direct URL (BTS format), download directly + // If we have a direct URL (BTS format), download directly with progress tracking if directURL != "" { - resp, err := client.Get(directURL) + // Set current file being downloaded + SetCurrentFile(filepath.Base(outputPath)) + SetDownloading(true) + defer SetDownloading(false) + + req, err := http.NewRequest("GET", directURL, nil) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + resp, err := client.Do(req) if err != nil { return fmt.Errorf("failed to download file: %w", err) } @@ -705,13 +715,20 @@ func (t *TidalDownloader) downloadFromManifest(manifestB64, outputPath string) e return fmt.Errorf("download failed with status %d", resp.StatusCode) } + // Set total bytes for progress tracking + if resp.ContentLength > 0 { + SetBytesTotal(resp.ContentLength) + } + out, err := os.Create(outputPath) if err != nil { return fmt.Errorf("failed to create file: %w", err) } defer out.Close() - _, err = io.Copy(out, resp.Body) + // Use ProgressWriter for tracking + progressWriter := NewProgressWriter(out) + _, err = io.Copy(progressWriter, resp.Body) return err } diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index 38b20300..5555bf24 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -1,7 +1,9 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:ffmpeg_kit_flutter_new_audio/ffmpeg_kit.dart'; import 'package:ffmpeg_kit_flutter_new_audio/return_code.dart'; import 'package:spotiflac_android/models/download_item.dart'; @@ -31,6 +33,28 @@ class DownloadHistoryItem { required this.service, required this.downloadedAt, }); + + Map toJson() => { + 'id': id, + 'trackName': trackName, + 'artistName': artistName, + 'albumName': albumName, + 'coverUrl': coverUrl, + 'filePath': filePath, + 'service': service, + 'downloadedAt': downloadedAt.toIso8601String(), + }; + + factory DownloadHistoryItem.fromJson(Map json) => DownloadHistoryItem( + id: json['id'] as String, + trackName: json['trackName'] as String, + artistName: json['artistName'] as String, + albumName: json['albumName'] as String, + coverUrl: json['coverUrl'] as String?, + filePath: json['filePath'] as String, + service: json['service'] as String, + downloadedAt: DateTime.parse(json['downloadedAt'] as String), + ); } // Download History State @@ -46,23 +70,54 @@ class DownloadHistoryState { // Download History Notifier (Riverpod 3.x) class DownloadHistoryNotifier extends Notifier { + static const _storageKey = 'download_history'; + @override DownloadHistoryState build() { + // Load history from storage on init + Future.microtask(() => _loadFromStorage()); return const DownloadHistoryState(); } + Future _loadFromStorage() async { + try { + final prefs = await SharedPreferences.getInstance(); + final jsonStr = prefs.getString(_storageKey); + if (jsonStr != null) { + final List jsonList = jsonDecode(jsonStr); + final items = jsonList.map((e) => DownloadHistoryItem.fromJson(e as Map)).toList(); + state = state.copyWith(items: items); + } + } catch (e) { + print('[DownloadHistory] Failed to load history: $e'); + } + } + + Future _saveToStorage() async { + try { + final prefs = await SharedPreferences.getInstance(); + final jsonList = state.items.map((e) => e.toJson()).toList(); + await prefs.setString(_storageKey, jsonEncode(jsonList)); + } catch (e) { + print('[DownloadHistory] Failed to save history: $e'); + } + } + void addToHistory(DownloadHistoryItem item) { state = state.copyWith(items: [item, ...state.items]); + _saveToStorage(); } void removeFromHistory(String id) { state = state.copyWith( items: state.items.where((item) => item.id != id).toList(), ); + _saveToStorage(); } void clearHistory() { state = const DownloadHistoryState(); + _saveToStorage(); } }