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 @@
-
-
-
+
+
+
@@ -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