diff --git a/.cursorignore b/.cursorignore new file mode 100644 index 00000000..6f9f00ff --- /dev/null +++ b/.cursorignore @@ -0,0 +1 @@ +# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv) diff --git a/go_backend/metadata.go b/go_backend/metadata.go index 3001730e..9fdd02d2 100644 --- a/go_backend/metadata.go +++ b/go_backend/metadata.go @@ -92,14 +92,12 @@ func EmbedMetadata(filePath string, metadata Metadata, coverPath string) error { f.Meta = append(f.Meta, &cmtBlock) } - // Add cover art if provided if coverPath != "" { if fileExists(coverPath) { coverData, err := os.ReadFile(coverPath) if err != nil { fmt.Printf("[Metadata] Warning: Failed to read cover file %s: %v\n", coverPath, err) } else { - // Remove existing picture blocks first (like PC version) for i := len(f.Meta) - 1; i >= 0; i-- { if f.Meta[i].Type == flac.Picture { f.Meta = append(f.Meta[:i], f.Meta[i+1:]...) @@ -137,7 +135,6 @@ func EmbedMetadataWithCoverData(filePath string, metadata Metadata, coverData [] return fmt.Errorf("failed to parse FLAC file: %w", err) } - // Find or create vorbis comment block var cmtIdx int = -1 var cmt *flacvorbis.MetaDataBlockVorbisComment @@ -196,9 +193,7 @@ func EmbedMetadataWithCoverData(filePath string, metadata Metadata, coverData [] f.Meta = append(f.Meta, &cmtBlock) } - // Add cover art if provided if len(coverData) > 0 { - // Remove existing picture blocks first for i := len(f.Meta) - 1; i >= 0; i-- { if f.Meta[i].Type == flac.Picture { f.Meta = append(f.Meta[:i], f.Meta[i+1:]...) @@ -220,7 +215,6 @@ func EmbedMetadataWithCoverData(filePath string, metadata Metadata, coverData [] } } - // Save file return f.Save(filePath) } @@ -257,7 +251,6 @@ func ReadMetadata(filePath string) (*Metadata, error) { if trackNum != "" { fmt.Sscanf(trackNum, "%d", &metadata.TrackNumber) } - // Also try lowercase variant (some encoders use lowercase) if metadata.TrackNumber == 0 { trackNum = getComment(cmt, "TRACK") if trackNum != "" { @@ -269,7 +262,6 @@ func ReadMetadata(filePath string) (*Metadata, error) { if discNum != "" { fmt.Sscanf(discNum, "%d", &metadata.DiscNumber) } - // Also try DISC variant if metadata.DiscNumber == 0 { discNum = getComment(cmt, "DISC") if discNum != "" { @@ -277,7 +269,6 @@ func ReadMetadata(filePath string) (*Metadata, error) { } } - // Try DATE variants if metadata.Date == "" { metadata.Date = getComment(cmt, "YEAR") } @@ -293,7 +284,6 @@ func setComment(cmt *flacvorbis.MetaDataBlockVorbisComment, key, value string) { if value == "" { return } - // Remove existing (case-insensitive comparison for Vorbis comments) keyUpper := strings.ToUpper(key) for i := len(cmt.Comments) - 1; i >= 0; i-- { comment := cmt.Comments[i] @@ -305,7 +295,6 @@ func setComment(cmt *flacvorbis.MetaDataBlockVorbisComment, key, value string) { } } } - // Add new cmt.Comments = append(cmt.Comments, key+"="+value) } @@ -313,7 +302,6 @@ func getComment(cmt *flacvorbis.MetaDataBlockVorbisComment, key string) string { keyUpper := strings.ToUpper(key) + "=" for _, comment := range cmt.Comments { if len(comment) > len(key) { - // Case-insensitive comparison for Vorbis comments commentUpper := strings.ToUpper(comment[:len(key)+1]) if commentUpper == keyUpper { return comment[len(key)+1:] diff --git a/go_backend/tidal.go b/go_backend/tidal.go index 6373501b..d34e5c2a 100644 --- a/go_backend/tidal.go +++ b/go_backend/tidal.go @@ -194,7 +194,6 @@ func (t *TidalDownloader) GetAccessToken() (string, error) { return "", err } - // Cache the token t.cachedToken = result.AccessToken if result.ExpiresIn > 0 { t.tokenExpiresAt = time.Now().Add(time.Duration(result.ExpiresIn) * time.Second) @@ -662,12 +661,10 @@ func getDownloadURLParallel(apis []string, trackID int64, quality string) (strin resultChan := make(chan tidalAPIResult, len(apis)) startTime := time.Now() - // Start all requests in parallel for _, apiURL := range apis { go func(api string) { reqStart := time.Now() - // Create client with timeout for parallel requests client := &http.Client{ Timeout: 15 * time.Second, } @@ -698,7 +695,6 @@ func getDownloadURLParallel(apis []string, trackID int64, quality string) (strin return } - // Try v2 format first (object with manifest) var v2Response TidalAPIResponseV2 if err := json.Unmarshal(body, &v2Response); err == nil && v2Response.Data.Manifest != "" { // IMPORTANT: Reject PREVIEW responses - we need FULL tracks @@ -716,7 +712,6 @@ func getDownloadURLParallel(apis []string, trackID int64, quality string) (strin return } - // Fallback to v1 format (array with OriginalTrackUrl) var v1Responses []struct { OriginalTrackURL string `json:"OriginalTrackUrl"` } @@ -738,13 +733,11 @@ func getDownloadURLParallel(apis []string, trackID int64, quality string) (strin }(apiURL) } - // Collect results - return first success var errors []string for i := 0; i < len(apis); i++ { result := <-resultChan if result.err == nil { - // First success - use this one GoLog("[Tidal] [Parallel] ✓ Got response from %s (%d-bit/%dHz) in %v\n", result.apiURL, result.info.BitDepth, result.info.SampleRate, result.duration) @@ -777,7 +770,6 @@ func (t *TidalDownloader) GetDownloadURL(trackID int64, quality string) (TidalDo return TidalDownloadInfo{}, fmt.Errorf("no API URL configured") } - // Use parallel approach - request from all APIs simultaneously _, info, err := getDownloadURLParallel(apis, trackID, quality) if err != nil { return TidalDownloadInfo{}, fmt.Errorf("failed to get download URL: %w", err) @@ -795,16 +787,13 @@ func parseManifest(manifestB64 string) (directURL string, initURL string, mediaU manifestStr := string(manifestBytes) - // Debug: log first 500 chars of manifest for debugging manifestPreview := manifestStr if len(manifestPreview) > 500 { manifestPreview = manifestPreview[:500] + "..." } GoLog("[Tidal] Manifest content: %s\n", manifestPreview) - // Check if it's BTS format (JSON) or DASH format (XML) if strings.HasPrefix(manifestStr, "{") { - // BTS format - JSON with direct URLs var btsManifest TidalBTSManifest if err := json.Unmarshal(manifestBytes, &btsManifest); err != nil { return "", "", nil, fmt.Errorf("failed to parse BTS manifest: %w", err) @@ -817,7 +806,6 @@ func parseManifest(manifestB64 string) (directURL string, initURL string, mediaU return btsManifest.URLs[0], "", nil, nil } - // DASH format - XML with segments var mpd MPD if err := xml.Unmarshal(manifestBytes, &mpd); err != nil { return "", "", nil, fmt.Errorf("failed to parse manifest XML: %w", err) @@ -828,7 +816,6 @@ func parseManifest(manifestB64 string) (directURL string, initURL string, mediaU mediaTemplate := segTemplate.Media if initURL == "" || mediaTemplate == "" { - // Fallback: try regex extraction initRe := regexp.MustCompile(`initialization="([^"]+)"`) mediaRe := regexp.MustCompile(`media="([^"]+)"`) @@ -844,11 +831,9 @@ func parseManifest(manifestB64 string) (directURL string, initURL string, mediaU return "", "", nil, fmt.Errorf("no initialization URL found in manifest") } - // Unescape HTML entities in URLs initURL = strings.ReplaceAll(initURL, "&", "&") mediaTemplate = strings.ReplaceAll(mediaTemplate, "&", "&") - // Calculate segment count from timeline segmentCount := 0 GoLog("[Tidal] XML parsed segments: %d entries in timeline\n", len(segTemplate.Timeline.Segments)) for i, seg := range segTemplate.Timeline.Segments { @@ -857,10 +842,8 @@ func parseManifest(manifestB64 string) (directURL string, initURL string, mediaU } GoLog("[Tidal] Segment count from XML: %d\n", segmentCount) - // If no segments found via XML, try regex if segmentCount == 0 { fmt.Println("[Tidal] No segments from XML, trying regex...") - // Match or segRe := regexp.MustCompile(` 0 && itemID != "" { SetItemBytesTotal(itemID, expectedSize) } @@ -946,24 +925,19 @@ func (t *TidalDownloader) DownloadFile(downloadURL, outputPath, itemID string) e return err } - // Use buffered writer for better performance (256KB buffer) bufWriter := bufio.NewWriterSize(out, 256*1024) - // Use item progress writer with buffered output var written int64 if itemID != "" { progressWriter := NewItemProgressWriter(bufWriter, itemID) written, err = io.Copy(progressWriter, resp.Body) } else { - // Fallback: direct copy without progress tracking written, err = io.Copy(bufWriter, resp.Body) } - // Flush buffer before checking for errors flushErr := bufWriter.Flush() closeErr := out.Close() - // Check for any errors if err != nil { os.Remove(outputPath) if isDownloadCancelled(itemID) { @@ -980,7 +954,6 @@ func (t *TidalDownloader) DownloadFile(downloadURL, outputPath, itemID string) e return fmt.Errorf("failed to close file: %w", closeErr) } - // Verify file size if Content-Length was provided if expectedSize > 0 && written != expectedSize { os.Remove(outputPath) return fmt.Errorf("incomplete download: expected %d bytes, got %d bytes", expectedSize, written) @@ -1003,7 +976,6 @@ func (t *TidalDownloader) downloadFromManifest(ctx context.Context, manifestB64, Timeout: 120 * time.Second, } - // If we have a direct URL (BTS format), download directly with progress tracking if directURL != "" { GoLog("[Tidal] BTS format - downloading from direct URL: %s...\n", directURL[:min(80, len(directURL))]) // Note: Progress tracking is initialized by the caller (DownloadFile) @@ -1035,7 +1007,6 @@ func (t *TidalDownloader) downloadFromManifest(ctx context.Context, manifestB64, GoLog("[Tidal] BTS response OK, Content-Length: %d\n", resp.ContentLength) expectedSize := resp.ContentLength - // Set total bytes for progress tracking if expectedSize > 0 && itemID != "" { SetItemBytesTotal(itemID, expectedSize) } @@ -1045,7 +1016,6 @@ func (t *TidalDownloader) downloadFromManifest(ctx context.Context, manifestB64, return fmt.Errorf("failed to create file: %w", err) } - // Use item progress writer var written int64 if itemID != "" { progressWriter := NewItemProgressWriter(out, itemID) @@ -1068,7 +1038,6 @@ func (t *TidalDownloader) downloadFromManifest(ctx context.Context, manifestB64, return fmt.Errorf("failed to close file: %w", closeErr) } - // Verify file size if Content-Length was provided if expectedSize > 0 && written != expectedSize { os.Remove(outputPath) return fmt.Errorf("incomplete download: expected %d bytes, got %d bytes", expectedSize, written) @@ -1077,21 +1046,15 @@ func (t *TidalDownloader) downloadFromManifest(ctx context.Context, manifestB64, return nil } - // DASH format - download segments directly to M4A file (no temp file to avoid Android permission issues) - // On Android, we can't use ffmpeg, so we save as M4A directly m4aPath := strings.TrimSuffix(outputPath, ".flac") + ".m4a" GoLog("[Tidal] DASH format - downloading %d segments directly to: %s\n", len(mediaURLs), m4aPath) - // Note: Progress tracking is initialized by the caller (DownloadFile or downloadFromTidal) - // We just update progress here based on segment count - out, err := os.Create(m4aPath) if err != nil { GoLog("[Tidal] Failed to create M4A file: %v\n", err) return fmt.Errorf("failed to create M4A file: %w", err) } - // Download initialization segment GoLog("[Tidal] Downloading init segment...\n") if isDownloadCancelled(itemID) { out.Close() @@ -1134,7 +1097,6 @@ func (t *TidalDownloader) downloadFromManifest(ctx context.Context, manifestB64, return fmt.Errorf("failed to write init segment: %w", err) } - // Download media segments with progress totalSegments := len(mediaURLs) for i, mediaURL := range mediaURLs { if isDownloadCancelled(itemID) { @@ -1147,7 +1109,6 @@ func (t *TidalDownloader) downloadFromManifest(ctx context.Context, manifestB64, GoLog("[Tidal] Downloading segment %d/%d...\n", i+1, totalSegments) } - // Update progress based on segment count if itemID != "" { progress := float64(i+1) / float64(totalSegments) SetItemProgress(itemID, progress, 0, 0) diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index 3c98cdc6..d7a32bac 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -45,7 +45,6 @@ class DownloadHistoryItem { final int? duration; final String? releaseDate; final String? quality; - // Audio quality info (from file after download) final int? bitDepth; final int? sampleRate; @@ -141,7 +140,6 @@ class DownloadHistoryNotifier extends Notifier { @override DownloadHistoryState build() { - // Load history from storage on init _loadFromStorageSync(); return DownloadHistoryState(); } @@ -165,13 +163,11 @@ class DownloadHistoryNotifier extends Notifier { .map((e) => DownloadHistoryItem.fromJson(e as Map)) .toList(); - // Deduplicate existing history on load final deduplicatedItems = _deduplicateHistory(items); state = state.copyWith(items: deduplicatedItems); _historyLog.i('Loaded ${deduplicatedItems.length} items from storage (original: ${items.length})'); - // Save if duplicates were removed if (deduplicatedItems.length < items.length) { _historyLog.i('Removed ${items.length - deduplicatedItems.length} duplicate entries'); await _saveToStorage(); @@ -194,9 +190,7 @@ class DownloadHistoryNotifier extends Notifier { final item = items[i]; String? key; - // Generate unique key based on available identifiers if (item.spotifyId != null && item.spotifyId!.isNotEmpty) { - // Extract numeric ID for deezer: prefixed IDs if (item.spotifyId!.startsWith('deezer:')) { key = 'deezer:${item.spotifyId!.substring(7)}'; } else { @@ -208,11 +202,9 @@ class DownloadHistoryNotifier extends Notifier { if (key != null) { if (!seen.containsKey(key)) { - // First occurrence - keep it (most recent since list is sorted by date desc) seen[key] = result.length; result.add(item); } else { - // Duplicate found - skip (keep the first/most recent one) _historyLog.d('Skipping duplicate: ${item.trackName} (key: $key)'); } } else { @@ -241,9 +233,7 @@ class DownloadHistoryNotifier extends Notifier { } void addToHistory(DownloadHistoryItem item) { - // Check if track already exists in history (by spotifyId, deezerId, or ISRC) final existingIndex = state.items.indexWhere((existing) { - // Match by spotifyId (primary identifier - includes deezer:xxx format) if (item.spotifyId != null && item.spotifyId!.isNotEmpty && existing.spotifyId == item.spotifyId) { @@ -253,14 +243,13 @@ class DownloadHistoryNotifier extends Notifier { // Match Deezer tracks: extract numeric ID from "deezer:123456" format if (item.spotifyId != null && item.spotifyId!.startsWith('deezer:') && existing.spotifyId != null && existing.spotifyId!.startsWith('deezer:')) { - final itemDeezerId = item.spotifyId!.substring(7); // Remove "deezer:" prefix + final itemDeezerId = item.spotifyId!.substring(7); final existingDeezerId = existing.spotifyId!.substring(7); if (itemDeezerId == existingDeezerId) { return true; } } - // Fallback: match by ISRC if spotifyId not available if (item.isrc != null && item.isrc!.isNotEmpty && existing.isrc == item.isrc) { @@ -279,7 +268,6 @@ class DownloadHistoryNotifier extends Notifier { state = state.copyWith(items: updatedItems); _historyLog.d('Updated existing history entry: ${item.trackName}'); } else { - // Add new entry state = state.copyWith(items: [item, ...state.items]); _historyLog.d('Added new history entry: ${item.trackName}'); } @@ -402,7 +390,6 @@ class DownloadQueueNotifier extends Notifier { _progressTimer = null; }); - // Initialize output directory and load persisted queue asynchronously Future.microtask(() async { await _initOutputDir(); await _loadQueueFromStorage(); @@ -432,7 +419,6 @@ class DownloadQueueNotifier extends Notifier { return item; }).toList(); - // Only restore queued/downloading items (not completed/failed/skipped) final pendingItems = restoredItems .where((item) => item.status == DownloadStatus.queued) .toList(); @@ -461,7 +447,6 @@ class DownloadQueueNotifier extends Notifier { try { final prefs = await SharedPreferences.getInstance(); - // Only persist queued and downloading items final pendingItems = state.items .where( (item) => @@ -471,7 +456,6 @@ class DownloadQueueNotifier extends Notifier { .toList(); if (pendingItems.isEmpty) { - // Clear storage if no pending items await prefs.remove(_queueStorageKey); _log.d('Cleared queue storage (no pending items)'); } else { @@ -523,12 +507,9 @@ class DownloadQueueNotifier extends Notifier { itemProgress['is_downloading'] as bool? ?? false; final status = itemProgress['status'] as String? ?? 'downloading'; - // Check if status is "finalizing" (embedding metadata) - // Only trust finalizing status if bytesTotal > 0 (download actually happened) if (status == 'finalizing' && bytesTotal > 0) { updateItemStatus(itemId, DownloadStatus.finalizing, progress: 1.0); - // Track finalizing item for notification final currentItem = state.items .where((i) => i.id == itemId) .firstOrNull; @@ -540,7 +521,6 @@ class DownloadQueueNotifier extends Notifier { continue; } - // Use progress from backend if available (handles both explicit progress and byte-based) final progressFromBackend = (itemProgress['progress'] as num?)?.toDouble() ?? 0.0; @@ -556,7 +536,6 @@ class DownloadQueueNotifier extends Notifier { updateProgress(itemId, percentage, speedMBps: speedMBps); - // Log progress for each item with speed final mbReceived = bytesReceived / (1024 * 1024); final mbTotal = bytesTotal / (1024 * 1024); if (bytesTotal > 0) { @@ -571,7 +550,6 @@ class DownloadQueueNotifier extends Notifier { } } - // Show finalizing notification if any item is finalizing (takes priority) if (hasFinalizingItem && finalizingTrackName != null) { _notificationService.showDownloadFinalizing( trackName: finalizingTrackName, @@ -592,7 +570,6 @@ class DownloadQueueNotifier extends Notifier { .where((i) => i.status == DownloadStatus.downloading) .toList(); if (downloadingItems.isNotEmpty) { - // Show single track name if only 1 download, otherwise show count final trackName = downloadingItems.length == 1 ? downloadingItems.first.track.name : '${downloadingItems.length} downloads'; @@ -600,12 +577,10 @@ class DownloadQueueNotifier extends Notifier { ? downloadingItems.first.track.artistName : 'Downloading...'; - // Calculate notification progress values int notifProgress = bytesReceived; int notifTotal = bytesTotal; if (bytesTotal <= 0) { - // Fallback to percentage for DASH/unknown size final progressPercent = (firstProgress['progress'] as num?)?.toDouble() ?? 0.0; notifProgress = (progressPercent * 100).toInt(); @@ -616,10 +591,9 @@ class DownloadQueueNotifier extends Notifier { trackName: trackName, artistName: artistName, progress: notifProgress, - total: notifTotal > 0 ? notifTotal : 1, - ); + total: notifTotal > 0 ? notifTotal : 1, + ); - // Update foreground service notification (Android) if (Platform.isAndroid) { PlatformBridge.updateDownloadServiceProgress( trackName: downloadingItems.first.track.name, @@ -632,7 +606,6 @@ class DownloadQueueNotifier extends Notifier { } } } catch (e) { - // Ignore polling errors } }); } @@ -665,7 +638,6 @@ class DownloadQueueNotifier extends Notifier { } state = state.copyWith(outputDir: musicDir.path); } else { - // Fallback to documents directory final docDir = await getApplicationDocumentsDirectory(); final musicDir = Directory('${docDir.path}/SpotiFLAC'); if (!await musicDir.exists()) { @@ -675,7 +647,6 @@ class DownloadQueueNotifier extends Notifier { } } } catch (e) { - // Fallback for any platform final dir = await getApplicationDocumentsDirectory(); final musicDir = Directory('${dir.path}/SpotiFLAC'); if (!await musicDir.exists()) { @@ -695,12 +666,10 @@ class DownloadQueueNotifier extends Notifier { String baseDir = state.outputDir; final albumArtist = _normalizeOptionalString(track.albumArtist) ?? track.artistName; - // If separateSingles is enabled, use Albums/Singles structure if (separateSingles) { final isSingle = track.isSingle; if (isSingle) { - // Singles go to Singles folder (flat structure) final singlesPath = '$baseDir${Platform.pathSeparator}Singles'; final dir = Directory(singlesPath); if (!await dir.exists()) { @@ -709,7 +678,6 @@ class DownloadQueueNotifier extends Notifier { } return singlesPath; } else { - // Albums folder structure based on setting final albumName = _sanitizeFolderName(track.albumName); final artistName = _sanitizeFolderName(albumArtist); final year = _extractYear(track.releaseDate); @@ -726,12 +694,10 @@ class DownloadQueueNotifier extends Notifier { albumPath = '$baseDir${Platform.pathSeparator}Albums${Platform.pathSeparator}$artistName${Platform.pathSeparator}$yearAlbum'; break; case 'year_album': - // Albums/[Year] Album structure (no artist folder) final yearAlbum = year != null ? '[$year] $albumName' : albumName; albumPath = '$baseDir${Platform.pathSeparator}Albums${Platform.pathSeparator}$yearAlbum'; break; default: - // Albums/Artist/Album structure (default: artist_album) albumPath = '$baseDir${Platform.pathSeparator}Albums${Platform.pathSeparator}$artistName${Platform.pathSeparator}$albumName'; } @@ -951,7 +917,6 @@ class DownloadQueueNotifier extends Notifier { if (state.isPaused) { state = state.copyWith(isPaused: false); _log.i('Queue resumed'); - // If there are still queued items, continue processing if (state.queuedCount > 0 && !state.isProcessing) { Future.microtask(() => _processQueue()); } @@ -995,9 +960,8 @@ class DownloadQueueNotifier extends Notifier { return i; }).toList(); state = state.copyWith(items: items); - _saveQueueToStorage(); // Persist queue + _saveQueueToStorage(); - // Start processing if not already running if (!state.isProcessing) { _log.d('Starting queue processing for retry'); Future.microtask(() => _processQueue()); @@ -1093,7 +1057,6 @@ class DownloadQueueNotifier extends Notifier { var coverUrl = track.coverUrl; if (coverUrl != null && coverUrl.isNotEmpty) { try { - // Upgrade cover URL to max quality if setting is enabled if (settings.maxQualityCover) { coverUrl = _upgradeToMaxQualityCover(coverUrl); _log.d('Cover URL upgraded to max quality: $coverUrl'); @@ -1104,7 +1067,6 @@ class DownloadQueueNotifier extends Notifier { '${DateTime.now().millisecondsSinceEpoch}_${Random().nextInt(10000)}'; coverPath = '${tempDir.path}/cover_$uniqueId.jpg'; - // Download cover using HTTP final httpClient = HttpClient(); final request = await httpClient.getUrl(Uri.parse(coverUrl)); final response = await request.close(); @@ -1125,12 +1087,7 @@ class DownloadQueueNotifier extends Notifier { } } - // Use Go backend to embed metadata try { - // Use FFmpeg to embed cover art AND text metadata - // FFmpeg can embed cover art to FLAC and also set tags - - // Construct metadata map final metadata = { 'TITLE': track.name, 'ARTIST': track.artistName, diff --git a/lib/providers/recent_access_provider.dart b/lib/providers/recent_access_provider.dart index 0882b39f..9d383cb3 100644 --- a/lib/providers/recent_access_provider.dart +++ b/lib/providers/recent_access_provider.dart @@ -112,7 +112,6 @@ class RecentAccessNotifier extends Notifier { .toList(); state = state.copyWith(items: items, isLoaded: true); } catch (e) { - // Invalid JSON, start fresh state = state.copyWith(isLoaded: true); } } else { @@ -210,10 +209,8 @@ class RecentAccessNotifier extends Notifier { .where((e) => e.uniqueKey != item.uniqueKey) .toList(); - // Add new item at the beginning updatedItems.insert(0, item); - // Limit to max items if (updatedItems.length > _maxRecentItems) { updatedItems.removeRange(_maxRecentItems, updatedItems.length); } @@ -221,7 +218,6 @@ class RecentAccessNotifier extends Notifier { state = state.copyWith(items: updatedItems); _saveHistory(); - // Debug log // ignore: avoid_print print('[RecentAccess] Total items now: ${updatedItems.length}'); } diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index 39bb3900..1983293f 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -22,13 +22,10 @@ class SettingsNotifier extends Notifier { if (json != null) { state = AppSettings.fromJson(jsonDecode(json)); - // Run migrations if needed await _runMigrations(prefs); - // Apply Spotify credentials to Go backend on load _applySpotifyCredentials(); - // Sync logging state LogBuffer.loggingEnabled = state.enableLogging; } } @@ -38,16 +35,12 @@ class SettingsNotifier extends Notifier { final lastMigration = prefs.getInt(_migrationVersionKey) ?? 0; if (lastMigration < 1) { - // Migration 1: Set metadataSource to 'deezer' for existing users - // Only apply if user hasn't enabled custom Spotify credentials - // (users with custom credentials likely prefer Spotify) if (!state.useCustomSpotifyCredentials) { state = state.copyWith(metadataSource: 'deezer'); await _saveSettings(); } } - // Save current migration version if (lastMigration < _currentMigrationVersion) { await prefs.setInt(_migrationVersionKey, _currentMigrationVersion); } @@ -68,8 +61,6 @@ class SettingsNotifier extends Notifier { state.spotifyClientSecret, ); } - // Note: If credentials are empty, Spotify API will return error - // User should use Deezer as metadata source instead } void setDefaultService(String service) { @@ -113,7 +104,6 @@ class SettingsNotifier extends Notifier { } void setConcurrentDownloads(int count) { - // Clamp between 1 and 3 final clamped = count.clamp(1, 3); state = state.copyWith(concurrentDownloads: clamped); _saveSettings(); diff --git a/lib/providers/track_provider.dart b/lib/providers/track_provider.dart index 83848520..49f65bf6 100644 --- a/lib/providers/track_provider.dart +++ b/lib/providers/track_provider.dart @@ -142,14 +142,11 @@ class TrackNotifier extends Notifier { bool _isRequestValid(int requestId) => requestId == _currentRequestId; Future fetchFromUrl(String url, {bool useDeezerFallback = true}) async { - // Increment request ID to cancel any pending requests final requestId = ++_currentRequestId; - // Preserve hasSearchText during fetch state = TrackState(isLoading: true, hasSearchText: state.hasSearchText); try { - // First, check if any extension can handle this URL final extensionHandler = await PlatformBridge.findURLHandler(url); if (extensionHandler != null) { _log.i('Found extension URL handler: $extensionHandler for URL: $url'); @@ -188,7 +185,6 @@ class TrackNotifier extends Notifier { final albumsList = artistData['albums'] as List? ?? []; final albums = albumsList.map((a) => _parseArtistAlbum(a as Map)).toList(); - // Parse top tracks if available final topTracksList = artistData['top_tracks'] as List? ?? []; final topTracks = topTracksList.map((t) => _parseSearchTrack(t as Map, source: extensionId)).toList(); @@ -209,13 +205,11 @@ class TrackNotifier extends Notifier { } } - // No extension handler found, try Spotify URL parsing final parsed = await PlatformBridge.parseSpotifyUrl(url); if (!_isRequestValid(requestId)) return; // Request cancelled final type = parsed['type'] as String; - // Use the new fallback-enabled method Map metadata; try { @@ -225,7 +219,6 @@ class TrackNotifier extends Notifier { // ignore: avoid_print print('[FetchURL] Metadata fetch success'); } catch (e) { - // If fallback also fails, show error // ignore: avoid_print print('[FetchURL] Metadata fetch failed: $e'); rethrow; @@ -252,7 +245,6 @@ class TrackNotifier extends Notifier { albumName: albumInfo['name'] as String?, coverUrl: albumInfo['images'] as String?, ); - // Pre-warm cache for album tracks in background _preWarmCacheForTracks(tracks); } else if (type == 'playlist') { final playlistInfo = metadata['playlist_info'] as Map; @@ -281,8 +273,7 @@ class TrackNotifier extends Notifier { ); } } catch (e) { - if (!_isRequestValid(requestId)) return; // Request cancelled - // Preserve hasSearchText on error so user stays on search screen + if (!_isRequestValid(requestId)) return; state = TrackState(isLoading: false, error: e.toString(), hasSearchText: state.hasSearchText); } } @@ -295,7 +286,6 @@ class TrackNotifier extends Notifier { state = TrackState(isLoading: true, hasSearchText: state.hasSearchText); try { - // Check if extension providers should be used for search final settings = ref.read(settingsProvider); final extensionState = ref.read(extensionProvider); final hasActiveMetadataExtensions = extensionState.extensions.any( @@ -308,7 +298,6 @@ class TrackNotifier extends Notifier { searchProvider != null && searchProvider.isNotEmpty; - // Use Deezer or Spotify based on settings final source = metadataSource ?? 'deezer'; _log.i( @@ -318,14 +307,12 @@ class TrackNotifier extends Notifier { Map results; List extensionTracks = []; - // Try extension providers first if enabled if (useExtensions) { try { _log.d('Calling extension search API...'); final extResults = await PlatformBridge.searchTracksWithExtensions(query, limit: 20); _log.i('Extensions returned ${extResults.length} tracks'); - // Parse extension results for (final t in extResults) { try { extensionTracks.add(_parseSearchTrack(t)); @@ -338,7 +325,6 @@ class TrackNotifier extends Notifier { } } - // Also search with built-in providers if (source == 'deezer') { _log.d('Calling Deezer search API...'); results = await PlatformBridge.searchDeezerAll(query, trackLimit: 20, artistLimit: 5); @@ -365,7 +351,6 @@ class TrackNotifier extends Notifier { // Add extension tracks first (they have priority) tracks.addAll(extensionTracks); - // Add built-in provider tracks, avoiding duplicates by ISRC final existingIsrcs = extensionTracks .where((t) => t.isrc != null && t.isrc!.isNotEmpty) .map((t) => t.isrc!) @@ -376,7 +361,6 @@ class TrackNotifier extends Notifier { try { if (t is Map) { final track = _parseSearchTrack(t); - // Skip if we already have this track from extensions if (track.isrc != null && existingIsrcs.contains(track.isrc)) { continue; } @@ -389,7 +373,6 @@ class TrackNotifier extends Notifier { } } - // Parse artists with error handling per item final artists = []; for (int i = 0; i < artistList.length; i++) { final a = artistList[i]; @@ -439,7 +422,6 @@ class TrackNotifier extends Notifier { _log.i('Custom search returned ${results.length} tracks'); - // Parse tracks with error handling per item, setting source to extension ID final tracks = []; for (int i = 0; i < results.length; i++) { final t = results[i]; @@ -563,7 +545,6 @@ class TrackNotifier extends Notifier { durationMs = durationValue.toInt(); } - // Get item_type - can be 'track', 'album', or 'playlist' final itemType = data['item_type']?.toString(); return Track( @@ -620,13 +601,10 @@ class TrackNotifier extends Notifier { 'track_name': t.name, 'artist_name': t.artistName, 'spotify_id': t.id, // Include Spotify ID for Amazon lookup - 'service': 'tidal', // Default to tidal for pre-warming + 'service': 'tidal', }).toList(); - // Fire and forget - runs in background - PlatformBridge.preWarmTrackCache(cacheRequests).catchError((_) { - // Silently ignore errors - this is just an optimization - }); + PlatformBridge.preWarmTrackCache(cacheRequests).catchError((_) {}); } } diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index cd6ae319..7714515e 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -52,11 +52,9 @@ class _QueueTabState extends ConsumerState { final Set _pendingChecks = {}; static const int _maxCacheSize = 500; - // Multi-select state bool _isSelectionMode = false; final Set _selectedIds = {}; - // Filter page controller for swipe between All/Albums/Singles PageController? _filterPageController; final List _filterModes = ['all', 'albums', 'singles']; bool _isPageControllerInitialized = false; @@ -66,7 +64,6 @@ class _QueueTabState extends ConsumerState { @override void initState() { super.initState(); - // Will be initialized in build when we have access to ref } void _initializePageController() { @@ -291,7 +288,6 @@ class _QueueTabState extends ConsumerState { ) { if (filterMode == 'all') return items; - // Count tracks per album final albumCounts = {}; for (final item in items) { final key = '${item.albumName}|${item.albumArtist ?? item.artistName}'; @@ -307,7 +303,6 @@ class _QueueTabState extends ConsumerState { return (albumCounts[key] ?? 0) > 1; }).toList(); case 'singles': - // Single = only 1 track from that album in history return items.where((item) { final key = '${item.albumName}|${item.albumArtist ?? item.artistName}'; @@ -320,7 +315,6 @@ class _QueueTabState extends ConsumerState { /// Count albums vs singles for filter chips Map _countAlbumsAndSingles(List items) { - // Count tracks per album final albumCounts = {}; for (final item in items) { final key = '${item.albumName}|${item.albumArtist ?? item.artistName}'; @@ -351,11 +345,9 @@ class _QueueTabState extends ConsumerState { albumMap.putIfAbsent(key, () => []).add(item); } - // Only include albums with more than 1 track final groupedAlbums = albumMap.entries.where((e) => e.value.length > 1).map( (e) { final tracks = e.value; - // Sort tracks by track number tracks.sort((a, b) { final aNum = a.trackNumber ?? 999; final bNum = b.trackNumber ?? 999; @@ -374,7 +366,6 @@ class _QueueTabState extends ConsumerState { }, ).toList(); - // Sort by latest download groupedAlbums.sort((a, b) => b.latestDownload.compareTo(a.latestDownload)); return groupedAlbums; @@ -447,10 +438,8 @@ class _QueueTabState extends ConsumerState { final colorScheme = Theme.of(context).colorScheme; final topPadding = MediaQuery.of(context).padding.top; - // Group albums for Albums filter view final groupedAlbums = _groupByAlbum(allHistoryItems); - // Count for filter chips final counts = _countAlbumsAndSingles(allHistoryItems); final albumCount = _countUniqueAlbums(allHistoryItems); final singleCount = counts['singles'] ?? 0; @@ -468,7 +457,6 @@ class _QueueTabState extends ConsumerState { children: [ NestedScrollView( headerSliverBuilder: (context, innerBoxIsScrolled) => [ - // App Bar - always normal style SliverAppBar( expandedHeight: 120 + topPadding, collapsedHeight: kToolbarHeight, diff --git a/lib/screens/settings/about_page.dart b/lib/screens/settings/about_page.dart index f63d01cf..966a25ed 100644 --- a/lib/screens/settings/about_page.dart +++ b/lib/screens/settings/about_page.dart @@ -18,7 +18,6 @@ class AboutPage extends StatelessWidget { child: Scaffold( body: CustomScrollView( slivers: [ - // Collapsing App Bar with back button SliverAppBar( expandedHeight: 120 + topPadding, collapsedHeight: kToolbarHeight, @@ -35,9 +34,7 @@ class AboutPage extends StatelessWidget { final maxHeight = 120 + topPadding; final minHeight = kToolbarHeight + topPadding; final expandRatio = ((constraints.maxHeight - minHeight) / (maxHeight - minHeight)).clamp(0.0, 1.0); - // When collapsed (expandRatio=0): left=56 to avoid back button - // When expanded (expandRatio=1): left=24 for normal padding - final leftPadding = 56 - (32 * expandRatio); // 56 -> 24 + final leftPadding = 56 - (32 * expandRatio); return FlexibleSpaceBar( expandedTitleScale: 1.0, titlePadding: EdgeInsets.only(left: leftPadding, bottom: 16), @@ -62,7 +59,6 @@ class AboutPage extends StatelessWidget { ), ), - // Contributors section SliverToBoxAdapter( child: SettingsSectionHeader(title: context.l10n.aboutContributors), ), @@ -91,7 +87,6 @@ class AboutPage extends StatelessWidget { ), ), - // Special Thanks section SliverToBoxAdapter( child: SettingsSectionHeader(title: context.l10n.aboutSpecialThanks), ), @@ -128,7 +123,6 @@ class AboutPage extends StatelessWidget { ), ), - // Links section SliverToBoxAdapter( child: SettingsSectionHeader(title: context.l10n.aboutLinks), ), @@ -167,7 +161,6 @@ class AboutPage extends StatelessWidget { ), ), - // Support section SliverToBoxAdapter( child: SettingsSectionHeader(title: context.l10n.aboutSupport), ), @@ -185,7 +178,6 @@ class AboutPage extends StatelessWidget { ), ), - // App info section SliverToBoxAdapter( child: SettingsSectionHeader(title: context.l10n.aboutApp), ), @@ -202,7 +194,6 @@ class AboutPage extends StatelessWidget { ), ), - // Copyright SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.all(24), @@ -227,7 +218,6 @@ class AboutPage extends StatelessWidget { static Future _launchUrl(String url) async { final uri = Uri.parse(url); - // Use inAppBrowserView for reliable URL opening with app chooser await launchUrl(uri, mode: LaunchMode.inAppBrowserView); } } @@ -275,7 +265,6 @@ class _AppHeaderCard extends StatelessWidget { ), ), const SizedBox(height: 16), - // App name Text( AppInfo.appName, style: Theme.of(context).textTheme.headlineSmall?.copyWith( @@ -283,7 +272,6 @@ class _AppHeaderCard extends StatelessWidget { ), ), const SizedBox(height: 4), - // Version badge Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), decoration: BoxDecoration( @@ -299,7 +287,6 @@ class _AppHeaderCard extends StatelessWidget { ), ), const SizedBox(height: 16), - // Description Text( context.l10n.aboutAppDescription, textAlign: TextAlign.center, @@ -341,7 +328,6 @@ class _ContributorItem extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), child: Row( children: [ - // GitHub Avatar ClipRRect( borderRadius: BorderRadius.circular(12), child: CachedNetworkImage( @@ -372,7 +358,6 @@ class _ContributorItem extends StatelessWidget { ), ), const SizedBox(width: 16), - // Name and description Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -391,7 +376,6 @@ class _ContributorItem extends StatelessWidget { ], ), ), - // GitHub icon Icon(Icons.chevron_right, color: colorScheme.onSurfaceVariant), ], ), @@ -446,7 +430,6 @@ class _AboutSettingsItem extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), child: Row( children: [ - // Icon with 40x40 size to match avatar SizedBox( width: 40, height: 40, diff --git a/lib/screens/settings/appearance_settings_page.dart b/lib/screens/settings/appearance_settings_page.dart index ac1f55bf..c92e0d83 100644 --- a/lib/screens/settings/appearance_settings_page.dart +++ b/lib/screens/settings/appearance_settings_page.dart @@ -21,7 +21,6 @@ class AppearanceSettingsPage extends ConsumerWidget { child: Scaffold( body: CustomScrollView( slivers: [ - // Collapsing App Bar with back button SliverAppBar( expandedHeight: 120 + topPadding, collapsedHeight: kToolbarHeight, @@ -50,7 +49,6 @@ class AppearanceSettingsPage extends ConsumerWidget { ), ), - // Color section SliverToBoxAdapter( child: SettingsSectionHeader(title: context.l10n.sectionColor), ), @@ -80,10 +78,9 @@ class AppearanceSettingsPage extends ConsumerWidget { onColorSelected: (color) => ref.read(themeProvider.notifier).setSeedColor(color), ), - ), ), + ), - // Theme section SliverToBoxAdapter( child: SettingsSectionHeader(title: context.l10n.sectionTheme), ), @@ -109,7 +106,6 @@ class AppearanceSettingsPage extends ConsumerWidget { ), ), - // Language section SliverToBoxAdapter( child: SettingsSectionHeader(title: context.l10n.sectionLanguage), ), @@ -126,7 +122,6 @@ class AppearanceSettingsPage extends ConsumerWidget { ), ), - // Layout section SliverToBoxAdapter( child: SettingsSectionHeader(title: context.l10n.sectionLayout), ), @@ -143,7 +138,6 @@ class AppearanceSettingsPage extends ConsumerWidget { ), ), - // Fill remaining for scroll const SliverFillRemaining( hasScrollBody: false, child: SizedBox(height: 32), @@ -174,7 +168,6 @@ class _ThemePreviewCard extends StatelessWidget { clipBehavior: Clip.antiAlias, child: Stack( children: [ - // Decorative background blobs Positioned( top: -50, right: -50, @@ -200,7 +193,6 @@ class _ThemePreviewCard extends StatelessWidget { ), ), - // Foreground "fake UI" Center( child: Container( width: 260, @@ -235,7 +227,6 @@ class _ThemePreviewCard extends StatelessWidget { ), const SizedBox(width: 16), - // Fake Text Info Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -288,7 +279,6 @@ class _ThemePreviewCard extends StatelessWidget { ), ), - // Label badge Positioned( bottom: 12, right: 12, @@ -510,10 +500,7 @@ class _ThemeModeChip extends StatelessWidget { Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final isDark = Theme.of(context).brightness == Brightness.dark; - - // Unselected chips need contrast with card background - // Card uses: dark = white 8% overlay, light = surfaceContainerHighest - // So chips use: dark = white 5% overlay (darker), light = black 5% overlay (darker than card) + final unselectedColor = isDark ? Color.alphaBlend( Colors.white.withValues(alpha: 0.05), @@ -732,15 +719,12 @@ class _LanguageSelector extends StatelessWidget { /// Uses filteredLocaleCodes from supported_locales.dart (generated file). List<(String, String, IconData)> get _languages { return _allLanguages.where((lang) { - // Always include 'system' option if (lang.$1 == 'system') return true; - // Only include languages in the filtered set return filteredLocaleCodes.contains(lang.$1); }).toList(); } String _getLanguageName(String code) { - // Search in all languages (not just filtered) for display name fallback for (final lang in _allLanguages) { if (lang.$1 == code) return lang.$2; } diff --git a/lib/screens/settings/download_settings_page.dart b/lib/screens/settings/download_settings_page.dart index 434cc9ae..0e81b508 100644 --- a/lib/screens/settings/download_settings_page.dart +++ b/lib/screens/settings/download_settings_page.dart @@ -11,7 +11,6 @@ import 'package:spotiflac_android/widgets/settings_group.dart'; class DownloadSettingsPage extends ConsumerWidget { const DownloadSettingsPage({super.key}); - // Built-in services that support quality options static const _builtInServices = ['tidal', 'qobuz', 'amazon']; @override @@ -20,7 +19,6 @@ class DownloadSettingsPage extends ConsumerWidget { final colorScheme = Theme.of(context).colorScheme; final topPadding = MediaQuery.of(context).padding.top; - // Check if current service is built-in (supports quality options) final isBuiltInService = _builtInServices.contains(settings.defaultService); return PopScope( @@ -28,7 +26,6 @@ class DownloadSettingsPage extends ConsumerWidget { child: Scaffold( body: CustomScrollView( slivers: [ - // Collapsing App Bar with back button SliverAppBar( expandedHeight: 120 + topPadding, collapsedHeight: kToolbarHeight, @@ -85,7 +82,6 @@ class DownloadSettingsPage extends ConsumerWidget { ), ), - // Quality section SliverToBoxAdapter( child: SettingsSectionHeader(title: context.l10n.sectionAudioQuality), ), @@ -99,7 +95,6 @@ class DownloadSettingsPage extends ConsumerWidget { ? context.l10n.downloadAskQualitySubtitle : 'Select a built-in service to enable', value: settings.askQualityBeforeDownload, - // Not selected visually if extension is active enabled: isBuiltInService, onChanged: (value) => ref .read(settingsProvider.notifier) @@ -159,7 +154,6 @@ class DownloadSettingsPage extends ConsumerWidget { ), ), - // File settings section SliverToBoxAdapter( child: SettingsSectionHeader(title: context.l10n.sectionFileSettings), ), @@ -321,11 +315,9 @@ class DownloadSettingsPage extends ConsumerWidget { String insertion = tag; if (start > 0) { final before = text.substring(0, start); - // Smart separator: if not starting a file and no hyphen separator exists, add " - " if (!before.trim().endsWith('-')) { insertion = ' - $tag'; } else if (before.trim().endsWith('-') && !before.endsWith(' ')) { - // If ends with '-' but no space, add space insertion = ' $tag'; } } @@ -697,12 +689,10 @@ class _ServiceSelector extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final extState = ref.watch(extensionProvider); - // Get enabled extension download providers final extensionProviders = extState.extensions .where((e) => e.enabled && e.hasDownloadProvider) .toList(); - // Check if current service is an extension that's now disabled final isExtensionService = !['tidal', 'qobuz', 'amazon'].contains(currentService); final isCurrentExtensionEnabled = isExtensionService ? extensionProviders.any((e) => e.id == currentService) @@ -739,7 +729,6 @@ class _ServiceSelector extends ConsumerWidget { ), ], ), - // Show extension download providers if any if (extensionProviders.isNotEmpty) ...[ const SizedBox(height: 8), Row( @@ -755,7 +744,6 @@ class _ServiceSelector extends ConsumerWidget { ), ), ], - // Fill remaining space if less than 3 extensions for (int i = extensionProviders.length; i < 3; i++) ...[ const SizedBox(width: 8), const Expanded(child: SizedBox()), diff --git a/lib/screens/settings/extension_detail_page.dart b/lib/screens/settings/extension_detail_page.dart index 323a8b96..34571577 100644 --- a/lib/screens/settings/extension_detail_page.dart +++ b/lib/screens/settings/extension_detail_page.dart @@ -62,7 +62,6 @@ class _ExtensionDetailPageState extends ConsumerState { child: Scaffold( body: CustomScrollView( slivers: [ - // App Bar SliverAppBar( expandedHeight: 120 + topPadding, collapsedHeight: kToolbarHeight, @@ -98,7 +97,6 @@ class _ExtensionDetailPageState extends ConsumerState { ), ), - // Extension Info Card SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.all(16), @@ -202,7 +200,6 @@ class _ExtensionDetailPageState extends ConsumerState { ), ), - // Capabilities SliverToBoxAdapter( child: SettingsSectionHeader(title: context.l10n.extensionCapabilities), ), @@ -254,9 +251,6 @@ class _ExtensionDetailPageState extends ConsumerState { ), ), - - - // URL Handler Section (if extension handles URLs) if (extension.hasURLHandler && extension.urlHandler!.patterns.isNotEmpty) ...[ SliverToBoxAdapter( child: SettingsSectionHeader(title: context.l10n.extensionUrlHandler), @@ -272,7 +266,6 @@ class _ExtensionDetailPageState extends ConsumerState { ), ], - // Quality Options Section (for download providers) if (extension.hasDownloadProvider && extension.qualityOptions.isNotEmpty) ...[ SliverToBoxAdapter( child: SettingsSectionHeader(title: context.l10n.extensionQualityOptions), @@ -291,7 +284,6 @@ class _ExtensionDetailPageState extends ConsumerState { ), ], - // Post-Processing Hooks (if available) if (extension.hasPostProcessing && extension.postProcessing!.hooks.isNotEmpty) ...[ SliverToBoxAdapter( child: SettingsSectionHeader(title: context.l10n.extensionPostProcessingHooks), @@ -310,7 +302,6 @@ class _ExtensionDetailPageState extends ConsumerState { ), ], - // Permissions if (extension.permissions.isNotEmpty) ...[ SliverToBoxAdapter( child: SettingsSectionHeader(title: context.l10n.extensionPermissions), @@ -329,7 +320,6 @@ class _ExtensionDetailPageState extends ConsumerState { ), ], - // Settings if (extension.settings.isNotEmpty) ...[ SliverToBoxAdapter( child: SettingsSectionHeader(title: context.l10n.extensionSettings), @@ -358,7 +348,6 @@ class _ExtensionDetailPageState extends ConsumerState { ), ], - // Remove button SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.all(16), @@ -424,7 +413,6 @@ class _ExtensionDetailPageState extends ConsumerState { .read(extensionProvider.notifier) .removeExtension(widget.extensionId); if (success && mounted) { - // Refresh store to update isInstalled status ref.read(storeProvider.notifier).refresh(); Navigator.pop(this.context); } @@ -557,7 +545,6 @@ class _PermissionItem extends StatelessWidget { Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; - // Parse permission to get icon and description IconData icon = Icons.security; String description = permission; diff --git a/lib/screens/settings/extensions_page.dart b/lib/screens/settings/extensions_page.dart index 234be119..2cce5c74 100644 --- a/lib/screens/settings/extensions_page.dart +++ b/lib/screens/settings/extensions_page.dart @@ -32,7 +32,6 @@ class _ExtensionsPageState extends ConsumerState { final extensionsDir = '${appDir.path}/extensions'; final dataDir = '${appDir.path}/extension_data'; - // Create directories if they don't exist await Directory(extensionsDir).create(recursive: true); await Directory(dataDir).create(recursive: true); @@ -87,7 +86,6 @@ class _ExtensionsPageState extends ConsumerState { ), ), - // Loading indicator if (extState.isLoading) const SliverToBoxAdapter( child: Padding( @@ -96,7 +94,6 @@ class _ExtensionsPageState extends ConsumerState { ), ), - // Error message if (extState.error != null) SliverToBoxAdapter( child: Padding( @@ -137,7 +134,6 @@ class _ExtensionsPageState extends ConsumerState { ), ), - // Installed Extensions SliverToBoxAdapter( child: SettingsSectionHeader(title: context.l10n.extensionsInstalledSection), ), @@ -203,7 +199,6 @@ class _ExtensionsPageState extends ConsumerState { ), ), - // Install button SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.all(16), @@ -284,11 +279,9 @@ class _ExtensionsPageState extends ConsumerState { if (success) { message = context.l10n.extensionsInstalledSuccess; } else { - // Parse friendly error message message = _getFriendlyErrorMessage(extState.error); } - // Clear the error from state to avoid showing it twice (in error container) ref.read(extensionProvider.notifier).clearError(); ScaffoldMessenger.of(context).showSnackBar( @@ -305,15 +298,11 @@ class _ExtensionsPageState extends ConsumerState { String message = error; - // Remove PlatformException wrapper if present - // Format: PlatformException(ERROR, actual message, null, null) if (message.contains('PlatformException')) { - // Try to extract the actual error message final match = RegExp(r'PlatformException\([^,]+,\s*([^,]+(?:,[^,]+)?),').firstMatch(message); if (match != null) { message = match.group(1)?.trim() ?? message; } else { - // Fallback: try simpler extraction final simpleMatch = RegExp(r'PlatformException\([^,]+,\s*(.+?),\s*null').firstMatch(message); if (simpleMatch != null) { message = simpleMatch.group(1)?.trim() ?? message; @@ -321,7 +310,6 @@ class _ExtensionsPageState extends ConsumerState { } } - // Clean up any remaining artifacts message = message.replaceAll(RegExp(r',\s*null\s*,\s*null\)?$'), ''); message = message.replaceAll(RegExp(r'^\s*,\s*'), ''); @@ -390,7 +378,6 @@ class _ExtensionItem extends StatelessWidget { ), ), const SizedBox(width: 16), - // Extension info Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -445,7 +432,6 @@ class _DownloadPriorityItem extends ConsumerWidget { final extState = ref.watch(extensionProvider); final colorScheme = Theme.of(context).colorScheme; - // Check if any extension has download provider final hasDownloadExtensions = extState.extensions .any((e) => e.enabled && e.hasDownloadProvider); @@ -584,12 +570,10 @@ class _SearchProviderSelector extends ConsumerWidget { final extState = ref.watch(extensionProvider); final colorScheme = Theme.of(context).colorScheme; - // Get extensions with custom search final searchProviders = extState.extensions .where((e) => e.enabled && e.hasCustomSearch) .toList(); - // Get current provider name String currentProviderName = context.l10n.extensionDefaultProvider; if (settings.searchProvider != null && settings.searchProvider!.isNotEmpty) { final ext = searchProviders.where((e) => e.id == settings.searchProvider).firstOrNull; @@ -689,7 +673,6 @@ class _SearchProviderSelector extends ConsumerWidget { ), ), ), - // Default option ListTile( leading: Icon(Icons.music_note, color: colorScheme.primary), title: Text(ctx.l10n.extensionDefaultProvider), @@ -702,7 +685,6 @@ class _SearchProviderSelector extends ConsumerWidget { Navigator.pop(ctx); }, ), - // Extension options ...searchProviders.map((ext) => ListTile( leading: Icon(Icons.extension, color: colorScheme.secondary), title: Text(ext.displayName), diff --git a/lib/screens/settings/log_screen.dart b/lib/screens/settings/log_screen.dart index f6c1eb3b..7598b338 100644 --- a/lib/screens/settings/log_screen.dart +++ b/lib/screens/settings/log_screen.dart @@ -25,14 +25,12 @@ class _LogScreenState extends State { void initState() { super.initState(); LogBuffer().addListener(_onLogUpdate); - // Start polling Go backend logs LogBuffer().startGoLogPolling(); } @override void dispose() { LogBuffer().removeListener(_onLogUpdate); - // Stop polling when leaving screen LogBuffer().stopGoLogPolling(); _scrollController.dispose(); _searchController.dispose(); @@ -131,7 +129,6 @@ class _LogScreenState extends State { body: CustomScrollView( controller: _scrollController, slivers: [ - // Collapsing App Bar with back button - same as other settings pages SliverAppBar( expandedHeight: 120 + topPadding, collapsedHeight: kToolbarHeight, @@ -208,7 +205,6 @@ class _LogScreenState extends State { ), ), - // Filter section SliverToBoxAdapter( child: SettingsSectionHeader(title: context.l10n.logFilterSection), ), @@ -269,7 +265,6 @@ class _LogScreenState extends State { endIndent: 20, color: colorScheme.outlineVariant.withValues(alpha: 0.3), ), - // Search field Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), child: Row( @@ -323,12 +318,10 @@ class _LogScreenState extends State { ), ), - // Error summary card - shows detected issues SliverToBoxAdapter( child: _LogSummaryCard(logs: LogBuffer().entries), ), - // Log list logs.isEmpty ? SliverToBoxAdapter( child: SettingsGroup( @@ -379,7 +372,6 @@ class _LogScreenState extends State { ), ), - // Bottom padding const SliverToBoxAdapter(child: SizedBox(height: 32)), ], ), @@ -418,7 +410,6 @@ class _LogEntryTile extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Header: time, level, tag Row( children: [ Text( @@ -478,7 +469,6 @@ class _LogEntryTile extends StatelessWidget { ], ), const SizedBox(height: 6), - // Message Text( entry.message, style: TextStyle( @@ -488,7 +478,6 @@ class _LogEntryTile extends StatelessWidget { height: 1.4, ), ), - // Error if present if (entry.error != null) ...[ const SizedBox(height: 4), Text( @@ -526,10 +515,8 @@ class _LogSummaryCard extends StatelessWidget { Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; - // Analyze logs for issues final analysis = _analyzeLogs(); - // Don't show if no issues detected if (!analysis.hasIssues) { return const SizedBox.shrink(); } @@ -547,7 +534,6 @@ class _LogSummaryCard extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Header Row( children: [ Icon( @@ -567,7 +553,6 @@ class _LogSummaryCard extends StatelessWidget { ), const SizedBox(height: 12), - // ISP Blocking detected if (analysis.hasISPBlocking) ...[ _IssueBadge( icon: Icons.block, @@ -580,7 +565,6 @@ class _LogSummaryCard extends StatelessWidget { const SizedBox(height: 8), ], - // Rate limiting if (analysis.hasRateLimit) ...[ _IssueBadge( icon: Icons.speed, @@ -592,7 +576,6 @@ class _LogSummaryCard extends StatelessWidget { const SizedBox(height: 8), ], - // Network errors if (analysis.hasNetworkError && !analysis.hasISPBlocking) ...[ _IssueBadge( icon: Icons.wifi_off, @@ -604,7 +587,6 @@ class _LogSummaryCard extends StatelessWidget { const SizedBox(height: 8), ], - // Track not found if (analysis.hasNotFound) ...[ _IssueBadge( icon: Icons.search_off, @@ -615,7 +597,6 @@ class _LogSummaryCard extends StatelessWidget { ), ], - // Error count const SizedBox(height: 12), Text( 'Total errors: ${analysis.errorCount}', @@ -655,7 +636,6 @@ class _LogSummaryCard extends StatelessWidget { combined.contains('connection refused')) { hasISPBlocking = true; - // Try to extract domain final domainMatch = RegExp(r'domain:\s*([^\s,]+)', caseSensitive: false).firstMatch(combined); if (domainMatch != null) { blockedDomains.add(domainMatch.group(1)!); @@ -669,7 +649,6 @@ class _LogSummaryCard extends StatelessWidget { hasRateLimit = true; } - // Check for network errors if (combined.contains('connection') || combined.contains('timeout') || combined.contains('network') || @@ -677,7 +656,6 @@ class _LogSummaryCard extends StatelessWidget { hasNetworkError = true; } - // Check for not found if (combined.contains('not found') || combined.contains('no results') || combined.contains('could not find')) { diff --git a/lib/screens/settings/metadata_provider_priority_page.dart b/lib/screens/settings/metadata_provider_priority_page.dart index 24b97f8a..62e7c3a9 100644 --- a/lib/screens/settings/metadata_provider_priority_page.dart +++ b/lib/screens/settings/metadata_provider_priority_page.dart @@ -24,16 +24,13 @@ class _MetadataProviderPriorityPageState extends ConsumerState !allProviders.contains(p)); } else { _providers = allProviders; @@ -57,7 +54,6 @@ class _MetadataProviderPriorityPageState extends ConsumerState { final extState = ref.read(extensionProvider); final allProviders = ref.read(extensionProvider.notifier).getAllDownloadProviders(); - // Use saved priority if available, otherwise use default order if (extState.providerPriority.isNotEmpty) { - // Start with saved priority _providers = List.from(extState.providerPriority); - // Add any new providers not in saved priority for (final provider in allProviders) { if (!_providers.contains(provider)) { _providers.add(provider); } } - // Remove providers that no longer exist _providers.removeWhere((p) => !allProviders.contains(p)); } else { _providers = allProviders; @@ -58,7 +54,6 @@ class _ProviderPriorityPageState extends ConsumerState { child: Scaffold( body: CustomScrollView( slivers: [ - // App Bar SliverAppBar( expandedHeight: 120 + topPadding, collapsedHeight: kToolbarHeight, @@ -110,7 +105,6 @@ class _ProviderPriorityPageState extends ConsumerState { ), ), - // Description SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.all(16), @@ -123,7 +117,6 @@ class _ProviderPriorityPageState extends ConsumerState { ), ), - // Provider list SliverPadding( padding: const EdgeInsets.symmetric(horizontal: 16), sliver: SliverReorderableList( @@ -151,7 +144,6 @@ class _ProviderPriorityPageState extends ConsumerState { ), ), - // Info section SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.all(16), @@ -246,7 +238,6 @@ class _ProviderItem extends StatelessWidget { ) : colorScheme.surfaceContainerHigh; - // Get provider info final info = _getProviderInfo(provider); return Padding( @@ -260,7 +251,6 @@ class _ProviderItem extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( children: [ - // Priority number Container( width: 28, height: 28, @@ -283,7 +273,6 @@ class _ProviderItem extends StatelessWidget { ), ), const SizedBox(width: 16), - // Provider icon Icon( info.icon, color: info.isBuiltIn @@ -291,7 +280,6 @@ class _ProviderItem extends StatelessWidget { : colorScheme.secondary, ), const SizedBox(width: 12), - // Provider name Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -311,7 +299,6 @@ class _ProviderItem extends StatelessWidget { ], ), ), - // Drag handle Icon( Icons.drag_handle, color: colorScheme.onSurfaceVariant, @@ -345,7 +332,6 @@ class _ProviderItem extends StatelessWidget { isBuiltIn: true, ); default: - // Extension provider return _ProviderInfo( name: provider, icon: Icons.extension, diff --git a/lib/screens/settings/settings_tab.dart b/lib/screens/settings/settings_tab.dart index b6421a8d..c9d1899a 100644 --- a/lib/screens/settings/settings_tab.dart +++ b/lib/screens/settings/settings_tab.dart @@ -20,7 +20,6 @@ class SettingsTab extends ConsumerWidget { return CustomScrollView( slivers: [ - // Collapsing App Bar SliverAppBar( expandedHeight: 120 + topPadding, collapsedHeight: kToolbarHeight, @@ -54,7 +53,6 @@ class SettingsTab extends ConsumerWidget { ), ), - // First group: Appearance & Download SliverToBoxAdapter( child: Builder( builder: (context) { @@ -94,7 +92,6 @@ class SettingsTab extends ConsumerWidget { ), ), - // Second group: Logs & About SliverToBoxAdapter( child: Builder( builder: (context) { @@ -120,7 +117,6 @@ class SettingsTab extends ConsumerWidget { ), ), - // Fill remaining space const SliverFillRemaining(hasScrollBody: false, child: SizedBox()), ], ); diff --git a/lib/screens/setup_screen.dart b/lib/screens/setup_screen.dart index 14dbb2b2..c95e9f59 100644 --- a/lib/screens/setup_screen.dart +++ b/lib/screens/setup_screen.dart @@ -25,13 +25,11 @@ class _SetupScreenState extends ConsumerState { bool _isLoading = false; int _androidSdkVersion = 0; - // Spotify API credentials final _clientIdController = TextEditingController(); final _clientSecretController = TextEditingController(); bool _useSpotifyApi = false; bool _showClientSecret = false; - // Total steps: Storage -> Notification (Android 13+) -> Folder -> Spotify API int get _totalSteps => _androidSdkVersion >= 33 ? 4 : 3; @override @@ -66,17 +64,14 @@ class _SetupScreenState extends ConsumerState { }); } } else if (Platform.isAndroid) { - // Check storage permission bool storageGranted = false; if (_androidSdkVersion >= 33) { - // Android 13+: Need BOTH MANAGE_EXTERNAL_STORAGE AND READ_MEDIA_AUDIO final manageStatus = await Permission.manageExternalStorage.status; final audioStatus = await Permission.audio.status; debugPrint('[Permission] Android 13+ check: MANAGE_EXTERNAL_STORAGE=$manageStatus, READ_MEDIA_AUDIO=$audioStatus'); storageGranted = manageStatus.isGranted && audioStatus.isGranted; } else if (_androidSdkVersion >= 30) { - // Android 11-12: Need MANAGE_EXTERNAL_STORAGE only final manageStatus = await Permission.manageExternalStorage.status; debugPrint('[Permission] Android 11-12 check: MANAGE_EXTERNAL_STORAGE=$manageStatus'); storageGranted = manageStatus.isGranted; @@ -89,7 +84,6 @@ class _SetupScreenState extends ConsumerState { debugPrint('[Permission] Final storageGranted=$storageGranted'); - // Check notification permission (Android 13+) PermissionStatus notificationStatus = PermissionStatus.granted; if (_androidSdkVersion >= 33) { notificationStatus = await Permission.notification.status; @@ -115,9 +109,6 @@ class _SetupScreenState extends ConsumerState { bool allGranted = false; if (_androidSdkVersion >= 33) { - // Android 13+: Need BOTH MANAGE_EXTERNAL_STORAGE AND READ_MEDIA_AUDIO - - // First check/request MANAGE_EXTERNAL_STORAGE var manageStatus = await Permission.manageExternalStorage.status; if (!manageStatus.isGranted) { if (mounted) { @@ -144,14 +135,12 @@ class _SetupScreenState extends ConsumerState { if (shouldOpen == true) { await Permission.manageExternalStorage.request(); - // Re-check after returning from settings await Future.delayed(const Duration(milliseconds: 500)); manageStatus = await Permission.manageExternalStorage.status; } } } - // Then request READ_MEDIA_AUDIO (this shows a dialog) var audioStatus = await Permission.audio.status; if (!audioStatus.isGranted && manageStatus.isGranted) { audioStatus = await Permission.audio.request(); @@ -160,7 +149,6 @@ class _SetupScreenState extends ConsumerState { allGranted = manageStatus.isGranted && audioStatus.isGranted; } else if (_androidSdkVersion >= 30) { - // Android 11-12: Need MANAGE_EXTERNAL_STORAGE only var manageStatus = await Permission.manageExternalStorage.status; if (!manageStatus.isGranted) { if (mounted) { @@ -187,7 +175,6 @@ class _SetupScreenState extends ConsumerState { if (shouldOpen == true) { await Permission.manageExternalStorage.request(); - // Re-check after returning from settings await Future.delayed(const Duration(milliseconds: 500)); manageStatus = await Permission.manageExternalStorage.status; } @@ -239,7 +226,6 @@ class _SetupScreenState extends ConsumerState { _showPermissionDeniedDialog('Notification'); } } else { - // Notification permission not needed for older Android setState(() => _notificationPermissionGranted = true); } } catch (e) { @@ -286,7 +272,6 @@ class _SetupScreenState extends ConsumerState { // iOS: Show options dialog await _showIOSDirectoryOptions(); } else { - // Android: Use file picker String? selectedDirectory = await FilePicker.platform.getDirectoryPath( dialogTitle: context.l10n.setupSelectDownloadFolder, ); @@ -359,7 +344,6 @@ class _SetupScreenState extends ConsumerState { subtitle: Text(context.l10n.setupChooseFromFilesSubtitle), onTap: () async { Navigator.pop(ctx); - // Note: iOS requires folder to have at least one file to be selectable final result = await FilePicker.platform.getDirectoryPath(); if (result != null) { setState(() => _selectedDirectory = result); @@ -444,10 +428,8 @@ class _SetupScreenState extends ConsumerState { _clientIdController.text.trim(), _clientSecretController.text.trim(), ); - // Set search source to Spotify when credentials are provided ref.read(settingsProvider.notifier).setMetadataSource('spotify'); } else { - // Use Deezer as default search source (free, no credentials required) ref.read(settingsProvider.notifier).setMetadataSource('deezer'); } @@ -482,7 +464,6 @@ class _SetupScreenState extends ConsumerState { child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - // Top section - Logo/Title Column( children: [ const SizedBox(height: 24), @@ -501,7 +482,6 @@ class _SetupScreenState extends ConsumerState { ], ), - // Middle section - Steps and Content Column( children: [ const SizedBox(height: 24), @@ -511,7 +491,6 @@ class _SetupScreenState extends ConsumerState { ], ), - // Bottom section - Navigation Buttons Column( children: [ const SizedBox(height: 24), @@ -637,7 +616,6 @@ class _SetupScreenState extends ConsumerState { mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ - // Icon with container background (M3 style) Container( width: 80, height: 80, @@ -691,7 +669,6 @@ class _SetupScreenState extends ConsumerState { mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ - // Icon with container background (M3 style) Container( width: 80, height: 80, @@ -754,7 +731,6 @@ class _SetupScreenState extends ConsumerState { mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ - // Icon with container background (M3 style) Container( width: 80, height: 80, @@ -829,7 +805,6 @@ class _SetupScreenState extends ConsumerState { mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ - // Icon with container background (M3 style) Container( width: 80, height: 80, @@ -860,7 +835,6 @@ class _SetupScreenState extends ConsumerState { ), const SizedBox(height: 24), - // Toggle card (M3 style) Card( elevation: 0, color: colorScheme.surfaceContainerHigh, @@ -891,7 +865,6 @@ class _SetupScreenState extends ConsumerState { ), ), - // Credentials form (animated) AnimatedSize( duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, @@ -906,7 +879,6 @@ class _SetupScreenState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Client ID Text(context.l10n.credentialsClientId, style: Theme.of(context).textTheme.labelMedium?.copyWith(color: colorScheme.onSurfaceVariant)), const SizedBox(height: 8), TextField( @@ -925,7 +897,6 @@ class _SetupScreenState extends ConsumerState { ), const SizedBox(height: 16), - // Client Secret Text(context.l10n.credentialsClientSecret, style: Theme.of(context).textTheme.labelMedium?.copyWith(color: colorScheme.onSurfaceVariant)), const SizedBox(height: 8), TextField( @@ -983,14 +954,12 @@ class _SetupScreenState extends ConsumerState { final isLastStep = _currentStep == _totalSteps - 1; final canProceed = _isStepCompleted(_currentStep); - // For Spotify step, check if credentials are valid when enabled final isSpotifyStepValid = !_useSpotifyApi || (_clientIdController.text.trim().isNotEmpty && _clientSecretController.text.trim().isNotEmpty); return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - // Back button if (_currentStep > 0) TextButton.icon( onPressed: () => setState(() => _currentStep--), @@ -1003,7 +972,6 @@ class _SetupScreenState extends ConsumerState { else const SizedBox(width: 100), - // Next/Finish button if (!isLastStep) FilledButton( onPressed: canProceed ? () => setState(() => _currentStep++) : null, diff --git a/lib/screens/store/extension_details_screen.dart b/lib/screens/store/extension_details_screen.dart index 0131e222..86610974 100644 --- a/lib/screens/store/extension_details_screen.dart +++ b/lib/screens/store/extension_details_screen.dart @@ -20,11 +20,8 @@ class _ExtensionDetailsScreenState @override Widget build(BuildContext context) { - // Watch store provider to get latest state of this extension (e.g. if updated/installed) final storeState = ref.watch(storeProvider); - // Find our extension in the store state to get the latest status - // If not found in current store state (rare), fallback to widget.extension final liveExtension = storeState.extensions .where((e) => e.id == widget.extension.id) @@ -188,7 +185,6 @@ class _ExtensionDetailsScreenState const SizedBox(height: 16), - // Badges row Wrap( spacing: 8, runSpacing: 8, @@ -215,7 +211,6 @@ class _ExtensionDetailsScreenState const SizedBox(height: 24), - // Action Buttons if (isDownloading) Center( child: CircularProgressIndicator( @@ -410,7 +405,6 @@ class _ExtensionDetailsScreenState StoreExtension ext, ColorScheme colorScheme, ) { - // Determine capabilities based on category final isMetadataProvider = ext.category == 'metadata' || ext.category == 'integration'; final isDownloadProvider = ext.category == 'download'; final isLyricsProvider = ext.category == 'lyrics'; diff --git a/lib/screens/store_tab.dart b/lib/screens/store_tab.dart index eab625cb..d603d9f0 100644 --- a/lib/screens/store_tab.dart +++ b/lib/screens/store_tab.dart @@ -29,7 +29,6 @@ class _StoreTabState extends ConsumerState { final cacheDir = await getApplicationCacheDirectory(); - // Check if widget is still mounted after async operation if (!mounted) return; await ref.read(storeProvider.notifier).initialize(cacheDir.path); @@ -53,7 +52,6 @@ class _StoreTabState extends ConsumerState { ref.read(storeProvider.notifier).refresh(forceRefresh: true), child: CustomScrollView( slivers: [ - // App Bar - consistent with other tabs SliverAppBar( expandedHeight: 120 + topPadding, collapsedHeight: kToolbarHeight, @@ -87,7 +85,6 @@ class _StoreTabState extends ConsumerState { ), ), - // Search Bar SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), @@ -131,7 +128,6 @@ class _StoreTabState extends ConsumerState { ), ), - // Category Chips SliverToBoxAdapter( child: SingleChildScrollView( scrollDirection: Axis.horizontal, @@ -203,7 +199,6 @@ class _StoreTabState extends ConsumerState { ), ), - // Content if (state.isLoading && state.extensions.isEmpty) const SliverFillRemaining( child: Center(child: CircularProgressIndicator()), @@ -215,7 +210,6 @@ class _StoreTabState extends ConsumerState { else if (state.filteredExtensions.isEmpty) SliverFillRemaining(child: _buildEmptyState(state, colorScheme)) else ...[ - // Extensions count SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), @@ -228,7 +222,6 @@ class _StoreTabState extends ConsumerState { ), ), - // Extensions list in grouped card (like queue_tab) SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), @@ -252,7 +245,6 @@ class _StoreTabState extends ConsumerState { ), ), - // Bottom padding const SliverToBoxAdapter(child: SizedBox(height: 16)), ], ], @@ -457,7 +449,6 @@ class _ExtensionItem extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( children: [ - // Extension icon - custom or category-based Container( width: 44, height: 44, @@ -507,7 +498,6 @@ class _ExtensionItem extends StatelessWidget { ), ), const SizedBox(width: 16), - // Extension info Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -518,10 +508,9 @@ class _ExtensionItem extends StatelessWidget { child: Text( extension.displayName, style: Theme.of(context).textTheme.bodyLarge - ?.copyWith(fontWeight: FontWeight.w500), + ?.copyWith(fontWeight: FontWeight.w500 ), ), ), - // Version badge Container( padding: const EdgeInsets.symmetric( horizontal: 6, @@ -548,7 +537,6 @@ class _ExtensionItem extends StatelessWidget { color: colorScheme.onSurfaceVariant, ), ), - // Warning badge for incompatible extensions if (extension.requiresNewerApp) ...[ const SizedBox(height: 4), Container( @@ -587,7 +575,6 @@ class _ExtensionItem extends StatelessWidget { ), ), const SizedBox(width: 12), - // Action button if (isDownloading) const SizedBox( width: 24, diff --git a/lib/screens/track_metadata_screen.dart b/lib/screens/track_metadata_screen.dart index 365ed248..8f47e4bc 100644 --- a/lib/screens/track_metadata_screen.dart +++ b/lib/screens/track_metadata_screen.dart @@ -44,7 +44,6 @@ class _TrackMetadataScreenState extends ConsumerState { } Future _checkFile() async { - // Strip EXISTS: prefix from legacy history items var filePath = widget.item.filePath; if (filePath.startsWith('EXISTS:')) { filePath = filePath.substring(7); @@ -66,14 +65,12 @@ class _TrackMetadataScreenState extends ConsumerState { _fileSize = size; }); - // Auto-load lyrics if file exists (embedded lyrics are instant) if (exists) { _fetchLyrics(); } } } - // Use data directly from history item (cached from download) DownloadHistoryItem get item => widget.item; String get trackName => item.trackName; String get artistName => item.artistName; @@ -84,7 +81,6 @@ class _TrackMetadataScreenState extends ConsumerState { String? get releaseDate => item.releaseDate; String? get isrc => item.isrc; - // Clean filePath - strip EXISTS: prefix from legacy history items String get cleanFilePath { final path = item.filePath; return path.startsWith('EXISTS:') ? path.substring(7) : path; @@ -99,7 +95,6 @@ class _TrackMetadataScreenState extends ConsumerState { return Scaffold( body: CustomScrollView( slivers: [ - // App Bar with cover art background SliverAppBar( expandedHeight: 280, pinned: true, @@ -138,34 +133,28 @@ class _TrackMetadataScreenState extends ConsumerState { ], ), - // Content SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Track info card _buildTrackInfoCard(context, colorScheme, _fileExists), const SizedBox(height: 16), - // Metadata card _buildMetadataCard(context, colorScheme, _fileSize), const SizedBox(height: 16), - // File info card _buildFileInfoCard(context, colorScheme, _fileExists, _fileSize), const SizedBox(height: 16), - // Lyrics card _buildLyricsCard(context, colorScheme), const SizedBox(height: 24), - // Action buttons _buildActionButtons(context, ref, colorScheme, _fileExists), const SizedBox(height: 32), @@ -182,7 +171,6 @@ class _TrackMetadataScreenState extends ConsumerState { return Stack( fit: StackFit.expand, children: [ - // Blurred background if (item.coverUrl != null) CachedNetworkImage( imageUrl: item.coverUrl!, @@ -191,7 +179,6 @@ class _TrackMetadataScreenState extends ConsumerState { colorBlendMode: BlendMode.darken, ), - // Gradient overlay Container( decoration: BoxDecoration( gradient: LinearGradient( @@ -207,7 +194,6 @@ class _TrackMetadataScreenState extends ConsumerState { ), ), - // Cover art centered Center( child: Padding( padding: const EdgeInsets.only(top: 60), @@ -268,7 +254,6 @@ class _TrackMetadataScreenState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Track name (from file metadata) Text( trackName, style: Theme.of(context).textTheme.headlineSmall?.copyWith( @@ -278,7 +263,6 @@ class _TrackMetadataScreenState extends ConsumerState { ), const SizedBox(height: 4), - // Artist name (from file metadata) Text( artistName, style: Theme.of(context).textTheme.titleMedium?.copyWith( @@ -287,7 +271,6 @@ class _TrackMetadataScreenState extends ConsumerState { ), const SizedBox(height: 8), - // Album name (from file metadata) Row( children: [ Icon( @@ -372,10 +355,8 @@ class _TrackMetadataScreenState extends ConsumerState { ), const SizedBox(height: 16), - // Metadata grid _buildMetadataGrid(context, colorScheme), - // Streaming service link button if (item.spotifyId != null && item.spotifyId!.isNotEmpty) ...[ const SizedBox(height: 8), Builder( @@ -416,28 +397,24 @@ class _TrackMetadataScreenState extends ConsumerState { : Uri.parse('spotify:track:$rawId'); try { - // Try to open in App first using URI scheme final launched = await launchUrl( appUri, mode: LaunchMode.externalApplication, ); if (!launched) { - // Fallback to web URL which will redirect to app if installed await launchUrl( Uri.parse(webUrl), mode: LaunchMode.externalApplication, ); } } catch (e) { - // If URI scheme fails, try web URL try { await launchUrl( Uri.parse(webUrl), mode: LaunchMode.externalApplication, ); } catch (_) { - // Last resort: copy to clipboard if (context.mounted) { _copyToClipboard(context, webUrl); ScaffoldMessenger.of(context).showSnackBar( @@ -449,7 +426,6 @@ class _TrackMetadataScreenState extends ConsumerState { } Widget _buildMetadataGrid(BuildContext context, ColorScheme colorScheme) { - // Build audio quality string from file metadata String? audioQualityStr; if (bitDepth != null && sampleRate != null) { final sampleRateKHz = (sampleRate! / 1000).toStringAsFixed(1); @@ -568,7 +544,6 @@ class _TrackMetadataScreenState extends ConsumerState { ), const SizedBox(height: 16), - // Format chip Wrap( spacing: 8, runSpacing: 8, @@ -651,7 +626,6 @@ class _TrackMetadataScreenState extends ConsumerState { const SizedBox(height: 16), - // File path InkWell( onTap: () => _copyToClipboard(context, cleanFilePath), borderRadius: BorderRadius.circular(12), @@ -811,7 +785,6 @@ class _TrackMetadataScreenState extends ConsumerState { _lyricsLoading = false; }); } else { - // Clean up LRC timestamps for display final cleanLyrics = _cleanLrcForDisplay(result); setState(() { _lyrics = cleanLyrics; @@ -851,7 +824,6 @@ class _TrackMetadataScreenState extends ConsumerState { Widget _buildActionButtons(BuildContext context, WidgetRef ref, ColorScheme colorScheme, bool fileExists) { return Row( children: [ - // Play button Expanded( flex: 2, child: FilledButton.icon( @@ -868,7 +840,6 @@ class _TrackMetadataScreenState extends ConsumerState { ), const SizedBox(width: 12), - // Delete button Expanded( child: OutlinedButton.icon( onPressed: () => _confirmDelete(context, ref, colorScheme), @@ -951,7 +922,6 @@ class _TrackMetadataScreenState extends ConsumerState { ), TextButton( onPressed: () async { - // Delete the file first try { final file = File(cleanFilePath); if (await file.exists()) { @@ -961,7 +931,6 @@ class _TrackMetadataScreenState extends ConsumerState { debugPrint('Failed to delete file: $e'); } - // Remove from history ref.read(downloadHistoryProvider.notifier).removeFromHistory(item.id); if (context.mounted) { diff --git a/lib/services/apk_downloader.dart b/lib/services/apk_downloader.dart index f17f7970..74bd7773 100644 --- a/lib/services/apk_downloader.dart +++ b/lib/services/apk_downloader.dart @@ -14,7 +14,6 @@ class ApkDownloader { required String version, ProgressCallback? onProgress, }) async { - // Validate URL for security final uri = Uri.tryParse(url); if (uri == null || uri.scheme != 'https') { _log.e('Refusing to download from invalid or non-HTTPS URL'); @@ -35,7 +34,6 @@ class ApkDownloader { final contentLength = response.contentLength ?? 0; - // Get download directory final dir = await getExternalStorageDirectory(); if (dir == null) { _log.e('Could not get storage directory'); @@ -45,7 +43,6 @@ class ApkDownloader { final filePath = '${dir.path}/SpotiFLAC-$version.apk'; final file = File(filePath); - // Delete if exists if (await file.exists()) { await file.delete(); } diff --git a/lib/services/csv_import_service.dart b/lib/services/csv_import_service.dart index e06eb9ce..0a0f8aa7 100644 --- a/lib/services/csv_import_service.dart +++ b/lib/services/csv_import_service.dart @@ -23,7 +23,6 @@ class CsvImportService { final content = await file.readAsString(); final tracks = _parseCsv(content); - // Enrich tracks with metadata from Deezer (cover URL, duration, etc.) if (tracks.isNotEmpty) { return await _enrichTracksMetadata(tracks, onProgress: onProgress); } @@ -48,7 +47,6 @@ class CsvImportService { final track = tracks[i]; onProgress?.call(i + 1, tracks.length); - // Only enrich if missing cover/duration if (track.coverUrl == null || track.duration == 0) { Map? trackData; @@ -62,7 +60,6 @@ class CsvImportService { } } - // Fallback to text search if ISRC failed or not available if (trackData == null) { try { final query = '${track.artistName} ${track.name}'; @@ -71,13 +68,11 @@ class CsvImportService { if (searchResult.containsKey('tracks')) { final tracksList = searchResult['tracks'] as List?; if (tracksList != null && tracksList.isNotEmpty) { - // Find best match by comparing names for (final result in tracksList) { final resultMap = result as Map; final resultName = (resultMap['name'] as String?)?.toLowerCase() ?? ''; final trackNameLower = track.name.toLowerCase(); - // Check if track name matches (contains or equals) if (resultName.contains(trackNameLower) || trackNameLower.contains(resultName)) { trackData = resultMap; _log.d('Text search match for ${track.name}: $resultName'); @@ -85,7 +80,6 @@ class CsvImportService { } } - // If no exact match, use first result if (trackData == null && tracksList.isNotEmpty) { trackData = tracksList.first as Map; _log.d('Using first search result for ${track.name}'); @@ -97,7 +91,6 @@ class CsvImportService { } } - // Apply enriched data if found if (trackData != null) { final coverUrl = trackData['images'] as String?; final durationMs = trackData['duration_ms'] as int? ?? 0; @@ -127,7 +120,6 @@ class CsvImportService { } } - // Keep original track if enrichment failed or not needed enrichedTracks.add(track); } @@ -137,10 +129,9 @@ class CsvImportService { static List _parseCsv(String content) { final List tracks = []; - final lines = content.split(RegExp(r'\r\n|\r|\n')); // Handle various newline formats + final lines = content.split(RegExp(r'\r\n|\r|\n')); if (lines.isEmpty) return tracks; - // Detect headers line (assume first non-empty line) int startIdx = 0; while (startIdx < lines.length && lines[startIdx].trim().isEmpty) { startIdx++; @@ -150,7 +141,6 @@ class CsvImportService { final headers = _parseLine(lines[startIdx]); final colMap = {}; for (int i = 0; i < headers.length; i++) { - // Normalize header: lowercase, trim, remove quotes String h = _cleanValue(headers[i]).toLowerCase(); colMap[h] = i; } @@ -164,7 +154,6 @@ class CsvImportService { final values = _parseLine(line); - // Helper to get value securely String? getVal(List keys) { return _getValue(values, colMap, keys); } @@ -180,7 +169,6 @@ class CsvImportService { spotifyId = spotifyId.replaceAll('spotify:track:', ''); } - // Basic validation: Need at least name and artist, OR a spotify ID if ((trackName != null && trackName.isNotEmpty && artistName != null) || (spotifyId != null && spotifyId.isNotEmpty)) { tracks.add(Track( id: spotifyId ?? 'csv_${DateTime.now().millisecondsSinceEpoch}_$i', @@ -215,7 +203,6 @@ class CsvImportService { if (val.startsWith('"') && val.endsWith('"') && val.length >= 2) { val = val.substring(1, val.length - 1); } - // Handle double quotes escape in CSV ("" -> ") val = val.replaceAll('""', '"'); return val; } diff --git a/lib/services/ffmpeg_service.dart b/lib/services/ffmpeg_service.dart index f7878bec..c0cbbf35 100644 --- a/lib/services/ffmpeg_service.dart +++ b/lib/services/ffmpeg_service.dart @@ -31,14 +31,12 @@ class FFmpegService { static Future convertM4aToFlac(String inputPath) async { final outputPath = inputPath.replaceAll('.m4a', '.flac'); - // FFmpeg command to remux M4A to FLAC final command = '-i "$inputPath" -c:a flac -compression_level 8 "$outputPath" -y'; final result = await _execute(command); if (result.success) { - // Delete original M4A file try { await File(inputPath).delete(); } catch (_) {} @@ -88,18 +86,15 @@ class FFmpegService { inputPath.split(Platform.pathSeparator).last.replaceAll('.flac', ''); final outputDir = '$dir${Platform.pathSeparator}M4A'; - // Create output directory await Directory(outputDir).create(recursive: true); final outputPath = '$outputDir${Platform.pathSeparator}$baseName.m4a'; String command; if (codec == 'alac') { - // ALAC - lossless command = '-i "$inputPath" -codec:a alac -map 0:a -map_metadata 0 "$outputPath" -y'; } else { - // AAC - lossy command = '-i "$inputPath" -codec:a aac -b:a $bitrate -map 0:a -map_metadata 0 "$outputPath" -y'; } @@ -141,25 +136,19 @@ class FFmpegService { String? coverPath, Map? metadata, }) async { - // Android Scoped Storage: Cannot write directly to Music folder with FFmpeg - // Use app-internal cache directory for temp output final tempDir = await getTemporaryDirectory(); final uniqueId = DateTime.now().millisecondsSinceEpoch; final tempOutput = '${tempDir.path}/temp_embed_$uniqueId.flac'; - // Construct command final StringBuffer cmdBuffer = StringBuffer(); cmdBuffer.write('-i "$flacPath" '); - // Add cover input if available if (coverPath != null) { cmdBuffer.write('-i "$coverPath" '); } - // Map audio stream cmdBuffer.write('-map 0:a '); - // Map cover stream if available if (coverPath != null) { cmdBuffer.write('-map 1:0 '); cmdBuffer.write('-c:v copy '); @@ -168,13 +157,10 @@ class FFmpegService { cmdBuffer.write('-metadata:s:v comment="Cover (front)" '); } - // Copy audio codec (don't re-encode) cmdBuffer.write('-c:a copy '); - // Add text metadata if (metadata != null) { metadata.forEach((key, value) { - // Sanitize value: escape double quotes final sanitizedValue = value.replaceAll('"', '\\"'); cmdBuffer.write('-metadata $key="$sanitizedValue" '); }); @@ -215,7 +201,6 @@ class FFmpegService { } } - // Clean up temp file if exists try { final tempFile = File(tempOutput); if (await tempFile.exists()) { diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index 0463ba4f..b2cab24f 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -32,7 +32,6 @@ class NotificationService { await _notifications.initialize(initSettings); - // Create notification channel for Android if (Platform.isAndroid) { await _notifications .resolvePlatformSpecificImplementation() @@ -227,7 +226,6 @@ class NotificationService { await _notifications.cancel(downloadProgressId); } - // Update APK download notifications Future showUpdateDownloadProgress({ required String version, required int received, diff --git a/lib/services/platform_bridge.dart b/lib/services/platform_bridge.dart index 31de5a07..2c7bc6cb 100644 --- a/lib/services/platform_bridge.dart +++ b/lib/services/platform_bridge.dart @@ -770,7 +770,6 @@ class PlatformBridge { if (result == null || result == '') return null; return jsonDecode(result as String) as Map; } catch (e) { - // No extension found or error handling URL return null; } } diff --git a/lib/services/share_intent_service.dart b/lib/services/share_intent_service.dart index cf45c893..6b900bc9 100644 --- a/lib/services/share_intent_service.dart +++ b/lib/services/share_intent_service.dart @@ -30,13 +30,11 @@ class ShareIntentService { if (_initialized) return; _initialized = true; - // Listen to media sharing coming from outside the app while the app is in memory _mediaSubscription = ReceiveSharingIntent.instance.getMediaStream().listen( _handleSharedMedia, onError: (err) => _log.e('Error: $err'), ); - // Get the media sharing coming from outside the app while the app is closed final initialMedia = await ReceiveSharingIntent.instance.getInitialMedia(); if (initialMedia.isNotEmpty) { _handleSharedMedia(initialMedia, isInitial: true); @@ -47,14 +45,12 @@ class ShareIntentService { void _handleSharedMedia(List files, {bool isInitial = false}) { for (final file in files) { - // Check the path - for text shares, the path contains the shared text final textToCheck = file.path; final url = _extractSpotifyUrl(textToCheck); if (url != null) { _log.i('Received Spotify URL: $url (initial: $isInitial)'); if (isInitial) { - // Store for later - listener might not be ready yet _pendingUrl = url; } _sharedUrlController.add(url); @@ -71,18 +67,15 @@ class ShareIntentService { String? _extractSpotifyUrl(String text) { if (text.isEmpty) return null; - // Check for spotify: URI format final uriMatch = RegExp(r'spotify:(track|album|playlist|artist):[a-zA-Z0-9]+').firstMatch(text); if (uriMatch != null) { return uriMatch.group(0); } - // Check for open.spotify.com URL final urlMatch = RegExp( r'https?://open\.spotify\.com/(track|album|playlist|artist)/[a-zA-Z0-9]+(\?[^\s]*)?', ).firstMatch(text); if (urlMatch != null) { - // Return URL without query params for cleaner handling final fullUrl = urlMatch.group(0)!; final queryIndex = fullUrl.indexOf('?'); return queryIndex > 0 ? fullUrl.substring(0, queryIndex) : fullUrl; diff --git a/lib/services/update_checker.dart b/lib/services/update_checker.dart index 4673e0a8..1f791496 100644 --- a/lib/services/update_checker.dart +++ b/lib/services/update_checker.dart @@ -65,7 +65,6 @@ class UpdateChecker { Map? releaseData; if (channel == 'preview') { - // For preview channel, get all releases and find the latest (including prereleases) final response = await http.get( Uri.parse('$_allReleasesApiUrl?per_page=10'), headers: {'Accept': 'application/vnd.github.v3+json'}, @@ -82,10 +81,8 @@ class UpdateChecker { return null; } - // First release is the latest (including prereleases) releaseData = releases.first as Map; } else { - // For stable channel, use /latest endpoint (excludes prereleases) final response = await http.get( Uri.parse(_latestApiUrl), headers: {'Accept': 'application/vnd.github.v3+json'}, @@ -124,7 +121,6 @@ class UpdateChecker { final name = (asset['name'] as String? ?? '').toLowerCase(); if (name.endsWith('.apk')) { final downloadUrl = asset['browser_download_url'] as String?; - // Only accept HTTPS URLs for security final uri = downloadUrl != null ? Uri.tryParse(downloadUrl) : null; if (uri == null || uri.scheme != 'https') { _log.w('Skipping non-HTTPS APK URL: $downloadUrl'); diff --git a/lib/widgets/collapsing_header.dart b/lib/widgets/collapsing_header.dart index 175fe51d..44681c96 100644 --- a/lib/widgets/collapsing_header.dart +++ b/lib/widgets/collapsing_header.dart @@ -64,7 +64,6 @@ class CollapsingHeader extends StatelessWidget { ), ), - // Info card if provided if (infoCard != null) SliverToBoxAdapter( child: Padding( @@ -73,7 +72,6 @@ class CollapsingHeader extends StatelessWidget { ), ), - // Content slivers ...slivers, ], ); diff --git a/lib/widgets/download_service_picker.dart b/lib/widgets/download_service_picker.dart index 78e373e9..c2748dbf 100644 --- a/lib/widgets/download_service_picker.dart +++ b/lib/widgets/download_service_picker.dart @@ -105,20 +105,17 @@ class _DownloadServicePickerState extends ConsumerState { /// Get quality options for the selected service List _getQualityOptions() { - // Check if it's a built-in service final builtIn = _builtInServices.where((s) => s.id == _selectedService).firstOrNull; if (builtIn != null) { return builtIn.qualityOptions; } - // Check if it's an extension final extensionState = ref.read(extensionProvider); final ext = extensionState.extensions.where((e) => e.id == _selectedService).firstOrNull; if (ext != null && ext.qualityOptions.isNotEmpty) { return ext.qualityOptions; } - // Default quality options if extension doesn't specify any return const [ QualityOption(id: 'DEFAULT', label: 'Default Quality', description: 'Best available'), ]; @@ -129,7 +126,6 @@ class _DownloadServicePickerState extends ConsumerState { final colorScheme = Theme.of(context).colorScheme; final extensionState = ref.watch(extensionProvider); - // Get enabled download provider extensions final downloadExtensions = extensionState.extensions .where((ext) => ext.enabled && ext.hasDownloadProvider) .toList(); @@ -142,7 +138,6 @@ class _DownloadServicePickerState extends ConsumerState { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Track info header (if provided) if (widget.trackName != null) ...[ _TrackInfoHeader( trackName: widget.trackName!, @@ -164,7 +159,6 @@ class _DownloadServicePickerState extends ConsumerState { ), ], - // Service selector section Padding( padding: const EdgeInsets.fromLTRB(24, 16, 24, 8), child: Text( @@ -173,21 +167,18 @@ class _DownloadServicePickerState extends ConsumerState { ), ), - // Built-in services Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Wrap( spacing: 8, runSpacing: 8, children: [ - // Built-in services for (final service in _builtInServices) _ServiceChip( label: service.label, isSelected: _selectedService == service.id, onTap: () => setState(() => _selectedService = service.id), ), - // Extension services for (final ext in downloadExtensions) _ServiceChip( label: ext.displayName, @@ -199,7 +190,6 @@ class _DownloadServicePickerState extends ConsumerState { ), ), - // Quality selector section Padding( padding: const EdgeInsets.fromLTRB(24, 16, 24, 8), child: Text( @@ -208,7 +198,6 @@ class _DownloadServicePickerState extends ConsumerState { ), ), - // Disclaimer for built-in services if (_builtInServices.any((s) => s.id == _selectedService)) Padding( padding: const EdgeInsets.fromLTRB(24, 0, 24, 12), @@ -221,7 +210,6 @@ class _DownloadServicePickerState extends ConsumerState { ), ), - // Quality options for (final quality in qualityOptions) _QualityOption( title: quality.label, diff --git a/lib/widgets/update_dialog.dart b/lib/widgets/update_dialog.dart index f34b1eb1..63be0b0c 100644 --- a/lib/widgets/update_dialog.dart +++ b/lib/widgets/update_dialog.dart @@ -30,7 +30,6 @@ class _UpdateDialogState extends State { Future _downloadAndInstall() async { final apkUrl = widget.updateInfo.apkDownloadUrl; - // If no direct APK URL, open release page if (apkUrl == null) { final uri = Uri.parse(widget.updateInfo.downloadUrl); if (await canLaunchUrl(uri)) { @@ -60,7 +59,6 @@ class _UpdateDialogState extends State { _statusText = '$receivedMB / $totalMB MB'; }); } - // Update notification notificationService.showUpdateDownloadProgress( version: widget.updateInfo.version, received: received, @@ -70,7 +68,6 @@ class _UpdateDialogState extends State { ); if (filePath != null) { - // Cancel progress notification first await notificationService.cancelUpdateNotification(); await notificationService.showUpdateDownloadComplete( @@ -81,10 +78,8 @@ class _UpdateDialogState extends State { Navigator.pop(context); } - // Open APK for installation await ApkDownloader.installApk(filePath); } else { - // Cancel progress notification first await notificationService.cancelUpdateNotification(); await notificationService.showUpdateDownloadFailed(); @@ -116,7 +111,6 @@ class _UpdateDialogState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Header with icon Row( children: [ Container( @@ -142,7 +136,6 @@ class _UpdateDialogState extends State { ), const SizedBox(height: 20), - // Version badge Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( @@ -165,7 +158,6 @@ class _UpdateDialogState extends State { ), const SizedBox(height: 20), - // Download progress (when downloading) if (_isDownloading) ...[ Container( padding: const EdgeInsets.all(16), @@ -209,7 +201,6 @@ class _UpdateDialogState extends State { ), ), ] else ...[ - // Changelog section Text(context.l10n.updateWhatsNew, style: Theme.of(context).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.bold)), const SizedBox(height: 8), Container( @@ -231,7 +222,6 @@ class _UpdateDialogState extends State { ], const SizedBox(height: 24), - // Action buttons if (_isDownloading) SizedBox( width: double.infinity, @@ -303,19 +293,16 @@ class _UpdateDialogState extends State { String _formatChangelog(String changelog) { var content = changelog; - // Find content after "What's New" header final whatsNewMatch = RegExp(r"###?\s*What'?s\s*New\s*\n", caseSensitive: false).firstMatch(content); if (whatsNewMatch != null) { content = content.substring(whatsNewMatch.end); } - // Cut off at "Downloads" section or horizontal rule final cutoffMatch = RegExp(r'\n---|\n###?\s*Downloads', caseSensitive: false).firstMatch(content); if (cutoffMatch != null) { content = content.substring(0, cutoffMatch.start); } - // Process line by line for better formatting final lines = content.split('\n'); final formattedLines = []; @@ -323,7 +310,6 @@ class _UpdateDialogState extends State { line = line.trim(); if (line.isEmpty) continue; - // Check if it's a section header final sectionMatch = RegExp(r'^#{1,3}\s*(.+)$').firstMatch(line); if (sectionMatch != null) { final section = sectionMatch.group(1)?.trim(); @@ -334,7 +320,6 @@ class _UpdateDialogState extends State { continue; } - // Check if it's a list item final listMatch = RegExp(r'^[-*]\s+(.+)$').firstMatch(line); if (listMatch != null) { var itemText = listMatch.group(1) ?? ''; @@ -344,7 +329,6 @@ class _UpdateDialogState extends State { continue; } - // Check if it's a sub-item final subListMatch = RegExp(r'^\s+[-*]\s+(.+)$').firstMatch(line); if (subListMatch != null) { var itemText = subListMatch.group(1) ?? ''; @@ -401,7 +385,6 @@ class _VersionChip extends StatelessWidget { } } -/// Show update dialog Future showUpdateDialog( BuildContext context, { required UpdateInfo updateInfo,