chore: rebuild dev history without streaming-era commits

This commit is contained in:
zarzet
2026-02-27 13:48:44 +07:00
parent c89600591c
commit ab26d84632
111 changed files with 18282 additions and 3459 deletions
+91
View File
@@ -0,0 +1,91 @@
import 'package:spotiflac_android/models/track.dart';
class PlaybackItem {
final String id;
final String title;
final String artist;
final String album;
final String coverUrl;
final String sourceUri;
final bool isLocal;
final String service;
final int durationMs;
// Stream quality metadata
final String format;
final int bitDepth;
final int sampleRate;
final int bitrate;
// Original track reference for queue operations
final Track? track;
const PlaybackItem({
required this.id,
required this.title,
required this.artist,
this.album = '',
this.coverUrl = '',
required this.sourceUri,
this.isLocal = false,
this.service = '',
this.durationMs = 0,
this.format = '',
this.bitDepth = 0,
this.sampleRate = 0,
this.bitrate = 0,
this.track,
});
PlaybackItem copyWith({
String? sourceUri,
String? service,
String? format,
int? bitDepth,
int? sampleRate,
int? bitrate,
}) {
return PlaybackItem(
id: id,
title: title,
artist: artist,
album: album,
coverUrl: coverUrl,
sourceUri: sourceUri ?? this.sourceUri,
isLocal: isLocal,
service: service ?? this.service,
durationMs: durationMs,
format: format ?? this.format,
bitDepth: bitDepth ?? this.bitDepth,
sampleRate: sampleRate ?? this.sampleRate,
bitrate: bitrate ?? this.bitrate,
track: track,
);
}
/// Human-readable quality label for UI display
String get qualityLabel {
final parts = <String>[];
if (format.isNotEmpty) {
parts.add(format.toUpperCase());
}
if (bitDepth > 0 && sampleRate > 0) {
final srKhz = sampleRate >= 1000
? '${(sampleRate / 1000).toStringAsFixed(sampleRate % 1000 == 0 ? 0 : 1)}kHz'
: '${sampleRate}Hz';
parts.add('$bitDepth-bit / $srKhz');
} else if (bitrate > 0) {
parts.add('${bitrate}kbps');
}
return parts.join(' ');
}
/// Whether this item has cover art that is a local file path
bool get hasLocalCover {
if (coverUrl.isEmpty) return false;
return !coverUrl.startsWith('http://') && !coverUrl.startsWith('https://');
}
}
+24
View File
@@ -11,6 +11,9 @@ class AppSettings {
final String storageMode; // 'app' or 'saf'
final String downloadTreeUri; // SAF persistable tree URI
final bool autoFallback;
final bool autoSkipUnavailableTracks;
final bool smartQueueEnabled; // Enable smart curated autoplay queue
final bool embedMetadata; // Master switch for metadata/cover/lyrics embedding
final bool embedLyrics;
final bool maxQualityCover;
final bool isFirstLaunch;
@@ -76,6 +79,10 @@ class AppSettings {
final String
musixmatchLanguage; // Optional ISO language code for Musixmatch localized lyrics
// Version upgrade tracking
final String
lastSeenVersion; // Last app version the user has acknowledged (e.g. '3.7.0')
const AppSettings({
this.defaultService = 'tidal',
this.audioQuality = 'LOSSLESS',
@@ -84,6 +91,9 @@ class AppSettings {
this.storageMode = 'app',
this.downloadTreeUri = '',
this.autoFallback = true,
this.autoSkipUnavailableTracks = true,
this.smartQueueEnabled = true,
this.embedMetadata = true,
this.embedLyrics = true,
this.maxQualityCover = true,
this.isFirstLaunch = true,
@@ -127,6 +137,7 @@ class AppSettings {
// Lyrics providers default order
this.lyricsProviders = const [
'lrclib',
'spotify_api',
'musixmatch',
'netease',
'apple_music',
@@ -136,6 +147,8 @@ class AppSettings {
this.lyricsIncludeRomanizationNetease = false,
this.lyricsMultiPersonWordByWord = false,
this.musixmatchLanguage = '',
// Version upgrade tracking
this.lastSeenVersion = '',
});
AppSettings copyWith({
@@ -146,6 +159,9 @@ class AppSettings {
String? storageMode,
String? downloadTreeUri,
bool? autoFallback,
bool? autoSkipUnavailableTracks,
bool? smartQueueEnabled,
bool? embedMetadata,
bool? embedLyrics,
bool? maxQualityCover,
bool? isFirstLaunch,
@@ -193,6 +209,8 @@ class AppSettings {
bool? lyricsIncludeRomanizationNetease,
bool? lyricsMultiPersonWordByWord,
String? musixmatchLanguage,
// Version upgrade tracking
String? lastSeenVersion,
}) {
return AppSettings(
defaultService: defaultService ?? this.defaultService,
@@ -202,6 +220,10 @@ class AppSettings {
storageMode: storageMode ?? this.storageMode,
downloadTreeUri: downloadTreeUri ?? this.downloadTreeUri,
autoFallback: autoFallback ?? this.autoFallback,
autoSkipUnavailableTracks:
autoSkipUnavailableTracks ?? this.autoSkipUnavailableTracks,
smartQueueEnabled: smartQueueEnabled ?? this.smartQueueEnabled,
embedMetadata: embedMetadata ?? this.embedMetadata,
embedLyrics: embedLyrics ?? this.embedLyrics,
maxQualityCover: maxQualityCover ?? this.maxQualityCover,
isFirstLaunch: isFirstLaunch ?? this.isFirstLaunch,
@@ -264,6 +286,8 @@ class AppSettings {
lyricsMultiPersonWordByWord:
lyricsMultiPersonWordByWord ?? this.lyricsMultiPersonWordByWord,
musixmatchLanguage: musixmatchLanguage ?? this.musixmatchLanguage,
// Version upgrade tracking
lastSeenVersion: lastSeenVersion ?? this.lastSeenVersion,
);
}
+17 -5
View File
@@ -14,6 +14,9 @@ AppSettings _$AppSettingsFromJson(Map<String, dynamic> json) => AppSettings(
storageMode: json['storageMode'] as String? ?? 'app',
downloadTreeUri: json['downloadTreeUri'] as String? ?? '',
autoFallback: json['autoFallback'] as bool? ?? true,
autoSkipUnavailableTracks: json['autoSkipUnavailableTracks'] as bool? ?? true,
smartQueueEnabled: json['smartQueueEnabled'] as bool? ?? true,
embedMetadata: json['embedMetadata'] as bool? ?? true,
embedLyrics: json['embedLyrics'] as bool? ?? true,
maxQualityCover: json['maxQualityCover'] as bool? ?? true,
isFirstLaunch: json['isFirstLaunch'] as bool? ?? true,
@@ -50,10 +53,7 @@ AppSettings _$AppSettingsFromJson(Map<String, dynamic> json) => AppSettings(
autoExportFailedDownloads:
json['autoExportFailedDownloads'] as bool? ?? false,
downloadNetworkMode: json['downloadNetworkMode'] as String? ?? 'any',
networkCompatibilityMode:
json['networkCompatibilityMode'] as bool? ??
json['songLinkCompatibilityMode'] as bool? ??
false,
networkCompatibilityMode: json['networkCompatibilityMode'] as bool? ?? false,
songLinkRegion: json['songLinkRegion'] as String? ?? 'US',
localLibraryEnabled: json['localLibraryEnabled'] as bool? ?? false,
localLibraryPath: json['localLibraryPath'] as String? ?? '',
@@ -64,7 +64,14 @@ AppSettings _$AppSettingsFromJson(Map<String, dynamic> json) => AppSettings(
(json['lyricsProviders'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
const ['lrclib', 'musixmatch', 'netease', 'apple_music', 'qqmusic'],
const [
'lrclib',
'spotify_api',
'musixmatch',
'netease',
'apple_music',
'qqmusic',
],
lyricsIncludeTranslationNetease:
json['lyricsIncludeTranslationNetease'] as bool? ?? false,
lyricsIncludeRomanizationNetease:
@@ -72,6 +79,7 @@ AppSettings _$AppSettingsFromJson(Map<String, dynamic> json) => AppSettings(
lyricsMultiPersonWordByWord:
json['lyricsMultiPersonWordByWord'] as bool? ?? false,
musixmatchLanguage: json['musixmatchLanguage'] as String? ?? '',
lastSeenVersion: json['lastSeenVersion'] as String? ?? '',
);
Map<String, dynamic> _$AppSettingsToJson(
@@ -84,6 +92,9 @@ Map<String, dynamic> _$AppSettingsToJson(
'storageMode': instance.storageMode,
'downloadTreeUri': instance.downloadTreeUri,
'autoFallback': instance.autoFallback,
'autoSkipUnavailableTracks': instance.autoSkipUnavailableTracks,
'smartQueueEnabled': instance.smartQueueEnabled,
'embedMetadata': instance.embedMetadata,
'embedLyrics': instance.embedLyrics,
'maxQualityCover': instance.maxQualityCover,
'isFirstLaunch': instance.isFirstLaunch,
@@ -128,4 +139,5 @@ Map<String, dynamic> _$AppSettingsToJson(
'lyricsIncludeRomanizationNetease': instance.lyricsIncludeRomanizationNetease,
'lyricsMultiPersonWordByWord': instance.lyricsMultiPersonWordByWord,
'musixmatchLanguage': instance.musixmatchLanguage,
'lastSeenVersion': instance.lastSeenVersion,
};
+4
View File
@@ -9,6 +9,8 @@ class Track {
final String artistName;
final String albumName;
final String? albumArtist;
final String? artistId;
final String? albumId;
final String? coverUrl;
final String? isrc;
final int duration;
@@ -27,6 +29,8 @@ class Track {
required this.artistName,
required this.albumName,
this.albumArtist,
this.artistId,
this.albumId,
this.coverUrl,
this.isrc,
required this.duration,
+4
View File
@@ -12,6 +12,8 @@ Track _$TrackFromJson(Map<String, dynamic> json) => Track(
artistName: json['artistName'] as String,
albumName: json['albumName'] as String,
albumArtist: json['albumArtist'] as String?,
artistId: json['artistId'] as String?,
albumId: json['albumId'] as String?,
coverUrl: json['coverUrl'] as String?,
isrc: json['isrc'] as String?,
duration: (json['duration'] as num).toInt(),
@@ -35,6 +37,8 @@ Map<String, dynamic> _$TrackToJson(Track instance) => <String, dynamic>{
'artistName': instance.artistName,
'albumName': instance.albumName,
'albumArtist': instance.albumArtist,
'artistId': instance.artistId,
'albumId': instance.albumId,
'coverUrl': instance.coverUrl,
'isrc': instance.isrc,
'duration': instance.duration,