diff --git a/.gitignore b/.gitignore index 9150a859..b81c3a4b 100644 --- a/.gitignore +++ b/.gitignore @@ -44,6 +44,7 @@ go_backend/*.xcframework/ # Android android/.gradle/ android/app/libs/gobackend.aar +android/app/libs/gobackend-sources.jar android/local.properties android/*.iml android/key.properties @@ -57,7 +58,6 @@ ios/Pods/ ios/.symlinks/ ios/Flutter/Flutter.framework/ ios/Flutter/Flutter.podspec -android/app/libs/gobackend-sources.jar # Extension folder extension/ @@ -67,7 +67,10 @@ AGENTS.md # Temp/misc nul +NUL network_requests.txt +*.bak +/AndroidManifest.xml # Log files *.log diff --git a/AndroidManifest.xml b/AndroidManifest.xml deleted file mode 100644 index e8ef0445..00000000 Binary files a/AndroidManifest.xml and /dev/null differ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2c1e7b6e..7f4c5b26 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -86,7 +86,7 @@ Translation files are located in `lib/l10n/arb/`. git remote add upstream https://github.com/zarzet/SpotiFLAC-Mobile.git ``` -3. **Use FVM (Flutter Version: 3.38.1)** +3. **Use FVM (Flutter Version: 3.41.5)** ```bash fvm use ``` diff --git a/README.md b/README.md index fe159ce0..07c9783d 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@
- - - SpotiFLAC Mobile + + + SpotiFLAC Mobile

@@ -28,10 +28,10 @@ ## Screenshots

- - - - + + + +

--- diff --git a/android/app/build.gradle.bak b/android/app/build.gradle.bak deleted file mode 100644 index f5ed5e6b..00000000 --- a/android/app/build.gradle.bak +++ /dev/null @@ -1,71 +0,0 @@ -plugins { - id "com.android.application" - id "kotlin-android" - id "dev.flutter.flutter-gradle-plugin" -} - -def localProperties = new Properties() -def localPropertiesFile = rootProject.file('local.properties') -if (localPropertiesFile.exists()) { - localPropertiesFile.withReader('UTF-8') { reader -> - localProperties.load(reader) - } -} - -def flutterVersionCode = localProperties.getProperty('flutter.versionCode') -if (flutterVersionCode == null) { - flutterVersionCode = '1' -} - -def flutterVersionName = localProperties.getProperty('flutter.versionName') -if (flutterVersionName == null) { - flutterVersionName = '1.0' -} - -android { - namespace "com.zarz.spotiflac" - compileSdk flutter.compileSdkVersion - ndkVersion flutter.ndkVersion - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - - kotlinOptions { - jvmTarget = '1.8' - } - - sourceSets { - main.java.srcDirs += 'src/main/kotlin' - } - - defaultConfig { - applicationId "com.zarz.spotiflac" - minSdkVersion flutter.minSdkVersion - targetSdk flutter.targetSdkVersion - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName - } - - buildTypes { - release { - signingConfig signingConfigs.debug - minifyEnabled false - shrinkResources false - } - } -} - -flutter { - source '../..' -} - -dependencies { - // Go backend library (gomobile generated) - implementation fileTree(dir: 'libs', include: ['*.aar']) - - // Kotlin coroutines for async Go backend calls - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3' - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3' -} diff --git a/android/app/src/main/kotlin/com/example/temp_project/MainActivity.kt b/android/app/src/main/kotlin/com/example/temp_project/MainActivity.kt deleted file mode 100644 index 3aebb9bc..00000000 --- a/android/app/src/main/kotlin/com/example/temp_project/MainActivity.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.temp_project - -import io.flutter.embedding.android.FlutterActivity - -class MainActivity : FlutterActivity() diff --git a/apps.json b/apps.json index a6cc8c4c..4bd95482 100644 --- a/apps.json +++ b/apps.json @@ -7,9 +7,9 @@ "name": "SpotiFLAC Mobile", "bundleIdentifier": "com.zarzet.spotiflac", "developerName": "zarzet", - "version": "4.5.0", + "version": "4.5.1", "versionDate": "2026-05-06", - "downloadURL": "https://github.com/zarzet/SpotiFLAC-Mobile/releases/download/v4.5.0/SpotiFLAC-v4.5.0-ios-unsigned.ipa", + "downloadURL": "https://github.com/zarzet/SpotiFLAC-Mobile/releases/download/v4.5.1/SpotiFLAC-v4.5.1-ios-unsigned.ipa", "localizedDescription": "SpotiFLAC Mobile is written in Flutter. Download tracks in true FLAC from Tidal, Qobuz, & Amazon Music.", "iconURL": "https://raw.githubusercontent.com/zarzet/SpotiFLAC-Mobile/main/assets/images/logo.png", "size": 37191956 diff --git a/assets/images/logo-transparant.png b/assets/images/logo-transparent.png similarity index 100% rename from assets/images/logo-transparant.png rename to assets/images/logo-transparent.png diff --git a/assets/images/1.jpg b/assets/readme/1.jpg similarity index 100% rename from assets/images/1.jpg rename to assets/readme/1.jpg diff --git a/assets/images/2.jpg b/assets/readme/2.jpg similarity index 100% rename from assets/images/2.jpg rename to assets/readme/2.jpg diff --git a/assets/images/3.jpg b/assets/readme/3.jpg similarity index 100% rename from assets/images/3.jpg rename to assets/readme/3.jpg diff --git a/assets/images/4.jpg b/assets/readme/4.jpg similarity index 100% rename from assets/images/4.jpg rename to assets/readme/4.jpg diff --git a/assets/images/banner-readme-dark.png b/assets/readme/banner-readme-dark.png similarity index 100% rename from assets/images/banner-readme-dark.png rename to assets/readme/banner-readme-dark.png diff --git a/assets/images/banner-readme-light.png b/assets/readme/banner-readme-light.png similarity index 100% rename from assets/images/banner-readme-light.png rename to assets/readme/banner-readme-light.png diff --git a/lib/models/theme_settings.dart b/lib/models/theme_settings.dart index 5f331d1e..2163cf8f 100644 --- a/lib/models/theme_settings.dart +++ b/lib/models/theme_settings.dart @@ -46,7 +46,7 @@ class ThemeSettings { factory ThemeSettings.fromJson(Map json) { return ThemeSettings( - themeMode: _themeModeFromString(json[kThemeModeKey] as String?), + themeMode: themeModeFromString(json[kThemeModeKey] as String?), useDynamicColor: json[kUseDynamicColorKey] as bool? ?? true, seedColorValue: json[kSeedColorKey] as int? ?? kDefaultSeedColor, useAmoled: json[kUseAmoledKey] as bool? ?? false, @@ -68,7 +68,7 @@ class ThemeSettings { themeMode.hashCode ^ useDynamicColor.hashCode ^ seedColorValue.hashCode ^ useAmoled.hashCode; } -ThemeMode _themeModeFromString(String? value) { +ThemeMode themeModeFromString(String? value) { if (value == null) return ThemeMode.system; return ThemeMode.values.firstWhere( (e) => e.name == value, diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index 21e0db5e..3990951f 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -21,6 +21,7 @@ import 'package:spotiflac_android/utils/logger.dart' hide log; import 'package:spotiflac_android/utils/file_access.dart'; import 'package:spotiflac_android/utils/string_utils.dart'; import 'package:spotiflac_android/utils/artist_utils.dart'; +import 'package:spotiflac_android/utils/int_utils.dart'; export 'package:spotiflac_android/services/history_database.dart' show HistoryLookupRequest, HistoryBatchLookupRequest; @@ -34,19 +35,8 @@ final _trimUnderscoresAndSpacesRegex = RegExp(r'^[_ ]+|[_ ]+$'); final _multiWhitespaceRegex = RegExp(r'\s+'); final _multiUnderscoreRegex = RegExp(r'_+'); -int? _readPositiveIntValue(dynamic value) { - if (value == null) return null; - if (value is num) { - final asInt = value.toInt(); - return asInt > 0 ? asInt : null; - } - final parsed = int.tryParse(value.toString()); - if (parsed == null || parsed <= 0) return null; - return parsed; -} - int? _readPositiveBitrateKbps(dynamic value) { - final parsed = _readPositiveIntValue(value); + final parsed = readPositiveInt(value); if (parsed == null) return null; return parsed >= 10000 ? (parsed / 1000).round() : parsed; } @@ -666,17 +656,6 @@ class DownloadHistoryNotifier extends Notifier { } } - int? _readPositiveInt(dynamic value) { - if (value == null) return null; - if (value is num) { - final asInt = value.toInt(); - return asInt > 0 ? asInt : null; - } - final parsed = int.tryParse(value.toString()); - if (parsed == null || parsed <= 0) return null; - return parsed; - } - bool _supportsAudioMetadataProbe(String filePath) { final trimmed = filePath.trim().toLowerCase(); if (trimmed.isEmpty) return false; @@ -757,8 +736,8 @@ class DownloadHistoryNotifier extends Notifier { return null; } - final bitDepth = _readPositiveInt(result['bit_depth']); - final sampleRate = _readPositiveInt(result['sample_rate']); + final bitDepth = readPositiveInt(result['bit_depth']); + final sampleRate = readPositiveInt(result['sample_rate']); final bitrateKbps = _readPositiveBitrateKbps(result['bitrate']); final quality = _resolveDisplayQuality( filePath: filePath, @@ -768,11 +747,11 @@ class DownloadHistoryNotifier extends Notifier { storedQuality: fallbackQuality, ); final composer = normalizeOptionalString(result['composer']?.toString()); - final duration = _readPositiveInt(result['duration']); - final trackNumber = _readPositiveInt(result['track_number']); - final totalTracks = _readPositiveInt(result['total_tracks']); - final discNumber = _readPositiveInt(result['disc_number']); - final totalDiscs = _readPositiveInt(result['total_discs']); + final duration = readPositiveInt(result['duration']); + final trackNumber = readPositiveInt(result['track_number']); + final totalTracks = readPositiveInt(result['total_tracks']); + final discNumber = readPositiveInt(result['disc_number']); + final totalDiscs = readPositiveInt(result['total_discs']); if (quality == null && bitDepth == null && diff --git a/lib/providers/local_library_provider.dart b/lib/providers/local_library_provider.dart index 64b3ded3..8086716b 100644 --- a/lib/providers/local_library_provider.dart +++ b/lib/providers/local_library_provider.dart @@ -54,11 +54,6 @@ class LocalLibraryState { _isrcSet = isrcSet ?? const {}, _filePathById = filePathById ?? const {}; - @Deprecated( - 'LocalLibraryState no longer owns full track rows. Use DB-backed page providers.', - ) - List get items => const []; - bool hasIsrc(String isrc) => _isrcSet.contains(isrc); bool hasTrack(String trackName, String artistName) { diff --git a/lib/providers/theme_provider.dart b/lib/providers/theme_provider.dart index 0ed2c9e0..6421f348 100644 --- a/lib/providers/theme_provider.dart +++ b/lib/providers/theme_provider.dart @@ -25,7 +25,7 @@ class ThemeNotifier extends Notifier { final useAmoled = prefs.getBool(kUseAmoledKey); state = ThemeSettings( - themeMode: _themeModeFromString(modeString), + themeMode: themeModeFromString(modeString), useDynamicColor: useDynamic ?? true, seedColorValue: seedColor ?? kDefaultSeedColor, useAmoled: useAmoled ?? false, @@ -71,12 +71,4 @@ class ThemeNotifier extends Notifier { state = state.copyWith(useAmoled: value); await _saveToStorage(); } - - ThemeMode _themeModeFromString(String? value) { - if (value == null) return ThemeMode.system; - return ThemeMode.values.firstWhere( - (e) => e.name == value, - orElse: () => ThemeMode.system, - ); - } } diff --git a/lib/screens/home_tab.dart b/lib/screens/home_tab.dart index 95ac0669..d0a0faf3 100644 --- a/lib/screens/home_tab.dart +++ b/lib/screens/home_tab.dart @@ -1549,7 +1549,7 @@ class _HomeTabState extends ConsumerState shape: BoxShape.circle, ), child: Image.asset( - 'assets/images/logo-transparant.png', + 'assets/images/logo-transparent.png', color: colorScheme.onPrimary, fit: BoxFit.contain, errorBuilder: (_, _, _) => ClipRRect( diff --git a/lib/screens/settings/about_page.dart b/lib/screens/settings/about_page.dart index 73d73edb..74e6244e 100644 --- a/lib/screens/settings/about_page.dart +++ b/lib/screens/settings/about_page.dart @@ -302,7 +302,7 @@ class _AppHeaderCard extends StatelessWidget { shape: BoxShape.circle, ), child: Image.asset( - 'assets/images/logo-transparant.png', + 'assets/images/logo-transparent.png', color: colorScheme.onPrimary, fit: BoxFit.contain, errorBuilder: (_, _, _) => ClipRRect( diff --git a/lib/screens/setup_screen.dart b/lib/screens/setup_screen.dart index f15b5896..eb7eda84 100644 --- a/lib/screens/setup_screen.dart +++ b/lib/screens/setup_screen.dart @@ -681,7 +681,7 @@ class _SetupScreenState extends ConsumerState { mainAxisAlignment: MainAxisAlignment.center, children: [ Image.asset( - 'assets/images/logo-transparant.png', + 'assets/images/logo-transparent.png', width: logoSize, height: logoSize, color: colorScheme.primary, diff --git a/lib/screens/track_metadata_screen.dart b/lib/screens/track_metadata_screen.dart index 0d65a524..54378da5 100644 --- a/lib/screens/track_metadata_screen.dart +++ b/lib/screens/track_metadata_screen.dart @@ -22,6 +22,7 @@ import 'package:spotiflac_android/utils/lyrics_metadata_helper.dart'; import 'package:spotiflac_android/utils/mime_utils.dart'; import 'package:spotiflac_android/utils/image_cache_utils.dart'; import 'package:spotiflac_android/utils/string_utils.dart'; +import 'package:spotiflac_android/utils/int_utils.dart'; import 'package:spotiflac_android/widgets/audio_analysis_widget.dart'; import 'package:spotiflac_android/widgets/cached_cover_image.dart'; @@ -365,9 +366,9 @@ class _TrackMetadataScreenState extends ConsumerState { return; } - final resolvedBitDepth = _readPositiveInt(metadata['bit_depth']); - final resolvedSampleRate = _readPositiveInt(metadata['sample_rate']); - final resolvedDuration = _readPositiveInt(metadata['duration']); + final resolvedBitDepth = readPositiveInt(metadata['bit_depth']); + final resolvedSampleRate = readPositiveInt(metadata['sample_rate']); + final resolvedDuration = readPositiveInt(metadata['duration']); final resolvedAlbum = metadata['album']?.toString(); final resolvedQuality = buildDisplayAudioQuality( bitDepth: resolvedBitDepth ?? bitDepth, @@ -386,10 +387,10 @@ class _TrackMetadataScreenState extends ConsumerState { // Resolve label/copyright from file when the model doesn't carry them // (e.g. local library items, or download history items without these fields). - final resolvedTrackNumber = _readPositiveInt(metadata['track_number']); - final resolvedTotalTracks = _readPositiveInt(metadata['total_tracks']); - final resolvedDiscNumber = _readPositiveInt(metadata['disc_number']); - final resolvedTotalDiscs = _readPositiveInt(metadata['total_discs']); + final resolvedTrackNumber = readPositiveInt(metadata['track_number']); + final resolvedTotalTracks = readPositiveInt(metadata['total_tracks']); + final resolvedDiscNumber = readPositiveInt(metadata['disc_number']); + final resolvedTotalDiscs = readPositiveInt(metadata['total_discs']); final resolvedComposer = metadata['composer']?.toString(); final resolvedLabel = metadata['label']?.toString(); final resolvedCopyright = metadata['copyright']?.toString(); @@ -614,7 +615,7 @@ class _TrackMetadataScreenState extends ConsumerState { } int? get totalTracks => - _readPositiveInt(_editedMetadata?['total_tracks']) ?? + readPositiveInt(_editedMetadata?['total_tracks']) ?? (_isLocalItem ? _localLibraryItem!.totalTracks : _downloadItem!.totalTracks); @@ -631,7 +632,7 @@ class _TrackMetadataScreenState extends ConsumerState { } int? get totalDiscs => - _readPositiveInt(_editedMetadata?['total_discs']) ?? + readPositiveInt(_editedMetadata?['total_discs']) ?? (_isLocalItem ? _localLibraryItem!.totalDiscs : _downloadItem!.totalDiscs); @@ -670,13 +671,13 @@ class _TrackMetadataScreenState extends ConsumerState { _editedMetadata?['composer']?.toString() ?? (_isLocalItem ? _localLibraryItem!.composer : null); int? get duration => - _readPositiveInt(_editedMetadata?['duration']) ?? + readPositiveInt(_editedMetadata?['duration']) ?? (_isLocalItem ? _localLibraryItem!.duration : _downloadItem!.duration); int? get bitDepth => - _readPositiveInt(_editedMetadata?['bit_depth']) ?? + readPositiveInt(_editedMetadata?['bit_depth']) ?? (_isLocalItem ? _localLibraryItem!.bitDepth : _downloadItem!.bitDepth); int? get sampleRate => - _readPositiveInt(_editedMetadata?['sample_rate']) ?? + readPositiveInt(_editedMetadata?['sample_rate']) ?? (_isLocalItem ? _localLibraryItem!.sampleRate : _downloadItem!.sampleRate); @@ -706,17 +707,6 @@ class _TrackMetadataScreenState extends ConsumerState { String? get _quality => _isLocalItem ? null : _downloadItem!.quality; - int? _readPositiveInt(dynamic value) { - if (value == null) return null; - if (value is num) { - final asInt = value.toInt(); - return asInt > 0 ? asInt : null; - } - final parsed = int.tryParse(value.toString()); - if (parsed == null || parsed <= 0) return null; - return parsed; - } - String _displayServiceTrackId(String value) { final raw = value.trim(); if (raw.isEmpty) return raw; @@ -4394,7 +4384,7 @@ class _TrackMetadataScreenState extends ConsumerState { }; final initialDurationSeconds = - _readPositiveInt(fileMetadata?['duration']) ?? duration ?? 0; + readPositiveInt(fileMetadata?['duration']) ?? duration ?? 0; if (!context.mounted) return; diff --git a/lib/utils/int_utils.dart b/lib/utils/int_utils.dart new file mode 100644 index 00000000..52022da3 --- /dev/null +++ b/lib/utils/int_utils.dart @@ -0,0 +1,13 @@ +/// Parses a dynamic value to a positive integer (> 0), or returns null. +/// +/// Accepts [num] and parseable [String] values. +int? readPositiveInt(dynamic value) { + if (value == null) return null; + if (value is num) { + final asInt = value.toInt(); + return asInt > 0 ? asInt : null; + } + final parsed = int.tryParse(value.toString()); + if (parsed == null || parsed <= 0) return null; + return parsed; +} diff --git a/pubspec.yaml b/pubspec.yaml index d14fe516..9b4c7fa2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,7 +27,6 @@ dependencies: path_provider: ^2.1.5 path: ^1.9.0 sqflite: ^2.4.2+1 - sqflite_common_ffi: ^2.4.0+3 # HTTP & Network http: ^1.6.0