mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-07-18 10:07:29 +02:00
chore: cleanup unused code and dead imports
This commit is contained in:
@@ -125,7 +125,7 @@ class DownloadHistoryItem {
|
||||
|
||||
class DownloadHistoryState {
|
||||
final List<DownloadHistoryItem> items;
|
||||
final Set<String> _downloadedSpotifyIds; // Cache for O(1) lookup
|
||||
final Set<String> _downloadedSpotifyIds;
|
||||
|
||||
DownloadHistoryState({this.items = const []})
|
||||
: _downloadedSpotifyIds = items
|
||||
@@ -133,7 +133,6 @@ class DownloadHistoryState {
|
||||
.map((item) => item.spotifyId!)
|
||||
.toSet();
|
||||
|
||||
/// Check if a track has been downloaded (by Spotify ID)
|
||||
bool isDownloaded(String spotifyId) =>
|
||||
_downloadedSpotifyIds.contains(spotifyId);
|
||||
|
||||
@@ -188,7 +187,6 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Deduplicate history items by spotifyId, deezerId, or ISRC
|
||||
/// Keeps the most recent entry (first occurrence since list is sorted by date desc)
|
||||
List<DownloadHistoryItem> _deduplicateHistory(List<DownloadHistoryItem> items) {
|
||||
final seen = <String, int>{}; // key -> index of first occurrence
|
||||
@@ -234,7 +232,6 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Force reload from storage (useful after app restart)
|
||||
Future<void> reloadFromStorage() async {
|
||||
await _loadFromStorage();
|
||||
}
|
||||
@@ -285,7 +282,6 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
|
||||
_saveToStorage();
|
||||
}
|
||||
|
||||
/// Remove item from history by Spotify ID
|
||||
void removeBySpotifyId(String spotifyId) {
|
||||
state = state.copyWith(
|
||||
items: state.items.where((item) => item.spotifyId != spotifyId).toList(),
|
||||
@@ -294,7 +290,6 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
|
||||
_historyLog.d('Removed item with spotifyId: $spotifyId');
|
||||
}
|
||||
|
||||
/// Get history item by Spotify ID
|
||||
DownloadHistoryItem? getBySpotifyId(String spotifyId) {
|
||||
return state.items.where((item) => item.spotifyId == spotifyId).firstOrNull;
|
||||
}
|
||||
@@ -314,12 +309,12 @@ class DownloadQueueState {
|
||||
final List<DownloadItem> items;
|
||||
final DownloadItem? currentDownload;
|
||||
final bool isProcessing;
|
||||
final bool isPaused; // NEW: pause state
|
||||
final bool isPaused;
|
||||
final String outputDir;
|
||||
final String filenameFormat;
|
||||
final String audioQuality; // LOSSLESS, HI_RES, HI_RES_LOSSLESS
|
||||
final String audioQuality;
|
||||
final bool autoFallback;
|
||||
final int concurrentDownloads; // 1 = sequential, max 3
|
||||
final int concurrentDownloads;
|
||||
|
||||
const DownloadQueueState({
|
||||
this.items = const [],
|
||||
@@ -386,14 +381,13 @@ class _ProgressUpdate {
|
||||
|
||||
class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
Timer? _progressTimer;
|
||||
int _downloadCount = 0; // Counter for connection cleanup
|
||||
static const _cleanupInterval = 50; // Cleanup every 50 downloads
|
||||
static const _queueStorageKey =
|
||||
'download_queue'; // Storage key for queue persistence
|
||||
int _downloadCount = 0;
|
||||
static const _cleanupInterval = 50;
|
||||
static const _queueStorageKey = 'download_queue';
|
||||
final NotificationService _notificationService = NotificationService();
|
||||
int _totalQueuedAtStart = 0; // Track total items when queue started
|
||||
int _completedInSession = 0; // Track completed downloads in current session
|
||||
int _failedInSession = 0; // Track failed downloads in current session
|
||||
int _totalQueuedAtStart = 0;
|
||||
int _completedInSession = 0;
|
||||
int _failedInSession = 0;
|
||||
bool _isLoaded = false;
|
||||
final Set<String> _ensuredDirs = {};
|
||||
|
||||
@@ -411,7 +405,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
return const DownloadQueueState();
|
||||
}
|
||||
|
||||
/// Load persisted queue from storage (for app restart recovery)
|
||||
Future<void> _loadQueueFromStorage() async {
|
||||
if (_isLoaded) return;
|
||||
_isLoaded = true;
|
||||
@@ -453,7 +446,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Save current queue to storage (only pending items)
|
||||
Future<void> _saveQueueToStorage() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
@@ -479,7 +471,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Start multi-progress polling for all downloads (sequential and parallel)
|
||||
void _startMultiProgressPolling() {
|
||||
_progressTimer?.cancel();
|
||||
_progressTimer = Timer.periodic(const Duration(milliseconds: 500), (
|
||||
@@ -607,7 +598,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
trackName: finalizingTrackName,
|
||||
artistName: finalizingArtistName ?? '',
|
||||
);
|
||||
return; // Don't show download progress notification
|
||||
return;
|
||||
}
|
||||
|
||||
if (items.isNotEmpty) {
|
||||
@@ -651,14 +642,11 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
progress: notifProgress,
|
||||
total: notifTotal > 0 ? notifTotal : 1,
|
||||
queueCount: state.queuedCount,
|
||||
).catchError((_) {}); // Ignore errors
|
||||
).catchError((_) {});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Silently ignore polling errors to avoid spamming logs
|
||||
// Polling is not critical and will retry on next interval
|
||||
}
|
||||
} catch (_) {}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -725,7 +713,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
state = state.copyWith(outputDir: dir);
|
||||
}
|
||||
|
||||
/// Build output directory based on folder organization setting and separateSingles
|
||||
Future<String> _buildOutputDir(Track track, String folderOrganization, {bool separateSingles = false, String albumFolderStructure = 'artist_album'}) async {
|
||||
String baseDir = state.outputDir;
|
||||
final albumArtist = _normalizeOptionalString(track.albumArtist) ?? track.artistName;
|
||||
@@ -794,7 +781,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
return baseDir;
|
||||
}
|
||||
|
||||
/// Sanitize folder names (remove invalid characters)
|
||||
String _sanitizeFolderName(String name) {
|
||||
return name
|
||||
.replaceAll(RegExp(r'[<>:"/\\|?*]'), '_')
|
||||
@@ -866,7 +852,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
}).toList();
|
||||
|
||||
state = state.copyWith(items: [...state.items, ...newItems]);
|
||||
_saveQueueToStorage(); // Persist queue
|
||||
_saveQueueToStorage();
|
||||
|
||||
if (!state.isProcessing) {
|
||||
Future.microtask(() => _processQueue());
|
||||
@@ -951,15 +937,14 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
.toList();
|
||||
|
||||
state = state.copyWith(items: items);
|
||||
_saveQueueToStorage(); // Persist queue
|
||||
_saveQueueToStorage();
|
||||
}
|
||||
|
||||
void clearAll() {
|
||||
state = state.copyWith(items: [], isPaused: false);
|
||||
_saveQueueToStorage(); // Clear persisted queue
|
||||
_saveQueueToStorage();
|
||||
}
|
||||
|
||||
/// Pause the download queue
|
||||
void pauseQueue() {
|
||||
if (state.isProcessing && !state.isPaused) {
|
||||
state = state.copyWith(isPaused: true);
|
||||
@@ -968,7 +953,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Resume the download queue
|
||||
void resumeQueue() {
|
||||
if (state.isPaused) {
|
||||
state = state.copyWith(isPaused: false);
|
||||
@@ -979,7 +963,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle pause/resume
|
||||
void togglePause() {
|
||||
if (state.isPaused) {
|
||||
resumeQueue();
|
||||
@@ -988,7 +971,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Retry a failed or skipped download
|
||||
void retryItem(String id) {
|
||||
final item = state.items.where((i) => i.id == id).firstOrNull;
|
||||
if (item == null) {
|
||||
@@ -1025,14 +1007,12 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a specific item from queue
|
||||
void removeItem(String id) {
|
||||
final items = state.items.where((item) => item.id != id).toList();
|
||||
state = state.copyWith(items: items);
|
||||
_saveQueueToStorage(); // Persist queue
|
||||
_saveQueueToStorage();
|
||||
}
|
||||
|
||||
/// Run post-processing hooks on a downloaded file
|
||||
Future<void> _runPostProcessingHooks(String filePath, Track track) async {
|
||||
try {
|
||||
final settings = ref.read(settingsProvider);
|
||||
@@ -1079,7 +1059,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Upgrade Spotify cover URL to max quality (~2000x2000)
|
||||
/// Same logic as Go backend cover.go
|
||||
String _upgradeToMaxQualityCover(String coverUrl) {
|
||||
const spotifySize300 = 'ab67616d00001e02'; // 300x300 (small)
|
||||
@@ -1098,7 +1077,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Embed metadata and cover to a FLAC file after M4A conversion
|
||||
Future<void> _embedMetadataAndCover(
|
||||
String flacPath,
|
||||
Track track, {
|
||||
@@ -1155,12 +1133,12 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
|
||||
if (track.trackNumber != null) {
|
||||
metadata['TRACKNUMBER'] = track.trackNumber.toString();
|
||||
metadata['TRACK'] = track.trackNumber.toString(); // Compatibility
|
||||
metadata['TRACK'] = track.trackNumber.toString();
|
||||
}
|
||||
|
||||
if (track.discNumber != null) {
|
||||
metadata['DISCNUMBER'] = track.discNumber.toString();
|
||||
metadata['DISC'] = track.discNumber.toString(); // Compatibility
|
||||
metadata['DISC'] = track.discNumber.toString();
|
||||
}
|
||||
|
||||
if (track.releaseDate != null) {
|
||||
@@ -1172,7 +1150,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
metadata['ISRC'] = track.isrc!;
|
||||
}
|
||||
|
||||
// Extended metadata from enrichment (genre, label, copyright)
|
||||
if (genre != null && genre.isNotEmpty) {
|
||||
metadata['GENRE'] = genre;
|
||||
_log.d('Adding GENRE: $genre');
|
||||
@@ -1189,20 +1166,19 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
_log.d('Metadata map content: $metadata');
|
||||
|
||||
try {
|
||||
// Convert duration from seconds to milliseconds for better lyrics matching
|
||||
final durationMs = track.duration * 1000;
|
||||
|
||||
final lrcContent = await PlatformBridge.getLyricsLRC(
|
||||
track.id, // spotifyID
|
||||
track.id,
|
||||
track.name,
|
||||
track.artistName,
|
||||
filePath: '', // No local file path yet (processed in memory)
|
||||
filePath: '',
|
||||
durationMs: durationMs,
|
||||
);
|
||||
|
||||
if (lrcContent.isNotEmpty) {
|
||||
metadata['LYRICS'] = lrcContent;
|
||||
metadata['UNSYNCEDLYRICS'] = lrcContent; // Fallback for some players
|
||||
metadata['UNSYNCEDLYRICS'] = lrcContent;
|
||||
_log.d('Lyrics fetched for embedding (${lrcContent.length} chars)');
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -1240,7 +1216,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Embed metadata, lyrics, and cover to a MP3 file
|
||||
Future<void> _embedMetadataToMp3(String mp3Path, Track track) async {
|
||||
final settings = ref.read(settingsProvider);
|
||||
|
||||
@@ -1310,7 +1285,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
|
||||
_log.d('MP3 Metadata map content: $metadata');
|
||||
|
||||
// Fetch lyrics if embedLyrics is enabled
|
||||
if (settings.embedLyrics) {
|
||||
try {
|
||||
final durationMs = track.duration * 1000;
|
||||
@@ -1365,7 +1339,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
}
|
||||
|
||||
Future<void> _processQueue() async {
|
||||
if (state.isProcessing) return; // Prevent multiple concurrent processing
|
||||
if (state.isProcessing) return;
|
||||
|
||||
state = state.copyWith(isProcessing: true);
|
||||
_log.i('Starting queue processing...');
|
||||
@@ -1462,7 +1436,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sequential download processing (uses multi-progress system with single item)
|
||||
Future<void> _processQueueSequential() async {
|
||||
_startMultiProgressPolling();
|
||||
|
||||
@@ -1508,10 +1481,10 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
_stopProgressPolling();
|
||||
}
|
||||
|
||||
/// Parallel download processing with worker pool
|
||||
Future<void> _processQueueParallel() async {
|
||||
final maxConcurrent = state.concurrentDownloads;
|
||||
final activeDownloads = <String, Future<void>>{}; // Map item ID to future
|
||||
final activeDownloads = <String, Future<void>>{};
|
||||
|
||||
|
||||
_startMultiProgressPolling();
|
||||
|
||||
@@ -1565,7 +1538,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
_stopProgressPolling();
|
||||
}
|
||||
|
||||
/// Download a single item (used by both sequential and parallel processing)
|
||||
Future<void> _downloadSingleItem(DownloadItem item) async {
|
||||
_log.d('Processing: ${item.track.name} by ${item.track.artistName}');
|
||||
_log.d('Cover URL: ${item.track.coverUrl}');
|
||||
@@ -1628,7 +1600,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
trackToDownload.albumName,
|
||||
albumArtist: data['album_artist'] as String?,
|
||||
coverUrl: data['images'] as String?,
|
||||
// duration_ms from Go is in milliseconds, Track.duration is in seconds
|
||||
duration:
|
||||
((data['duration_ms'] as int?) ??
|
||||
(trackToDownload.duration * 1000)) ~/
|
||||
@@ -1675,7 +1646,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
String? genre;
|
||||
String? label;
|
||||
|
||||
// Try to get Deezer track ID from various sources
|
||||
String? deezerTrackId = trackToDownload.deezerId;
|
||||
if (deezerTrackId == null && trackToDownload.id.startsWith('deezer:')) {
|
||||
deezerTrackId = trackToDownload.id.split(':')[1];
|
||||
@@ -1696,7 +1666,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
}
|
||||
} catch (e) {
|
||||
_log.w('Failed to fetch extended metadata from Deezer: $e');
|
||||
// Continue without extended metadata
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1728,7 +1697,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
releaseDate: trackToDownload.releaseDate,
|
||||
itemId: item.id,
|
||||
durationMs: trackToDownload.duration,
|
||||
source: trackToDownload.source, // Pass extension ID that provided this track
|
||||
source: trackToDownload.source,
|
||||
genre: genre,
|
||||
label: label,
|
||||
lyricsMode: settings.lyricsMode,
|
||||
@@ -1754,9 +1723,8 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
discNumber: trackToDownload.discNumber ?? 1,
|
||||
releaseDate: trackToDownload.releaseDate,
|
||||
preferredService: item.service,
|
||||
itemId: item.id, // Pass item ID for progress tracking
|
||||
durationMs:
|
||||
trackToDownload.duration, // Duration in ms for verification
|
||||
itemId: item.id,
|
||||
durationMs: trackToDownload.duration,
|
||||
genre: genre,
|
||||
label: label,
|
||||
lyricsMode: settings.lyricsMode,
|
||||
@@ -1809,8 +1777,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
if (result['success'] == true) {
|
||||
var filePath = result['file_path'] as String?;
|
||||
|
||||
// Track if this was an existing file (not a new download)
|
||||
// This is important to prevent converting existing FLAC files to MP3
|
||||
final wasExisting = filePath != null && filePath.startsWith('EXISTS:');
|
||||
if (wasExisting) {
|
||||
filePath = filePath.substring(7); // Remove "EXISTS:" prefix
|
||||
@@ -1912,7 +1878,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
);
|
||||
}
|
||||
|
||||
// Get extended metadata from backend response
|
||||
final backendGenre = result['genre'] as String?;
|
||||
final backendLabel = result['label'] as String?;
|
||||
final backendCopyright = result['copyright'] as String?;
|
||||
@@ -1962,15 +1927,9 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert FLAC to MP3 if MP3 quality was selected
|
||||
// IMPORTANT: Only convert NEW downloads, never convert existing files
|
||||
// to prevent overwriting the user's existing FLAC files
|
||||
if (quality == 'MP3' && filePath != null && filePath.endsWith('.flac')) {
|
||||
if (wasExisting) {
|
||||
// User wanted MP3 but an existing FLAC file was found
|
||||
// Do NOT convert it - that would delete their existing FLAC
|
||||
_log.i('MP3 requested but existing FLAC found - skipping conversion to preserve original file');
|
||||
// Keep the existing FLAC file as-is
|
||||
} else {
|
||||
_log.i('MP3 quality selected, converting FLAC to MP3...');
|
||||
updateItemStatus(
|
||||
@@ -1991,7 +1950,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
actualQuality = 'MP3 320kbps';
|
||||
_log.i('Successfully converted to MP3: $mp3Path');
|
||||
|
||||
// Embed metadata, lyrics, and cover to the MP3 file
|
||||
_log.i('Embedding metadata to MP3...');
|
||||
updateItemStatus(
|
||||
item.id,
|
||||
@@ -2050,7 +2008,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
? normalizedAlbumArtist
|
||||
: null;
|
||||
|
||||
// For MP3 files, don't save FLAC bitDepth/sampleRate - they're not applicable
|
||||
final isMp3 = filePath.endsWith('.mp3');
|
||||
final historyBitDepth = isMp3 ? null : backendBitDepth;
|
||||
final historySampleRate = isMp3 ? null : backendSampleRate;
|
||||
|
||||
@@ -5,7 +5,6 @@ import 'package:spotiflac_android/providers/settings_provider.dart';
|
||||
|
||||
final _log = AppLogger('ExtensionProvider');
|
||||
|
||||
/// Represents an installed extension
|
||||
class Extension {
|
||||
final String id;
|
||||
final String name;
|
||||
@@ -14,19 +13,19 @@ class Extension {
|
||||
final String author;
|
||||
final String description;
|
||||
final bool enabled;
|
||||
final String status; // 'loaded', 'error', 'disabled'
|
||||
final String status;
|
||||
final String? errorMessage;
|
||||
final String? iconPath; // Path to extension icon
|
||||
final String? iconPath;
|
||||
final List<String> permissions;
|
||||
final List<ExtensionSetting> settings;
|
||||
final List<QualityOption> qualityOptions; // Custom quality options for download providers
|
||||
final List<QualityOption> qualityOptions;
|
||||
final bool hasMetadataProvider;
|
||||
final bool hasDownloadProvider;
|
||||
final bool skipMetadataEnrichment; // If true, use metadata from extension instead of enriching
|
||||
final SearchBehavior? searchBehavior; // Custom search behavior
|
||||
final URLHandler? urlHandler; // Custom URL handling
|
||||
final TrackMatching? trackMatching; // Custom track matching
|
||||
final PostProcessing? postProcessing; // Post-processing hooks
|
||||
final SearchBehavior? searchBehavior;
|
||||
final URLHandler? urlHandler;
|
||||
final TrackMatching? trackMatching;
|
||||
final PostProcessing? postProcessing;
|
||||
|
||||
const Extension({
|
||||
required this.id,
|
||||
@@ -140,7 +139,6 @@ class Extension {
|
||||
bool get hasPostProcessing => postProcessing?.enabled ?? false;
|
||||
}
|
||||
|
||||
/// Custom search behavior configuration
|
||||
class SearchBehavior {
|
||||
final bool enabled;
|
||||
final String? placeholder;
|
||||
@@ -172,8 +170,6 @@ class SearchBehavior {
|
||||
);
|
||||
}
|
||||
|
||||
/// Get thumbnail size based on configuration
|
||||
/// Returns (width, height) tuple
|
||||
(double, double) getThumbnailSize({double defaultSize = 56}) {
|
||||
if (thumbnailWidth != null && thumbnailHeight != null) {
|
||||
return (thumbnailWidth!.toDouble(), thumbnailHeight!.toDouble());
|
||||
@@ -191,11 +187,10 @@ class SearchBehavior {
|
||||
}
|
||||
}
|
||||
|
||||
/// Custom track matching configuration
|
||||
class TrackMatching {
|
||||
final bool customMatching;
|
||||
final String? strategy; // "isrc", "name", "duration", "custom"
|
||||
final int durationTolerance; // in seconds
|
||||
final String? strategy;
|
||||
final int durationTolerance;
|
||||
|
||||
const TrackMatching({
|
||||
required this.customMatching,
|
||||
@@ -212,7 +207,6 @@ class TrackMatching {
|
||||
}
|
||||
}
|
||||
|
||||
/// Post-processing configuration
|
||||
class PostProcessing {
|
||||
final bool enabled;
|
||||
final List<PostProcessingHook> hooks;
|
||||
@@ -262,7 +256,6 @@ class URLHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/// A post-processing hook
|
||||
class PostProcessingHook {
|
||||
final String id;
|
||||
final String name;
|
||||
@@ -289,12 +282,11 @@ class PostProcessingHook {
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a quality option for download providers
|
||||
class QualityOption {
|
||||
final String id;
|
||||
final String label;
|
||||
final String? description;
|
||||
final List<QualitySpecificSetting> settings; // Quality-specific settings
|
||||
final List<QualitySpecificSetting> settings;
|
||||
|
||||
const QualityOption({
|
||||
required this.id,
|
||||
@@ -315,14 +307,13 @@ class QualityOption {
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a setting that's specific to a quality option
|
||||
class QualitySpecificSetting {
|
||||
final String key;
|
||||
final String label;
|
||||
final String type; // 'string', 'number', 'boolean', 'select'
|
||||
final String type;
|
||||
final dynamic defaultValue;
|
||||
final String? description;
|
||||
final List<String>? options; // For select type
|
||||
final List<String>? options;
|
||||
final bool required;
|
||||
final bool secret;
|
||||
|
||||
@@ -351,16 +342,15 @@ class QualitySpecificSetting {
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a setting field for an extension
|
||||
class ExtensionSetting {
|
||||
final String key;
|
||||
final String label;
|
||||
final String type; // 'string', 'number', 'boolean', 'select', 'button'
|
||||
final String type;
|
||||
final dynamic defaultValue;
|
||||
final String? description;
|
||||
final List<String>? options; // For select type
|
||||
final List<String>? options;
|
||||
final bool required;
|
||||
final String? action; // For button type: JS function name to call
|
||||
final String? action;
|
||||
|
||||
const ExtensionSetting({
|
||||
required this.key,
|
||||
@@ -387,7 +377,6 @@ class ExtensionSetting {
|
||||
}
|
||||
}
|
||||
|
||||
/// State for extension management
|
||||
class ExtensionState {
|
||||
final List<Extension> extensions;
|
||||
final List<String> providerPriority;
|
||||
@@ -425,7 +414,6 @@ class ExtensionState {
|
||||
}
|
||||
|
||||
|
||||
/// Provider for managing extensions
|
||||
class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
@override
|
||||
ExtensionState build() {
|
||||
@@ -451,7 +439,6 @@ class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Load all extensions from directory
|
||||
Future<void> loadExtensions(String dirPath) async {
|
||||
state = state.copyWith(isLoading: true, error: null);
|
||||
|
||||
@@ -486,12 +473,10 @@ class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear any error state
|
||||
void clearError() {
|
||||
state = state.copyWith(error: null);
|
||||
}
|
||||
|
||||
/// Install extension from file (auto-upgrades if already installed with newer version)
|
||||
Future<bool> installExtension(String filePath) async {
|
||||
state = state.copyWith(isLoading: true, error: null);
|
||||
|
||||
@@ -508,8 +493,6 @@ class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a package file is an upgrade for an existing extension
|
||||
/// Returns: {extension_id, current_version, new_version, can_upgrade, is_installed}
|
||||
Future<Map<String, dynamic>> checkExtensionUpgrade(String filePath) async {
|
||||
try {
|
||||
return await PlatformBridge.checkExtensionUpgrade(filePath);
|
||||
@@ -519,7 +502,6 @@ class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Upgrade an existing extension from a new package file
|
||||
Future<bool> upgradeExtension(String filePath) async {
|
||||
state = state.copyWith(isLoading: true, error: null);
|
||||
|
||||
@@ -553,7 +535,6 @@ class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Enable or disable an extension
|
||||
Future<void> setExtensionEnabled(String extensionId, bool enabled) async {
|
||||
try {
|
||||
await PlatformBridge.setExtensionEnabled(extensionId, enabled);
|
||||
@@ -600,7 +581,6 @@ class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Update settings for an extension
|
||||
Future<void> setExtensionSettings(String extensionId, Map<String, dynamic> settings) async {
|
||||
try {
|
||||
await PlatformBridge.setExtensionSettings(extensionId, settings);
|
||||
@@ -621,7 +601,6 @@ class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Set provider priority order
|
||||
Future<void> setProviderPriority(List<String> priority) async {
|
||||
try {
|
||||
await PlatformBridge.setProviderPriority(priority);
|
||||
@@ -643,7 +622,6 @@ class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Set metadata provider priority order
|
||||
Future<void> setMetadataProviderPriority(List<String> priority) async {
|
||||
try {
|
||||
await PlatformBridge.setMetadataProviderPriority(priority);
|
||||
@@ -665,7 +643,6 @@ class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get extension by ID
|
||||
Extension? getExtension(String extensionId) {
|
||||
try {
|
||||
return state.extensions.firstWhere((ext) => ext.id == extensionId);
|
||||
@@ -679,7 +656,6 @@ class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
return state.extensions.where((ext) => ext.enabled).toList();
|
||||
}
|
||||
|
||||
/// Get all download providers (built-in + extensions)
|
||||
List<String> getAllDownloadProviders() {
|
||||
final providers = ['tidal', 'qobuz', 'amazon'];
|
||||
for (final ext in state.extensions) {
|
||||
@@ -700,7 +676,6 @@ class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
}
|
||||
return providers;
|
||||
}
|
||||
/// Get all extensions that provide custom search
|
||||
List<Extension> get searchProviders {
|
||||
return state.extensions.where((ext) => ext.enabled && ext.hasCustomSearch).toList();
|
||||
}
|
||||
|
||||
@@ -121,7 +121,6 @@ class RecentAccessNotifier extends Notifier<RecentAccessState> {
|
||||
.map((e) => RecentAccessItem.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
} catch (e) {
|
||||
// Ignore parse errors
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,7 +265,6 @@ class RecentAccessNotifier extends Notifier<RecentAccessState> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Provider instance
|
||||
final recentAccessProvider = NotifierProvider<RecentAccessNotifier, RecentAccessState>(
|
||||
RecentAccessNotifier.new,
|
||||
);
|
||||
|
||||
@@ -30,7 +30,6 @@ class SettingsNotifier extends Notifier<AppSettings> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Run one-time migrations for settings
|
||||
Future<void> _runMigrations(SharedPreferences prefs) async {
|
||||
final lastMigration = prefs.getInt(_migrationVersionKey) ?? 0;
|
||||
|
||||
@@ -51,7 +50,6 @@ class SettingsNotifier extends Notifier<AppSettings> {
|
||||
await prefs.setString(_settingsKey, jsonEncode(state.toJson()));
|
||||
}
|
||||
|
||||
/// Apply current Spotify credentials to Go backend
|
||||
Future<void> _applySpotifyCredentials() async {
|
||||
if (state.spotifyClientId.isNotEmpty &&
|
||||
state.spotifyClientSecret.isNotEmpty) {
|
||||
@@ -93,7 +91,6 @@ class SettingsNotifier extends Notifier<AppSettings> {
|
||||
}
|
||||
|
||||
void setLyricsMode(String mode) {
|
||||
// Valid modes: embed, external, both
|
||||
if (mode == 'embed' || mode == 'external' || mode == 'both') {
|
||||
state = state.copyWith(lyricsMode: mode);
|
||||
_saveSettings();
|
||||
|
||||
@@ -52,7 +52,6 @@ class StoreCategory {
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents an extension in the store
|
||||
class StoreExtension {
|
||||
final String id;
|
||||
final String name;
|
||||
@@ -118,7 +117,6 @@ class StoreExtension {
|
||||
}
|
||||
}
|
||||
|
||||
/// State for extension store
|
||||
class StoreState {
|
||||
final List<StoreExtension> extensions;
|
||||
final String? selectedCategory;
|
||||
@@ -200,7 +198,6 @@ class StoreNotifier extends Notifier<StoreState> {
|
||||
return const StoreState();
|
||||
}
|
||||
|
||||
/// Initialize the store
|
||||
Future<void> initialize(String cacheDir) async {
|
||||
if (state.isInitialized) return;
|
||||
|
||||
@@ -234,7 +231,6 @@ class StoreNotifier extends Notifier<StoreState> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Set category filter
|
||||
void setCategory(String? category) {
|
||||
if (category == null) {
|
||||
state = state.copyWith(clearCategory: true);
|
||||
@@ -248,7 +244,6 @@ class StoreNotifier extends Notifier<StoreState> {
|
||||
state = state.copyWith(searchQuery: query);
|
||||
}
|
||||
|
||||
/// Clear search
|
||||
void clearSearch() {
|
||||
state = state.copyWith(searchQuery: '', clearCategory: true);
|
||||
}
|
||||
@@ -279,7 +274,6 @@ class StoreNotifier extends Notifier<StoreState> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Update an installed extension
|
||||
Future<bool> updateExtension(String extensionId, String tempDir) async {
|
||||
state = state.copyWith(isDownloading: true, downloadingId: extensionId, clearError: true);
|
||||
|
||||
@@ -305,7 +299,6 @@ class StoreNotifier extends Notifier<StoreState> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear error
|
||||
void clearError() {
|
||||
state = state.copyWith(clearError: true);
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ class ThemeNotifier extends Notifier<ThemeSettings> {
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('Error loading theme settings: $e');
|
||||
// Keep default state on error
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -89,7 +89,6 @@ class TrackState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents an album in artist discography
|
||||
class ArtistAlbum {
|
||||
final String id;
|
||||
final String name;
|
||||
@@ -112,7 +111,6 @@ class ArtistAlbum {
|
||||
});
|
||||
}
|
||||
|
||||
/// Represents an artist in search results
|
||||
class SearchArtist {
|
||||
final String id;
|
||||
final String name;
|
||||
@@ -130,7 +128,6 @@ class SearchArtist {
|
||||
}
|
||||
|
||||
class TrackNotifier extends Notifier<TrackState> {
|
||||
/// Request ID to track and cancel outdated requests
|
||||
int _currentRequestId = 0;
|
||||
|
||||
@override
|
||||
@@ -213,14 +210,8 @@ class TrackNotifier extends Notifier<TrackState> {
|
||||
Map<String, dynamic> metadata;
|
||||
|
||||
try {
|
||||
// ignore: avoid_print
|
||||
print('[FetchURL] Fetching $type with Deezer fallback enabled...');
|
||||
metadata = await PlatformBridge.getSpotifyMetadataWithFallback(url);
|
||||
// ignore: avoid_print
|
||||
print('[FetchURL] Metadata fetch success');
|
||||
} catch (e) {
|
||||
// ignore: avoid_print
|
||||
print('[FetchURL] Metadata fetch failed: $e');
|
||||
rethrow;
|
||||
}
|
||||
|
||||
@@ -263,7 +254,7 @@ class TrackNotifier extends Notifier<TrackState> {
|
||||
final albumsList = metadata['albums'] as List<dynamic>;
|
||||
final albums = albumsList.map((a) => _parseArtistAlbum(a as Map<String, dynamic>)).toList();
|
||||
state = TrackState(
|
||||
tracks: [], // No tracks for artist view
|
||||
tracks: [],
|
||||
isLoading: false,
|
||||
artistId: artistInfo['id'] as String?,
|
||||
artistName: artistInfo['name'] as String?,
|
||||
@@ -397,7 +388,6 @@ class TrackNotifier extends Notifier<TrackState> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Perform custom search using a specific extension
|
||||
Future<void> customSearch(String extensionId, String query, {Map<String, dynamic>? options}) async {
|
||||
final requestId = ++_currentRequestId;
|
||||
|
||||
@@ -429,7 +419,7 @@ class TrackNotifier extends Notifier<TrackState> {
|
||||
|
||||
state = TrackState(
|
||||
tracks: tracks,
|
||||
searchArtists: [], // Custom search doesn't return artists
|
||||
searchArtists: [],
|
||||
isLoading: false,
|
||||
hasSearchText: state.hasSearchText,
|
||||
searchExtensionId: extensionId, // Store which extension was used
|
||||
@@ -477,8 +467,6 @@ class TrackNotifier extends Notifier<TrackState> {
|
||||
tracks[index] = updatedTrack;
|
||||
state = state.copyWith(tracks: tracks);
|
||||
} catch (e) {
|
||||
// Silently ignore availability check errors
|
||||
// This is a background operation that shouldn't disrupt the user
|
||||
}
|
||||
}
|
||||
|
||||
@@ -494,7 +482,6 @@ class TrackNotifier extends Notifier<TrackState> {
|
||||
state = state.copyWith(hasSearchText: hasText);
|
||||
}
|
||||
|
||||
/// Set recent access mode state
|
||||
void setShowingRecentAccess(bool showing) {
|
||||
state = state.copyWith(isShowingRecentAccess: showing);
|
||||
}
|
||||
@@ -584,8 +571,6 @@ class TrackNotifier extends Notifier<TrackState> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Pre-warm track ID cache for faster downloads
|
||||
/// Runs in background, doesn't block UI
|
||||
void _preWarmCacheForTracks(List<Track> tracks) {
|
||||
final tracksWithIsrc = tracks.where((t) => t.isrc != null && t.isrc!.isNotEmpty).toList();
|
||||
if (tracksWithIsrc.isEmpty) return;
|
||||
|
||||
Reference in New Issue
Block a user