chore: housekeeping cleanup and code deduplication

- Remove stray tracked files (root AndroidManifest.xml, build.gradle.bak, temp_project template)
- Move README-only images out of app asset bundle to reduce APK/IPA size (~1.68MB)
- Fix logo filename typo (transparant -> transparent)
- Deduplicate _readPositiveInt into shared int_utils.dart
- Deduplicate _themeModeFromString (reuse from theme_settings.dart)
- Remove deprecated LocalLibraryState.items getter
- Remove unused sqflite_common_ffi dependency
- Update apps.json version to 4.5.1
- Fix Flutter version in CONTRIBUTING.md (3.38.1 -> 3.41.5)
- Improve .gitignore patterns (NUL, *.bak, root AndroidManifest.xml)
This commit is contained in:
zarzet 2026-05-08 21:37:56 +07:00
parent 1bd54c530b
commit 904b45e8f6
24 changed files with 56 additions and 161 deletions

5
.gitignore vendored
View file

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

Binary file not shown.

View file

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

View file

@ -1,9 +1,9 @@
<div align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="assets/images/banner-readme-dark.png">
<source media="(prefers-color-scheme: light)" srcset="assets/images/banner-readme-light.png">
<img alt="SpotiFLAC Mobile" src="assets/images/banner-readme-light.png" width="650" height="auto">
<source media="(prefers-color-scheme: dark)" srcset="assets/readme/banner-readme-dark.png">
<source media="(prefers-color-scheme: light)" srcset="assets/readme/banner-readme-light.png">
<img alt="SpotiFLAC Mobile" src="assets/readme/banner-readme-light.png" width="650" height="auto">
</picture>
<p align="center">
@ -28,10 +28,10 @@
## Screenshots
<p align="center">
<img src="assets/images/1.jpg?v=2" width="200" />
<img src="assets/images/2.jpg?v=2" width="200" />
<img src="assets/images/3.jpg?v=2" width="200" />
<img src="assets/images/4.jpg?v=2" width="200" />
<img src="assets/readme/1.jpg?v=2" width="200" />
<img src="assets/readme/2.jpg?v=2" width="200" />
<img src="assets/readme/3.jpg?v=2" width="200" />
<img src="assets/readme/4.jpg?v=2" width="200" />
</p>
---

View file

@ -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'
}

View file

@ -1,5 +0,0 @@
package com.example.temp_project
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()

View file

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

View file

Before

Width:  |  Height:  |  Size: 143 KiB

After

Width:  |  Height:  |  Size: 143 KiB

View file

Before

Width:  |  Height:  |  Size: 539 KiB

After

Width:  |  Height:  |  Size: 539 KiB

View file

Before

Width:  |  Height:  |  Size: 811 KiB

After

Width:  |  Height:  |  Size: 811 KiB

View file

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 104 KiB

View file

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 122 KiB

View file

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View file

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View file

@ -46,7 +46,7 @@ class ThemeSettings {
factory ThemeSettings.fromJson(Map<String, dynamic> 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,

View file

@ -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<DownloadHistoryState> {
}
}
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<DownloadHistoryState> {
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<DownloadHistoryState> {
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 &&

View file

@ -54,11 +54,6 @@ class LocalLibraryState {
_isrcSet = isrcSet ?? const <String>{},
_filePathById = filePathById ?? const <String, String>{};
@Deprecated(
'LocalLibraryState no longer owns full track rows. Use DB-backed page providers.',
)
List<LocalLibraryItem> get items => const <LocalLibraryItem>[];
bool hasIsrc(String isrc) => _isrcSet.contains(isrc);
bool hasTrack(String trackName, String artistName) {

View file

@ -25,7 +25,7 @@ class ThemeNotifier extends Notifier<ThemeSettings> {
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<ThemeSettings> {
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,
);
}
}

View file

@ -1549,7 +1549,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
shape: BoxShape.circle,
),
child: Image.asset(
'assets/images/logo-transparant.png',
'assets/images/logo-transparent.png',
color: colorScheme.onPrimary,
fit: BoxFit.contain,
errorBuilder: (_, _, _) => ClipRRect(

View file

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

View file

@ -681,7 +681,7 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
'assets/images/logo-transparant.png',
'assets/images/logo-transparent.png',
width: logoSize,
height: logoSize,
color: colorScheme.primary,

View file

@ -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<TrackMetadataScreen> {
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<TrackMetadataScreen> {
// 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<TrackMetadataScreen> {
}
int? get totalTracks =>
_readPositiveInt(_editedMetadata?['total_tracks']) ??
readPositiveInt(_editedMetadata?['total_tracks']) ??
(_isLocalItem
? _localLibraryItem!.totalTracks
: _downloadItem!.totalTracks);
@ -631,7 +632,7 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
}
int? get totalDiscs =>
_readPositiveInt(_editedMetadata?['total_discs']) ??
readPositiveInt(_editedMetadata?['total_discs']) ??
(_isLocalItem
? _localLibraryItem!.totalDiscs
: _downloadItem!.totalDiscs);
@ -670,13 +671,13 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
_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<TrackMetadataScreen> {
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<TrackMetadataScreen> {
};
final initialDurationSeconds =
_readPositiveInt(fileMetadata?['duration']) ?? duration ?? 0;
readPositiveInt(fileMetadata?['duration']) ?? duration ?? 0;
if (!context.mounted) return;

13
lib/utils/int_utils.dart Normal file
View file

@ -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;
}

View file

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