chore: clean up codebase

This commit is contained in:
zarzet
2026-03-26 16:43:56 +07:00
parent bf0f4bdf3e
commit 79a69f8f70
37 changed files with 80 additions and 415 deletions
@@ -874,10 +874,6 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
await _db.upsert(updated.toJson());
}
/// Remove history entries where the file no longer exists on disk.
/// Returns the number of orphaned entries removed.
/// Audio file extensions that the app commonly produces or converts between.
static const _audioExtensions = [
'.flac',
'.m4a',
@@ -888,9 +884,6 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
'.aac',
];
/// When the original file is missing, check whether a sibling with a
/// different audio extension exists (e.g. the user converted .flac → .opus).
/// Returns the path of the first match found, or `null` if none exist.
Future<String?> _findConvertedSibling(String originalPath) async {
final dotIndex = originalPath.lastIndexOf('.');
if (dotIndex < 0) return null;
@@ -2711,7 +2704,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
static final _deezerSizeRegex = RegExp(r'/(\d+)x(\d+)-\d+-\d+-\d+-\d+\.jpg$');
String _upgradeToMaxQualityCover(String coverUrl) {
// Spotify CDN upgrade (hash-based size identifiers)
const spotifySize300 = 'ab67616d00001e02';
const spotifySize640 = 'ab67616d0000b273';
const spotifySizeMax = 'ab67616d000082c1';
@@ -2724,7 +2716,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
result = result.replaceFirst(spotifySize640, spotifySizeMax);
}
// Deezer CDN upgrade (1000x1000 → 1800x1800)
if (result.contains('cdn-images.dzcdn.net')) {
final upgraded = result.replaceFirst(
_deezerSizeRegex,
@@ -3405,7 +3396,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
Future<void> _processQueue() async {
if (state.isProcessing) return;
// Check network connectivity before starting
final settings = ref.read(settingsProvider);
updateSettings(settings);
final isSafMode = _isSafMode(settings);
@@ -3465,7 +3455,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
state = state.copyWith(outputDir: musicDir.path);
ref.read(settingsProvider.notifier).setDownloadDirectory(musicDir.path);
} else if (!isValidIosWritablePath(state.outputDir)) {
// Check for other invalid paths (like container root without Documents/)
_log.w(
'iOS: Invalid output path detected (container root?), falling back to app Documents folder',
);
@@ -3487,7 +3476,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
_log.d('Output directory: ${state.outputDir}');
} else {
_log.d('Output directory: SAF (tree_uri=${settings.downloadTreeUri})');
// Validate SAF permission is still accessible
try {
final testResult = await PlatformBridge.createSafFileFromPath(
treeUri: settings.downloadTreeUri,
@@ -3496,16 +3484,12 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
mimeType: 'application/octet-stream',
srcPath: '',
);
// If we got a result, permission is valid (file creation may fail but that's ok)
// If permission is revoked, this will throw
if (testResult != null) {
// Clean up test file
await PlatformBridge.safDelete(testResult);
}
} catch (e) {
_log.e('SAF permission validation failed: $e');
_log.w('SAF tree URI may be invalid or permission revoked');
// Mark all queued items as failed
for (final item in state.items) {
if (item.status == DownloadStatus.queued) {
updateItemStatus(
@@ -3639,8 +3623,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
}
if (activeDownloads.isNotEmpty) {
// Re-check queue/settings periodically so concurrency changes
// (e.g. 1 -> 3) can take effect before any active item finishes.
await Future.any([
Future.any(activeDownloads.values),
Future.delayed(_queueSchedulingInterval),
@@ -3926,7 +3908,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
if (resolvedIsrc != null && _isValidISRC(resolvedIsrc)) {
_log.d('Resolved ISRC from $provider: $resolvedIsrc');
// Enrich track with provider metadata
final provReleaseDate = normalizeOptionalString(
trackData['release_date'] as String?,
);
@@ -3962,7 +3943,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
source: trackToDownload.source,
);
// Search Deezer by the resolved ISRC
try {
final deezerResult = await PlatformBridge.searchDeezerByISRC(
resolvedIsrc,
@@ -3988,9 +3968,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
}
}
// Fallback: Use SongLink to convert Spotify ID to Deezer ID
// Skip for tidal:/qobuz: IDs they are not Spotify URLs and the
// provider ISRC resolution above already handles them.
if (!selectedExtensionDownloadProvider &&
deezerTrackId == null &&
!shouldSkipExtensionSongLinkPrelookup &&
@@ -4011,7 +3988,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
'track',
spotifyId,
);
// Response is TrackResponse: {"track": {"spotify_id": "deezer:XXXXX", ...}}
final trackData = deezerData['track'];
if (trackData is Map<String, dynamic>) {
final rawId = trackData['spotify_id'] as String?;
@@ -4317,7 +4293,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
finalSafFileName = reportedFileName;
}
// Check if file already existed (detected via ISRC match in Go backend)
final wasExisting = result['already_exists'] == true;
if (wasExisting) {
_log.i('File already exists in library: $filePath');
@@ -4330,7 +4305,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
String actualQuality = quality;
if (actualBitDepth != null && actualBitDepth > 0) {
// Format: "24-bit/96kHz" or "16-bit/44.1kHz"
final sampleRateKHz = actualSampleRate != null && actualSampleRate > 0
? (actualSampleRate / 1000).toStringAsFixed(
actualSampleRate % 1000 == 0 ? 0 : 1,
@@ -4486,7 +4460,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
}
if (isM4aFile || shouldForceTidalSafM4aHandling) {
// At this point filePath is guaranteed non-null by the checks above.
final currentFilePath = filePath;
if (isContentUriPath && effectiveSafMode) {
@@ -4979,9 +4952,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
return;
}
// SAF downloads should end with content URI. If we still have a
// transient FD path, recover URI from SAF metadata to keep history
// dedup/exclusion stable.
if (effectiveSafMode &&
filePath != null &&
filePath.isNotEmpty &&
@@ -5296,8 +5266,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
);
_failedInSession++;
// Immediately cleanup connections after failure to prevent
// poisoned connection pool from affecting subsequent downloads
try {
await PlatformBridge.cleanupConnections();
} catch (e) {
@@ -5350,7 +5318,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
);
_failedInSession++;
// Immediately cleanup connections after exception
try {
await PlatformBridge.cleanupConnections();
} catch (cleanupErr) {
+1 -9
View File
@@ -11,7 +11,7 @@ final _log = AppLogger('ExploreProvider');
class ExploreItem {
final String id;
final String uri;
final String type; // track, album, playlist, artist, station
final String type;
final String name;
final String artists;
final String? description;
@@ -168,7 +168,6 @@ class ExploreNotifier extends Notifier<ExploreState> {
return const ExploreState();
}
/// Restore cached home feed from SharedPreferences immediately on startup
Future<void> _restoreFromCache() async {
try {
final prefs = await SharedPreferences.getInstance();
@@ -199,7 +198,6 @@ class ExploreNotifier extends Notifier<ExploreState> {
}
}
/// Save home feed to SharedPreferences for instant restore on next launch
Future<void> _saveToCache(List<ExploreSection> sections) async {
try {
final prefs = await SharedPreferences.getInstance();
@@ -212,11 +210,9 @@ class ExploreNotifier extends Notifier<ExploreState> {
}
}
/// Fetch home feed from spotify-web extension
Future<void> fetchHomeFeed({bool forceRefresh = false}) async {
_log.i('fetchHomeFeed called, forceRefresh=$forceRefresh');
// If we have cached content and it's fresh enough, skip network fetch
if (!forceRefresh &&
state.hasContent &&
state.lastFetched != null &&
@@ -230,7 +226,6 @@ class ExploreNotifier extends Notifier<ExploreState> {
return;
}
// Only show loading spinner if we have no cached content to display
final showLoading = !state.hasContent;
state = state.copyWith(isLoading: showLoading, error: null);
@@ -247,14 +242,12 @@ class ExploreNotifier extends Notifier<ExploreState> {
if (!extension.enabled || !extension.hasHomeFeed) {
continue;
}
// If user has a preference, use that
if (preferredId != null &&
preferredId.isNotEmpty &&
extension.id == preferredId) {
targetExt = extension;
break;
}
// Otherwise take the first available (fallback to spotify-web if found)
if (targetExt == null || extension.id == 'spotify-web') {
targetExt = extension;
if (preferredId == null && extension.id == 'spotify-web') {
@@ -317,7 +310,6 @@ class ExploreNotifier extends Notifier<ExploreState> {
lastFetched: DateTime.now(),
);
// Save to disk cache for instant restore on next app launch
_saveToCache(sections);
} catch (e, stack) {
_log.e('Error fetching home feed: $e', e, stack);
+7 -13
View File
@@ -32,14 +32,12 @@ class Extension {
final bool hasMetadataProvider;
final bool hasDownloadProvider;
final bool hasLyricsProvider;
final bool
skipMetadataEnrichment; // If true, use metadata from extension instead of enriching
final bool skipMetadataEnrichment;
final SearchBehavior? searchBehavior;
final URLHandler? urlHandler;
final TrackMatching? trackMatching;
final PostProcessing? postProcessing;
final Map<String, dynamic>
capabilities; // Extension capabilities (homeFeed, browseCategories, etc.)
final Map<String, dynamic> capabilities;
const Extension({
required this.id,
@@ -198,12 +196,10 @@ class SearchBehavior {
final String? placeholder;
final bool primary;
final String? icon;
final String?
thumbnailRatio; // "square" (1:1), "wide" (16:9), "portrait" (2:3)
final String? thumbnailRatio;
final int? thumbnailWidth;
final int? thumbnailHeight;
final List<SearchFilter>
filters; // Available search filters (e.g., track, album, artist, playlist)
final List<SearchFilter> filters;
const SearchBehavior({
required this.enabled,
@@ -239,11 +235,11 @@ class SearchBehavior {
}
switch (thumbnailRatio) {
case 'wide': // 16:9 - YouTube style
case 'wide':
return (defaultSize * 16 / 9, defaultSize);
case 'portrait': // 2:3 - Poster style
case 'portrait':
return (defaultSize * 2 / 3, defaultSize);
case 'square': // 1:1 - Album art style
case 'square':
default:
return (defaultSize, defaultSize);
}
@@ -290,7 +286,6 @@ class PostProcessing {
}
}
/// URL handler configuration for custom URL patterns
class URLHandler {
final bool enabled;
final List<String> patterns;
@@ -304,7 +299,6 @@ class URLHandler {
);
}
/// Check if a URL matches any of the patterns
bool matchesURL(String url) {
if (!enabled || patterns.isEmpty) return false;
final lowerUrl = url.toLowerCase();
@@ -666,7 +666,6 @@ class LibraryCollectionsNotifier extends Notifier<LibraryCollectionsState> {
final destPath = p.join(coversDir.path, '$playlistId$ext');
if (playlist.coverImagePath == destPath) return;
// Copy image to persistent location
await File(sourceFilePath).copy(destPath);
final now = DateTime.now();
@@ -686,7 +685,6 @@ class LibraryCollectionsNotifier extends Notifier<LibraryCollectionsState> {
final playlist = state.playlistById(playlistId);
if (playlist == null || playlist.coverImagePath == null) return;
// Delete the file if it exists
final path = playlist.coverImagePath;
if (path != null) {
final file = File(path);
-14
View File
@@ -252,8 +252,6 @@ class LocalLibraryNotifier extends Notifier<LocalLibraryState> {
_startProgressPolling();
// On iOS, start accessing the security-scoped bookmark so the Go backend
// can read files outside the app sandbox.
String? resolvedPath;
bool didStartSecurityAccess = false;
if (Platform.isIOS && iosBookmark != null && iosBookmark.isNotEmpty) {
@@ -275,9 +273,6 @@ class LocalLibraryNotifier extends Notifier<LocalLibraryState> {
try {
final isSaf = effectiveFolderPath.startsWith('content://');
// Get all file paths from download history to exclude them.
// Merge DB + in-memory state to avoid race when a fresh download has not
// been flushed to SQLite yet.
final downloadedPaths = await _historyDb.getAllFilePaths();
final inMemoryHistoryPaths = ref
.read(downloadHistoryProvider)
@@ -298,7 +293,6 @@ class LocalLibraryNotifier extends Notifier<LocalLibraryState> {
);
if (forceFullScan) {
// Full scan path - ignores existing data
final results = isSaf
? await PlatformBridge.scanSafTree(effectiveFolderPath)
: await PlatformBridge.scanLibraryFolder(effectiveFolderPath);
@@ -324,7 +318,6 @@ class LocalLibraryNotifier extends Notifier<LocalLibraryState> {
_log.i('Skipped $skippedDownloads files already in download history');
}
// Full scan should replace library index atomically.
await _db.replaceAll(items.map((e) => e.toJson()).toList());
final persistedItems = [...items]..sort(_compareLibraryItems);
@@ -357,7 +350,6 @@ class LocalLibraryNotifier extends Notifier<LocalLibraryState> {
errorCount: state.scanErrorCount,
);
} else {
// Incremental scan path - only scans new/modified files
final existingFiles = await _db.getFileModTimes();
_log.i(
'Incremental scan: ${existingFiles.length} existing files in database',
@@ -416,7 +408,6 @@ class LocalLibraryNotifier extends Notifier<LocalLibraryState> {
return;
}
// SAF returns 'files' and 'removedUris', non-SAF returns 'scanned' and 'deletedPaths'
final scannedList =
(result['files'] as List<dynamic>?) ??
(result['scanned'] as List<dynamic>?) ??
@@ -437,10 +428,6 @@ class LocalLibraryNotifier extends Notifier<LocalLibraryState> {
'$skippedCount skipped, ${deletedPaths.length} deleted, $totalFiles total',
);
// Build the incremental merge base from SQLite, not the current
// provider state. Startup auto-scan can fire before `state.items` has
// finished loading, which would otherwise drop unchanged rows from the
// in-memory library until a manual full rescan.
final existingJson = await _db.getAll();
final currentByPath = <String, LocalLibraryItem>{
for (final item in existingJson.map(LocalLibraryItem.fromJson))
@@ -461,7 +448,6 @@ class LocalLibraryNotifier extends Notifier<LocalLibraryState> {
);
}
// Upsert new/modified items (excluding downloaded files)
final updatedItems = <LocalLibraryItem>[];
int skippedDownloads = existingDownloadedPaths.length;
if (scannedList.isNotEmpty) {
+2 -16
View File
@@ -5,18 +5,16 @@ import 'package:spotiflac_android/services/app_state_database.dart';
const _maxRecentItems = 20;
/// Types of items that can be accessed
enum RecentAccessType { artist, album, track, playlist }
/// Represents a recently accessed item
class RecentAccessItem {
final String id;
final String name;
final String? subtitle; // Artist name for tracks/albums, null for artists
final String? subtitle;
final String? imageUrl;
final RecentAccessType type;
final DateTime accessedAt;
final String? providerId; // Extension ID or 'deezer' for built-in
final String? providerId;
const RecentAccessItem({
required this.id,
@@ -53,7 +51,6 @@ class RecentAccessItem {
);
}
/// Create a unique key for deduplication
String get uniqueKey => '${type.name}:${providerId ?? 'default'}:$id';
@override
@@ -67,7 +64,6 @@ class RecentAccessItem {
int get hashCode => uniqueKey.hashCode;
}
/// State for recent access history
class RecentAccessState {
final List<RecentAccessItem> items;
final Set<String> hiddenDownloadIds;
@@ -92,7 +88,6 @@ class RecentAccessState {
}
}
/// Provider for managing recent access history
class RecentAccessNotifier extends Notifier<RecentAccessState> {
final AppStateDatabase _appStateDb = AppStateDatabase.instance;
@@ -135,7 +130,6 @@ class RecentAccessNotifier extends Notifier<RecentAccessState> {
}
}
/// Record an access to an artist
void recordArtistAccess({
required String id,
required String name,
@@ -154,7 +148,6 @@ class RecentAccessNotifier extends Notifier<RecentAccessState> {
);
}
/// Record an access to an album
void recordAlbumAccess({
required String id,
required String name,
@@ -175,7 +168,6 @@ class RecentAccessNotifier extends Notifier<RecentAccessState> {
);
}
/// Record an access to a track
void recordTrackAccess({
required String id,
required String name,
@@ -196,7 +188,6 @@ class RecentAccessNotifier extends Notifier<RecentAccessState> {
);
}
/// Record an access to a playlist
void recordPlaylistAccess({
required String id,
required String name,
@@ -242,7 +233,6 @@ class RecentAccessNotifier extends Notifier<RecentAccessState> {
}
}
/// Remove a specific item from history
void removeItem(RecentAccessItem item) {
final updatedItems = state.items
.where((e) => e.uniqueKey != item.uniqueKey)
@@ -251,25 +241,21 @@ class RecentAccessNotifier extends Notifier<RecentAccessState> {
unawaited(_appStateDb.deleteRecentAccessRow(item.uniqueKey));
}
/// Hide a download item from recents (without deleting the actual download)
void hideDownloadFromRecents(String downloadId) {
final updatedHidden = {...state.hiddenDownloadIds, downloadId};
state = state.copyWith(hiddenDownloadIds: updatedHidden);
unawaited(_appStateDb.addHiddenRecentDownloadId(downloadId));
}
/// Check if a download is hidden from recents
bool isDownloadHidden(String downloadId) {
return state.hiddenDownloadIds.contains(downloadId);
}
/// Clear all history
void clearHistory() {
state = state.copyWith(items: []);
unawaited(_appStateDb.clearRecentAccessRows());
}
/// Clear hidden downloads (show all again)
void clearHiddenDownloads() {
state = state.copyWith(hiddenDownloadIds: {});
unawaited(_appStateDb.clearHiddenRecentDownloadIds());
+1 -2
View File
@@ -264,13 +264,12 @@ class StoreNotifier extends Notifier<StoreState> {
// Read back the resolved URL (may differ from input after normalisation).
final resolvedUrl = await PlatformBridge.getStoreRegistryUrl();
// Persist to SharedPreferences
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_registryUrlPrefKey, resolvedUrl);
state = state.copyWith(
registryUrl: resolvedUrl,
extensions: const [], // Clear old extensions
extensions: const [],
);
_log.i('Registry URL set to: $resolvedUrl');
-2
View File
@@ -57,7 +57,6 @@ class ThemeNotifier extends Notifier<ThemeSettings> {
await _saveToStorage();
}
/// Set custom seed color (used when dynamic color is disabled)
Future<void> setSeedColor(Color color) async {
state = state.copyWith(seedColorValue: color.toARGB32());
await _saveToStorage();
@@ -81,4 +80,3 @@ class ThemeNotifier extends Notifier<ThemeSettings> {
);
}
}
+19 -35
View File
@@ -18,21 +18,18 @@ class TrackState {
final String? artistId;
final String? artistName;
final String? coverUrl;
final String? headerImageUrl; // Artist header image for background
final String? headerImageUrl;
final int? monthlyListeners;
final List<ArtistAlbum>? artistAlbums; // For artist page
final List<Track>? artistTopTracks; // Artist's popular tracks
final List<SearchArtist>? searchArtists; // For search results
final List<SearchAlbum>? searchAlbums; // For search results (albums)
final List<SearchPlaylist>? searchPlaylists; // For search results (playlists)
final bool hasSearchText; // For back button handling
final bool isShowingRecentAccess; // For recent access mode
final String?
searchExtensionId; // Extension ID used for current search results
final String?
selectedSearchFilter; // Currently selected search filter (e.g., "track", "album", "artist", "playlist")
final String?
searchSource; // Built-in search provider used for current results (e.g., "deezer", "tidal", "qobuz")
final List<ArtistAlbum>? artistAlbums;
final List<Track>? artistTopTracks;
final List<SearchArtist>? searchArtists;
final List<SearchAlbum>? searchAlbums;
final List<SearchPlaylist>? searchPlaylists;
final bool hasSearchText;
final bool isShowingRecentAccess;
final String? searchExtensionId;
final String? selectedSearchFilter;
final String? searchSource;
const TrackState({
this.tracks = const [],
@@ -127,9 +124,9 @@ class ArtistAlbum {
final String releaseDate;
final int totalTracks;
final String? coverUrl;
final String albumType; // album, single, compilation
final String albumType;
final String artists;
final String? providerId; // Extension ID if from extension
final String? providerId;
const ArtistAlbum({
required this.id,
@@ -204,7 +201,6 @@ class TrackNotifier extends Notifier<TrackState> {
return const TrackState();
}
/// Check if request is still valid (not cancelled by newer request)
bool _isRequestValid(int requestId) => requestId == _currentRequestId;
Future<void> fetchFromUrl(String url, {bool useDeezerFallback = true}) async {
@@ -217,7 +213,6 @@ class TrackNotifier extends Notifier<TrackState> {
if (extensionHandler != null) {
_log.i('Found extension URL handler: $extensionHandler for URL: $url');
// Retry logic for extension URL handlers (up to 3 attempts)
Map<String, dynamic>? result;
for (int attempt = 1; attempt <= 3; attempt++) {
result = await PlatformBridge.handleURLWithExtension(url);
@@ -541,7 +536,6 @@ class TrackNotifier extends Notifier<TrackState> {
return;
}
// If URL doesn't match any known service, it's unrecognized
final isSpotifyUrl =
url.contains('open.spotify.com') ||
url.contains('spotify.link') ||
@@ -643,7 +637,6 @@ class TrackNotifier extends Notifier<TrackState> {
}) async {
final requestId = ++_currentRequestId;
// Preserve selected filter during loading
final currentFilter = filterOverride ?? state.selectedSearchFilter;
state = TrackState(
@@ -662,7 +655,6 @@ class TrackNotifier extends Notifier<TrackState> {
final includeExtensions =
settings.useExtensionProviders && hasActiveMetadataExtensions;
// Determine the effective search provider
final effectiveProvider = builtInSearchProvider ?? 'deezer';
_log.i(
@@ -672,7 +664,6 @@ class TrackNotifier extends Notifier<TrackState> {
Map<String, dynamic> results;
List<Map<String, dynamic>> metadataTrackResults = [];
// Only use metadata providers for Deezer search (default behavior)
if (effectiveProvider == 'deezer') {
try {
_log.d('Calling metadata provider search API...');
@@ -692,7 +683,6 @@ class TrackNotifier extends Notifier<TrackState> {
}
}
// Call the appropriate search API
switch (effectiveProvider) {
case 'tidal':
_log.d('Calling Tidal search API...');
@@ -808,9 +798,8 @@ class TrackNotifier extends Notifier<TrackState> {
isLoading: false,
hasSearchText: state.hasSearchText,
isShowingRecentAccess: state.isShowingRecentAccess,
selectedSearchFilter: currentFilter, // Preserve filter in results
searchSource:
effectiveProvider, // Track which service was used for search
selectedSearchFilter: currentFilter,
searchSource: effectiveProvider,
);
} catch (e, stackTrace) {
if (!_isRequestValid(requestId)) return;
@@ -837,7 +826,7 @@ class TrackNotifier extends Notifier<TrackState> {
hasSearchText: state.hasSearchText,
isShowingRecentAccess: state.isShowingRecentAccess,
selectedSearchFilter:
state.selectedSearchFilter, // Preserve filter during loading
state.selectedSearchFilter,
);
try {
@@ -876,9 +865,8 @@ class TrackNotifier extends Notifier<TrackState> {
isLoading: false,
hasSearchText: state.hasSearchText,
isShowingRecentAccess: state.isShowingRecentAccess,
searchExtensionId: extensionId, // Store which extension was used
selectedSearchFilter:
state.selectedSearchFilter, // Preserve selected filter
searchExtensionId: extensionId,
selectedSearchFilter: state.selectedSearchFilter,
);
} catch (e, stackTrace) {
if (!_isRequestValid(requestId)) return;
@@ -934,7 +922,6 @@ class TrackNotifier extends Notifier<TrackState> {
tracks[index] = updatedTrack;
state = state.copyWith(tracks: tracks);
} catch (_) {
// Silently ignore update failures - track may have been removed
}
}
@@ -942,7 +929,6 @@ class TrackNotifier extends Notifier<TrackState> {
state = const TrackState();
}
/// Set selected search filter for extension search
void setSearchFilter(String? filter) {
if (state.selectedSearchFilter == filter) return;
state = state.copyWith(
@@ -951,7 +937,6 @@ class TrackNotifier extends Notifier<TrackState> {
);
}
/// Set search text state for back button handling
void setSearchText(bool hasText) {
if (state.hasSearchText == hasText) {
return;
@@ -966,7 +951,6 @@ class TrackNotifier extends Notifier<TrackState> {
state = state.copyWith(isShowingRecentAccess: showing);
}
/// Set tracks from a collection (album/playlist) opened from search results
void setTracksFromCollection({
required List<Track> tracks,
String? albumName,
@@ -1127,7 +1111,7 @@ class TrackNotifier extends Notifier<TrackState> {
'isrc': isrc,
'track_name': track.name,
'artist_name': track.artistName,
'spotify_id': track.id, // Include Spotify ID for Amazon lookup
'spotify_id': track.id,
'service': 'tidal',
});
if (cacheRequests.length >= _maxPreWarmTracksPerRequest) {
-1
View File
@@ -1105,7 +1105,6 @@ class _DownloadedAlbumScreenState extends ConsumerState<DownloadedAlbumScreen> {
? 'Opus'
: null;
if (ext == null || ext == targetFormat) continue;
// Skip lossy sources when target is lossless (pointless re-encoding)
final isLosslessTarget = targetFormat == 'ALAC' || targetFormat == 'FLAC';
final isLosslessSource = ext == 'FLAC' || ext == 'M4A';
if (isLosslessTarget && !isLosslessSource) continue;
+1 -8
View File
@@ -1367,7 +1367,6 @@ class _LocalAlbumScreenState extends ConsumerState<LocalAlbumScreen> {
}
}
if (currentFormat == null || currentFormat == targetFormat) continue;
// Skip lossy sources when target is lossless (pointless re-encoding)
final isLosslessTarget = targetFormat == 'ALAC' || targetFormat == 'FLAC';
final isLosslessSource =
currentFormat == 'FLAC' || currentFormat == 'M4A';
@@ -1488,7 +1487,7 @@ class _LocalAlbumScreenState extends ConsumerState<LocalAlbumScreen> {
bitrate: bitrate,
metadata: metadata,
coverPath: coverPath,
deleteOriginal: !isSaf, // Only delete original for regular files
deleteOriginal: !isSaf,
);
if (coverPath != null) {
@@ -1507,15 +1506,9 @@ class _LocalAlbumScreenState extends ConsumerState<LocalAlbumScreen> {
}
if (isSaf) {
// For SAF: derive the parent tree URI and relative dir from the content URI,
// then create new SAF file and delete old one
// Parse the SAF URI to get the tree document path:
// content://...tree/...document/.../oldName.flac
// We need tree URI and relative dir to create the new file
final uri = Uri.parse(item.filePath);
final pathSegments = uri.pathSegments;
// Try to find 'tree' and 'document' segments
String? treeUri;
String relativeDir = '';
String oldFileName = '';
-1
View File
@@ -162,7 +162,6 @@ class _MainShellState extends ConsumerState<MainShell>
if (!Platform.isAndroid) return;
final settings = ref.read(settingsProvider);
// Only show if user is still on legacy storage mode with a download dir set
if (settings.storageMode == 'saf') return;
if (settings.downloadDirectory.isEmpty) return;
+2 -23
View File
@@ -97,7 +97,6 @@ class UnifiedLibraryItem {
} else if (item.bitDepth != null &&
item.bitDepth! > 0 &&
item.sampleRate != null) {
// Lossless format with actual bit depth
quality = buildDisplayAudioQuality(
bitDepth: item.bitDepth,
sampleRate: item.sampleRate,
@@ -108,7 +107,7 @@ class UnifiedLibraryItem {
trackName: item.trackName,
artistName: item.artistName,
albumName: item.albumName,
coverUrl: null, // Local library doesn't have cover URLs
coverUrl: null,
localCoverPath: item.coverPath,
filePath: item.filePath,
quality: quality,
@@ -170,9 +169,6 @@ class UnifiedLibraryItem {
}
if (localItem != null) {
final l = localItem!;
// Store coverPath (even local file paths) in coverUrl so playlist
// entries retain the cover. All renderers must check whether the
// value is a URL or a local path and use the appropriate widget.
return Track(
id: l.id,
name: l.trackName,
@@ -188,7 +184,6 @@ class UnifiedLibraryItem {
source: 'local',
);
}
// Fallback — should not happen
return Track(
id: id,
name: trackName,
@@ -4889,15 +4884,12 @@ class _QueueTabState extends ConsumerState<QueueTab> {
for (final id in _selectedIds) {
final item = itemsById[id];
if (item == null) continue;
// Detect format: use safFileName for download history SAF items,
// item.localItem?.format for local library items, file extension as fallback
String nameToCheck;
if (item.historyItem?.safFileName != null &&
item.historyItem!.safFileName!.isNotEmpty) {
nameToCheck = item.historyItem!.safFileName!.toLowerCase();
} else if (item.localItem?.format != null &&
item.localItem!.format!.isNotEmpty) {
// Synthesize a fake extension to keep detection unified
nameToCheck = '.${item.localItem!.format!.toLowerCase()}';
} else {
nameToCheck = item.filePath.toLowerCase();
@@ -4912,7 +4904,6 @@ class _QueueTabState extends ConsumerState<QueueTab> {
? 'Opus'
: null;
if (ext == null || ext == targetFormat) continue;
// Skip lossy sources when target is lossless (pointless re-encoding)
final isLosslessTarget = targetFormat == 'ALAC' || targetFormat == 'FLAC';
final isLosslessSource = ext == 'FLAC' || ext == 'M4A';
if (isLosslessTarget && !isLosslessSource) continue;
@@ -5060,7 +5051,6 @@ class _QueueTabState extends ConsumerState<QueueTab> {
continue;
}
// Handle SAF write-back
if (isSaf && item.historyItem != null) {
final hi = item.historyItem!;
final treeUri = hi.downloadTreeUri;
@@ -5113,12 +5103,10 @@ class _QueueTabState extends ConsumerState<QueueTab> {
continue;
}
// Delete old SAF file
try {
await PlatformBridge.safDelete(item.filePath);
} catch (_) {}
// Update history
await historyDb.updateFilePath(
hi.id,
safUri,
@@ -5127,7 +5115,6 @@ class _QueueTabState extends ConsumerState<QueueTab> {
clearAudioSpecs: true,
);
}
// Cleanup temp files
try {
await File(newPath).delete();
} catch (_) {}
@@ -5137,7 +5124,6 @@ class _QueueTabState extends ConsumerState<QueueTab> {
} catch (_) {}
}
} else if (isSaf && item.localItem != null) {
// Local library SAF item: parse content URI to derive tree and dir
final uri = Uri.parse(item.filePath);
final pathSegments = uri.pathSegments;
@@ -5214,7 +5200,6 @@ class _QueueTabState extends ConsumerState<QueueTab> {
await LibraryDatabase.instance.deleteByPath(item.filePath);
}
// Cleanup temp files
try {
await File(newPath).delete();
} catch (_) {}
@@ -5224,7 +5209,6 @@ class _QueueTabState extends ConsumerState<QueueTab> {
} catch (_) {}
}
} else if (item.historyItem != null) {
// Regular file - update history path
await historyDb.updateFilePath(
item.historyItem!.id,
newPath,
@@ -5232,17 +5216,13 @@ class _QueueTabState extends ConsumerState<QueueTab> {
clearAudioSpecs: true,
);
} else if (item.localItem != null) {
// Regular local library file - delete old db entry, rescan picks up new file
await LibraryDatabase.instance.deleteByPath(item.filePath);
}
successCount++;
} catch (_) {
// Continue to next item on error
}
} catch (_) {}
}
// Reload history and local library to reflect path changes in UI
ref.read(downloadHistoryProvider.notifier).reloadFromStorage();
ref.read(localLibraryProvider.notifier).reloadFromStorage();
@@ -5264,7 +5244,6 @@ class _QueueTabState extends ConsumerState<QueueTab> {
}
}
/// Bottom action bar for selection mode (Material Design 3 style)
Widget _buildSelectionBottomBar(
BuildContext context,
ColorScheme colorScheme,
@@ -61,7 +61,7 @@ class _ExtensionDetailPageState extends ConsumerState<ExtensionDetailPage> {
final hasError = extension.status == 'error';
return PopScope(
canPop: true, // Always allow back gesture
canPop: true,
child: Scaffold(
body: CustomScrollView(
slivers: [
+1 -3
View File
@@ -61,7 +61,7 @@ class _ExtensionsPageState extends ConsumerState<ExtensionsPage> {
final topPadding = normalizedHeaderTopPadding(context);
return PopScope(
canPop: true, // Always allow back gesture
canPop: true,
child: Scaffold(
body: CustomScrollView(
slivers: [
@@ -600,14 +600,12 @@ class _SearchProviderSelector extends ConsumerWidget {
.where((e) => e.enabled && e.hasCustomSearch)
.toList();
// Always allow tapping: built-in providers are always available
final hasAnyProvider =
searchProviders.isNotEmpty || _builtInProviders.isNotEmpty;
String currentProviderName = context.l10n.extensionDefaultProvider;
if (settings.searchProvider != null &&
settings.searchProvider!.isNotEmpty) {
// Check built-in first
if (_builtInProviders.containsKey(settings.searchProvider)) {
currentProviderName = _builtInProviders[settings.searchProvider]!;
} else {
@@ -23,21 +23,15 @@ class _LibrarySettingsPageState extends ConsumerState<LibrarySettingsPage> {
int _androidSdkVersion = 0;
bool _hasStoragePermission = false;
/// Convert SAF content URI to a readable display path
String _getDisplayPath(String path) {
if (!path.startsWith('content://')) return path;
// Extract the path portion from SAF tree URI
// e.g. content://com.android.externalstorage.documents/tree/primary%3AMusic
// -> /storage/emulated/0/Music
try {
final uri = Uri.parse(path);
final treePath =
uri.pathSegments.last; // e.g. "primary:Music" or "primary%3AMusic"
final treePath = uri.pathSegments.last;
final decoded = Uri.decodeComponent(treePath);
if (decoded.startsWith('primary:')) {
return '/storage/emulated/0/${decoded.substring('primary:'.length)}';
}
// For SD card or other volumes, just show the decoded path
return decoded;
} catch (_) {
return path;
+1 -1
View File
@@ -136,7 +136,7 @@ class _LogScreenState extends State<LogScreen> {
final logs = _filteredLogs;
return PopScope(
canPop: true, // Always allow back gesture
canPop: true,
child: Scaffold(
body: CustomScrollView(
controller: _scrollController,
@@ -19,7 +19,7 @@ class OptionsSettingsPage extends ConsumerWidget {
final topPadding = normalizedHeaderTopPadding(context);
return PopScope(
canPop: true, // Always allow back gesture
canPop: true,
child: Scaffold(
body: CustomScrollView(
slivers: [
+1 -7
View File
@@ -441,14 +441,9 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
void _nextPage() {
bool canProceed = false;
// Step 0 is Welcome, always can proceed
if (_currentStep == 0) {
canProceed = true;
} else {
// Logic for other steps (offset by 1 because of welcome step)
// Step 1: Storage
// Step 2: Notification (if android 13+) OR Directory
// etc.
canProceed = _isStepCompleted(_currentStep);
}
@@ -470,9 +465,8 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
}
bool _isStepCompleted(int step) {
if (step == 0) return true; // Welcome
if (step == 0) return true;
// Adjust step index for logic because we added Welcome at 0
final logicStep = step - 1;
if (_androidSdkVersion >= 33) {
+10 -12
View File
@@ -60,19 +60,19 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
bool _fileExists = false;
bool _hasCheckedFile = false;
int? _fileSize;
String? _lyrics; // Cleaned lyrics for display (no timestamps)
String? _rawLyrics; // Raw LRC with timestamps for embedding
String? _lyrics;
String? _rawLyrics;
bool _lyricsLoading = false;
String? _lyricsError;
String? _lyricsSource;
bool _showTitleInAppBar = false;
bool _lyricsEmbedded = false;
bool _isEmbedding = false; // Track embed operation in progress
bool _isEmbedding = false;
bool _isInstrumental = false;
bool _isConverting = false; // Track convert operation in progress
bool _isConverting = false;
bool _hasMetadataChanges = false;
bool _hasLoadedResolvedAudioMetadata = false;
Map<String, dynamic>? _editedMetadata; // Overrides after metadata edit
Map<String, dynamic>? _editedMetadata;
String? _embeddedCoverPreviewPath;
final ScrollController _scrollController = ScrollController();
static final RegExp _lrcTimestampPattern = RegExp(
@@ -577,7 +577,6 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
String get cleanFilePath {
var path = _filePath;
if (path.startsWith('EXISTS:')) path = path.substring(7);
// Strip CUE virtual path suffix for filesystem operations
if (isCueVirtualPath(path)) path = stripCueTrackSuffix(path);
return path;
}
@@ -1707,7 +1706,7 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
_spotifyId ?? '',
trackName,
artistName,
filePath: null, // Don't check file again
filePath: null,
durationMs: durationMs,
).timeout(const Duration(seconds: 20));
@@ -1733,9 +1732,9 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
final cleanLyrics = _cleanLrcForDisplay(lrcText);
setState(() {
_lyrics = cleanLyrics;
_rawLyrics = lrcText; // Keep raw LRC with timestamps for embedding
_rawLyrics = lrcText;
_lyricsSource = source.isNotEmpty ? source : null;
_lyricsEmbedded = false; // Lyrics from online, not embedded
_lyricsEmbedded = false;
_lyricsLoading = false;
});
}
@@ -1762,7 +1761,6 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
setState(() => _isEmbedding = true);
// Capture l10n strings before async gaps to avoid use_build_context_synchronously
final l10nFailedToWriteStorage = context.l10n.snackbarFailedToWriteStorage;
final l10nFailedToEmbedLyrics = context.l10n.snackbarFailedToEmbedLyrics;
final l10nUnsupportedFormat = context.l10n.snackbarUnsupportedAudioFormat;
@@ -3556,7 +3554,7 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
bitrate: bitrate,
metadata: metadata,
coverPath: coverPath,
deleteOriginal: !isSaf, // Don't delete temp copy for SAF, we handle it
deleteOriginal: !isSaf,
);
if (coverPath != null) {
@@ -3627,7 +3625,7 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
newExt = '.flac';
mimeType = 'audio/flac';
break;
default: // mp3
default:
newExt = '.mp3';
mimeType = 'audio/mpeg';
break;
+2 -2
View File
@@ -198,8 +198,8 @@ class CsvImportService {
artistName: artistName ?? 'Unknown Artist',
albumName: albumName ?? 'Unknown Album',
isrc: isrc,
duration: 0, // Will be updated by enrichment later
coverUrl: null, // Will be fetched by enrichment
duration: 0,
coverUrl: null,
),
);
}
-7
View File
@@ -1437,7 +1437,6 @@ class FFmpegService {
final cmdBuffer = StringBuffer();
cmdBuffer.write('-i "$inputPath" ');
// Cover art as second input for M4A attached picture
final hasCover =
coverPath != null &&
coverPath.trim().isNotEmpty &&
@@ -1455,7 +1454,6 @@ class FFmpegService {
cmdBuffer.write('-c:a alac ');
cmdBuffer.write('-map_metadata -1 ');
// Embed M4A metadata tags
final m4aTags = _convertToM4aTags(metadata);
for (final entry in m4aTags.entries) {
final sanitized = entry.value.replaceAll('"', '\\"');
@@ -1764,7 +1762,6 @@ class FFmpegService {
final outputPaths = <String>[];
final inputExt = audioPath.toLowerCase().split('.').last;
// For lossless formats, keep as FLAC; for others, keep original format
final outputExt =
(inputExt == 'flac' ||
inputExt == 'wav' ||
@@ -1836,14 +1833,10 @@ class FFmpegService {
final result = await _execute(command);
if (!result.success) {
_log.e('CUE split failed for track ${track.number}: ${result.output}');
// Continue with remaining tracks instead of failing completely
continue;
}
// Embed cover art if available (for FLAC output)
if (coverPath != null && coverPath.isNotEmpty && outputExt == 'flac') {
// Use the Go backend for FLAC cover embedding via PlatformBridge
// (handled by the caller)
}
outputPaths.add(outputPath);
-18
View File
@@ -1081,7 +1081,6 @@ class PlatformBridge {
}
}
/// Set the directory for caching extracted cover art
static Future<void> setLibraryCoverCacheDir(String cacheDir) async {
_log.i('setLibraryCoverCacheDir: $cacheDir');
await _channel.invokeMethod('setLibraryCoverCacheDir', {
@@ -1089,8 +1088,6 @@ class PlatformBridge {
});
}
/// Scan a folder for audio files and read their metadata
/// Returns a list of track metadata
static Future<List<Map<String, dynamic>>> scanLibraryFolder(
String folderPath,
) async {
@@ -1102,10 +1099,6 @@ class PlatformBridge {
return list.map((e) => e as Map<String, dynamic>).toList();
}
/// Perform an incremental scan of the library folder
/// Only scans files that are new or have changed since last scan
/// [existingFiles] is a map of filePath -> modTime (unix millis)
/// Returns IncrementalScanResult with scanned items, deleted paths, and skip count
static Future<Map<String, dynamic>> scanLibraryFolderIncremental(
String folderPath,
Map<String, int> existingFiles,
@@ -1140,8 +1133,6 @@ class PlatformBridge {
return list.map((e) => e as Map<String, dynamic>).toList();
}
/// Incremental SAF tree scan - only scans new or modified files
/// Returns a map with 'files' (new/changed) and 'removedUris' (deleted files)
static Future<Map<String, dynamic>> scanSafTreeIncremental(
String treeUri,
Map<String, int> existingFiles,
@@ -1167,8 +1158,6 @@ class PlatformBridge {
return jsonDecode(result as String) as Map<String, dynamic>;
}
/// Get last-modified timestamps for a list of SAF file URIs.
/// Returns map uri -> modTime (unix millis), only for files that still exist.
static Future<Map<String, int>> getSafFileModTimes(List<String> uris) async {
final result = await _channel.invokeMethod('getSafFileModTimes', {
'uris': jsonEncode(uris),
@@ -1177,7 +1166,6 @@ class PlatformBridge {
return map.map((key, value) => MapEntry(key, (value as num).toInt()));
}
/// Get current library scan progress
static Future<Map<String, dynamic>> getLibraryScanProgress() async {
final result = await _channel.invokeMethod('getLibraryScanProgress');
return _decodeMapResult(result);
@@ -1189,7 +1177,6 @@ class PlatformBridge {
);
}
/// Cancel ongoing library scan
static Future<void> cancelLibraryScan() async {
await _channel.invokeMethod('cancelLibraryScan');
}
@@ -1249,7 +1236,6 @@ class PlatformBridge {
}
}
/// Read metadata from a single audio file
static Future<Map<String, dynamic>?> readAudioMetadata(
String filePath,
) async {
@@ -1369,10 +1355,6 @@ class PlatformBridge {
await _channel.invokeMethod('clearStoreCache');
}
/// Parse a .cue file and return split information (track listing, timing, metadata).
/// Returns a map with: cue_path, audio_path, album, artist, genre, date, tracks[]
/// Each track has: number, title, artist, isrc, composer, start_sec, end_sec
/// [audioDir] optionally overrides the directory for audio file resolution (used for SAF).
static Future<Map<String, dynamic>> parseCueSheet(
String cuePath, {
String audioDir = '',
-3
View File
@@ -80,7 +80,6 @@ class ShareIntentService {
bool isInitial = false,
}) {
for (final file in files) {
// Check both path and message - apps may share URL in either field
final textsToCheck = [file.path, if (file.message != null) file.message!];
for (final textToCheck in textsToCheck) {
@@ -100,13 +99,11 @@ class ShareIntentService {
String? _extractMusicUrl(String text) {
if (text.isEmpty) return null;
// Try Spotify URI first
final uriMatch = _spotifyUriPattern.firstMatch(text);
if (uriMatch != null) {
return uriMatch.group(0);
}
// Try all URL patterns
final patterns = [
_spotifyUrlPattern,
_deezerUrlPattern,
-8
View File
@@ -27,7 +27,6 @@ Future<void> navigateToArtist(
final normalizedArtistId = _normalizeArtistId(artistId);
// If we have a valid artist ID already, navigate directly
if (normalizedArtistId != null &&
_canNavigateArtistDirectly(
artistId: normalizedArtistId,
@@ -43,7 +42,6 @@ Future<void> navigateToArtist(
return;
}
// Search Deezer to resolve the artist ID
_showLoadingSnackBar(context, 'Looking up artist...');
try {
final results = await PlatformBridge.searchDeezerAll(
@@ -60,7 +58,6 @@ Future<void> navigateToArtist(
return;
}
// Find best match - prefer exact name match (case-insensitive)
Map<String, dynamic>? bestMatch;
final lowerName = artistName.toLowerCase().trim();
for (final a in artistList) {
@@ -113,7 +110,6 @@ Future<void> navigateToAlbum(
}) async {
if (albumName.isEmpty) return;
// If we have a valid album ID already, navigate directly
if (albumId != null &&
albumId.isNotEmpty &&
albumId != 'unknown' &&
@@ -128,16 +124,13 @@ Future<void> navigateToAlbum(
return;
}
// If it's extension-based content without an ID, can't search Deezer for it
if (extensionId != null) {
_showUnavailable(context, 'Album');
return;
}
// Search Deezer to resolve the album ID
_showLoadingSnackBar(context, 'Looking up album...');
try {
// Build search query: "albumName artistName" for better accuracy
final query = artistName != null && artistName.isNotEmpty
? '$albumName $artistName'
: albumName;
@@ -156,7 +149,6 @@ Future<void> navigateToAlbum(
return;
}
// Find best match - prefer exact name match (case-insensitive)
Map<String, dynamic>? bestMatch;
final lowerName = albumName.toLowerCase().trim();
for (final a in albumList) {
-24
View File
@@ -14,8 +14,6 @@ import 'package:path_provider/path_provider.dart';
import 'package:spotiflac_android/l10n/l10n.dart';
import 'package:spotiflac_android/services/platform_bridge.dart';
// Data models
class AudioAnalysisData {
final String filePath;
final int fileSize;
@@ -98,8 +96,6 @@ class SpectrogramData {
});
}
// Audio Analysis Card Widget
class AudioAnalysisCard extends StatefulWidget {
final String filePath;
@@ -179,7 +175,6 @@ class _AudioAnalysisCardState extends State<AudioAnalysisCard> {
});
try {
// Try loading from cache first
final cached = await _loadFromCache(widget.filePath);
AudioAnalysisData data;
@@ -187,7 +182,6 @@ class _AudioAnalysisCardState extends State<AudioAnalysisCard> {
data = cached;
} else {
data = await _runAnalysis(widget.filePath);
// Save to cache (fire-and-forget)
_saveToCache(widget.filePath, data);
}
@@ -214,8 +208,6 @@ class _AudioAnalysisCardState extends State<AudioAnalysisCard> {
}
}
// Analysis cache
static String _cacheKey(String filePath) {
var hash = 0xcbf29ce484222325;
for (final byte in utf8.encode(filePath)) {
@@ -267,8 +259,6 @@ class _AudioAnalysisCardState extends State<AudioAnalysisCard> {
} catch (_) {}
}
// Analysis pipeline
Future<AudioAnalysisData> _runAnalysis(String filePath) async {
await FFmpegKitConfig.setLogLevel(Level.avLogError);
@@ -302,7 +292,6 @@ class _AudioAnalysisCardState extends State<AudioAnalysisCard> {
),
);
// Total samples from file metadata (not truncated PCM)
final trueTotalSamples =
(info.duration * info.sampleRate * info.channels).round();
@@ -468,7 +457,6 @@ class _AudioAnalysisCardState extends State<AudioAnalysisCard> {
final cs = Theme.of(context).colorScheme;
final l10n = context.l10n;
// Still checking cache, show nothing yet
if (_checkingCache) return const SizedBox.shrink();
if (_analyzing) {
@@ -575,8 +563,6 @@ class _AudioAnalysisCardState extends State<AudioAnalysisCard> {
}
}
// Internal types
class _MediaInfo {
final int fileSize;
final int sampleRate;
@@ -623,8 +609,6 @@ class _AnalysisResult {
});
}
// Isolate: PCM analysis + FFT spectrogram
_AnalysisResult _analyzeInIsolate(_AnalysisParams params) {
final byteData = ByteData.sublistView(params.pcmBytes);
final sampleCount = params.pcmBytes.length ~/ 2;
@@ -767,8 +751,6 @@ Float64List _fft(Float64List realInput) {
return data;
}
// Audio Info Card
class _AudioInfoCard extends StatelessWidget {
final AudioAnalysisData data;
@@ -945,8 +927,6 @@ class _MetricChip extends StatelessWidget {
}
}
// Spectrogram View
class _SpectrogramView extends StatelessWidget {
final ui.Image image;
final SpectrogramData spectrum;
@@ -1011,8 +991,6 @@ class _ImagePainter extends CustomPainter {
bool shouldRepaint(covariant _ImagePainter old) => old.image != image;
}
// Spectrogram pixel-buffer rendering (runs in isolate)
class _SpectrogramRenderParams {
final SpectrogramData spectrum;
final int width;
@@ -1031,7 +1009,6 @@ Uint8List _renderSpectrogramPixels(_SpectrogramRenderParams params) {
final spectrum = params.spectrum;
final pixels = Uint8List(w * h * 4);
// Fill black
for (int i = 3; i < pixels.length; i += 4) {
pixels[i] = 255;
}
@@ -1041,7 +1018,6 @@ Uint8List _renderSpectrogramPixels(_SpectrogramRenderParams params) {
final freqBins = spectrum.freqBins;
// dB range
double minDB = 0;
double maxDB = -200;
for (final slice in slices) {
-1
View File
@@ -166,7 +166,6 @@ class _GitHubPainter extends CustomPainter {
9.47 * scale, 17.93 * scale,
9.81 * scale, 17.63 * scale,
);
// Bottom
path.cubicTo(
7.15 * scale, 17.33 * scale,
4.34 * scale, 16.33 * scale,