Files
SpotiFLAC-Mobile/lib/models/settings.dart
T
zarzet a549671c4e feat(metadata): add a primary artist tag mode
Artist Tag Mode gains "Primary artist only": downloads and re-enrichment write only the first credited artist to Artist and Album Artist, so players list one artist. The FFmpeg MP3, M4A and Vorbis embeds and the AC-4 path now honor the mode; MP3 and M4A previously ignored it.

Manual metadata edits, lyrics embedding and format conversion keep existing or typed credits joined instead of dropping names. The picker scrolls so three options fit on small and landscape screens. Closes #618.
2026-10-01 16:12:58 +07:00

439 lines
18 KiB
Dart

import 'package:json_annotation/json_annotation.dart';
import 'package:spotiflac_android/utils/artist_utils.dart';
part 'settings.g.dart';
@JsonSerializable()
class AppSettings {
static const String homeFeedProviderOff = '__off__';
static const String libraryQualityLabelBitrate = 'bitrate';
static const String libraryQualityLabelFileFormat = 'file_format';
static const String libraryQualityLabelBitDepthOnly = 'bit_depth_only';
static const String libraryQualityLabelBitDepth = 'bit_depth';
static const String libraryQualityLabelBitDepthBitrate = 'bit_depth_bitrate';
final String defaultService;
final String audioQuality;
final String filenameFormat;
final String downloadDirectory;
final String downloadDirectoryBookmark;
final String storageMode; // 'app' or 'saf'
final String downloadTreeUri; // SAF persistable tree URI
final bool autoFallback;
final bool embedMetadata;
/// Maximum width or height of remotely fetched artwork before embedding.
/// Zero preserves the provider's original image.
final int embeddedCoverMaxDimension;
final String
artistTagMode; // 'joined', 'split_vorbis' (Vorbis formats), or 'primary'
final bool embedLyrics;
final bool embedReplayGain;
// Apply ReplayGain/R128 tags as volume normalization in the built-in player.
final bool playbackNormalization;
final bool autoMix;
final bool usbBitPerfect;
final bool usbDirect;
final bool usbDsdOverPcm;
final bool usbAllowFixedVolume;
final bool dapExclusive;
final bool autoplay;
final bool isFirstLaunch;
final bool checkForUpdates;
final String updateChannel;
final bool hasSearchedBefore;
final String folderOrganization;
final bool createPlaylistFolder;
final bool useAlbumArtistForFolders;
final bool usePrimaryArtistOnly; // Strip featured artists from folder name
final bool filterContributingArtistsInAlbumArtist;
final String historyViewMode;
final String historyFilterMode;
/// Library view opened when switching to the Library tab:
/// 'last' (keep last used), 'all', 'albums', 'singles', or 'playlists'.
final String defaultLibraryView;
/// Library badge text: measured bitrate, file format, bit depth,
/// bit depth/sample rate, or the combined bit depth/bitrate label.
final String libraryQualityLabelMode;
final bool askQualityBeforeDownload;
final bool enableLogging;
final bool useExtensionProviders;
final List<String>? downloadFallbackExtensionIds;
final String? searchProvider;
final String defaultSearchTab;
final String? homeFeedProvider;
final bool separateSingles;
final String singleFilenameFormat;
final String albumFolderStructure;
final bool showExtensionStore;
/// Shared-element (Hero) flights, e.g. the mini player artwork expanding
/// into the full player. Off skips the flights entirely.
final bool heroAnimationsEnabled;
/// Forces the shell's backdrop blur on even when the startup runtime
/// profile disabled it for this device tier. Off means "follow the
/// device default".
final bool forceBackdropBlur;
final String
extensionVerificationBrowserMode; // 'external_first' or 'in_app_first'
final String locale;
final String lyricsMode;
final bool keepScreenOnLyrics;
final bool autoConvertDownloads;
// Re-download a Hi-Res request at LOSSLESS when the file measures as fake.
final bool redownloadFakeHiRes;
final String autoConvertFormat; // 'mp3', 'aac' (M4A), or 'opus'
final String autoConvertBitrate; // '128k', '192k', '256k', or '320k'
final bool
useAllFilesAccess; // Android 13+ only: enable MANAGE_EXTERNAL_STORAGE
final bool autoExportFailedDownloads;
final String
downloadNetworkMode; // 'any' = WiFi + Mobile, 'wifi_only' = WiFi only
final bool
networkCompatibilityMode; // Try HTTP + allow invalid TLS cert for API requests
final bool
allowLocalNetwork; // Allow requests to private/local network targets (local proxy / custom DNS)
final String
songLinkRegion; // SongLink userCountry region code used for platform lookup
final bool nativeDownloadWorkerEnabled; // Android service-owned worker
final int
concurrentDownloads; // Max simultaneous downloads in the Dart queue (1-3)
final bool localLibraryEnabled;
final String localLibraryPath;
final String
localLibraryBookmark; // Base64-encoded iOS security-scoped bookmark
final bool localLibraryShowDuplicates;
final String
localLibraryAutoScan; // Auto-scan mode: 'off', 'on_open', 'daily', 'weekly'
final bool hasCompletedTutorial;
final List<String> lyricsProviders;
final bool
lyricsIncludeTranslationNetease; // Append translated lyrics (Netease)
final bool
lyricsIncludeRomanizationNetease; // Append romanized lyrics (Netease)
final bool
lyricsMultiPersonWordByWord; // Enable v1/v2 + [bg:] tags for Apple/QQ syllable lyrics
final bool
lyricsAppleElrcWordSync; // Preserve Apple Music inline word timestamps for eLRC-capable players
final String
musixmatchLanguage; // Optional ISO language code for Musixmatch localized lyrics
final String
lastSeenVersion; // Last app version the user has acknowledged (e.g. '3.7.0')
final bool deduplicateDownloads;
final bool allowQualityVariants;
final bool saveDownloadHistory;
final String playerMode;
final bool playerShowPronunciation;
final bool playerShowTranslation;
const AppSettings({
this.defaultService = '',
this.audioQuality = 'LOSSLESS',
this.filenameFormat = '{title} - {artist}',
this.downloadDirectory = '',
this.downloadDirectoryBookmark = '',
this.storageMode = 'app',
this.downloadTreeUri = '',
this.autoFallback = true,
this.embedMetadata = true,
this.embeddedCoverMaxDimension = 0,
this.artistTagMode = artistTagModeJoined,
this.embedLyrics = true,
this.embedReplayGain = false,
this.playbackNormalization = false,
this.autoMix = false,
this.usbBitPerfect = false,
this.usbDirect = false,
this.usbDsdOverPcm = false,
this.usbAllowFixedVolume = false,
this.dapExclusive = false,
this.autoplay = false,
this.isFirstLaunch = true,
this.checkForUpdates = true,
this.updateChannel = 'stable',
this.hasSearchedBefore = false,
this.folderOrganization = 'none',
this.createPlaylistFolder = false,
this.useAlbumArtistForFolders = true,
this.usePrimaryArtistOnly = false,
this.filterContributingArtistsInAlbumArtist = false,
this.historyViewMode = 'grid',
this.historyFilterMode = 'all',
this.defaultLibraryView = 'last',
this.libraryQualityLabelMode = AppSettings.libraryQualityLabelBitrate,
this.askQualityBeforeDownload = true,
this.enableLogging = false,
this.useExtensionProviders = true,
this.downloadFallbackExtensionIds,
this.searchProvider,
this.defaultSearchTab = 'all',
this.homeFeedProvider,
this.separateSingles = false,
this.singleFilenameFormat = '{title} - {artist}',
this.albumFolderStructure = 'artist_album',
this.showExtensionStore = true,
this.heroAnimationsEnabled = true,
this.forceBackdropBlur = false,
this.extensionVerificationBrowserMode = 'in_app_first',
this.locale = 'system',
this.lyricsMode = 'embed',
this.keepScreenOnLyrics = true,
this.autoConvertDownloads = false,
this.redownloadFakeHiRes = false,
this.autoConvertFormat = 'mp3',
this.autoConvertBitrate = '320k',
this.useAllFilesAccess = false,
this.autoExportFailedDownloads = false,
this.downloadNetworkMode = 'any',
this.networkCompatibilityMode = false,
this.allowLocalNetwork = false,
this.songLinkRegion = 'US',
this.nativeDownloadWorkerEnabled = false,
this.concurrentDownloads = 1,
this.localLibraryEnabled = false,
this.localLibraryPath = '',
this.localLibraryBookmark = '',
this.localLibraryShowDuplicates = true,
this.localLibraryAutoScan = 'off',
this.hasCompletedTutorial = false,
this.lyricsProviders = const ['lrclib', 'apple_music'],
this.lyricsIncludeTranslationNetease = false,
this.lyricsIncludeRomanizationNetease = false,
this.lyricsMultiPersonWordByWord = false,
this.lyricsAppleElrcWordSync = false,
this.musixmatchLanguage = '',
this.lastSeenVersion = '',
this.deduplicateDownloads = true,
this.allowQualityVariants = false,
this.saveDownloadHistory = true,
this.playerMode = 'external',
this.playerShowPronunciation = true,
this.playerShowTranslation = true,
});
@JsonKey(includeFromJson: false, includeToJson: false)
Map<String, dynamic> get lyricsFetchOptions => {
'include_translation_netease': lyricsIncludeTranslationNetease,
'include_romanization_netease': lyricsIncludeRomanizationNetease,
'multi_person_word_by_word': lyricsMultiPersonWordByWord,
'apple_elrc_word_sync': lyricsAppleElrcWordSync,
'musixmatch_language': musixmatchLanguage,
};
AppSettings copyWith({
String? defaultService,
String? audioQuality,
String? filenameFormat,
String? downloadDirectory,
String? downloadDirectoryBookmark,
String? storageMode,
String? downloadTreeUri,
bool? autoFallback,
bool? embedMetadata,
int? embeddedCoverMaxDimension,
String? artistTagMode,
bool? embedLyrics,
bool? embedReplayGain,
bool? playbackNormalization,
bool? autoMix,
bool? usbBitPerfect,
bool? usbDirect,
bool? usbDsdOverPcm,
bool? usbAllowFixedVolume,
bool? dapExclusive,
bool? autoplay,
bool? isFirstLaunch,
bool? checkForUpdates,
String? updateChannel,
bool? hasSearchedBefore,
String? folderOrganization,
bool? createPlaylistFolder,
bool? useAlbumArtistForFolders,
bool? usePrimaryArtistOnly,
bool? filterContributingArtistsInAlbumArtist,
String? historyViewMode,
String? historyFilterMode,
String? defaultLibraryView,
String? libraryQualityLabelMode,
bool? askQualityBeforeDownload,
bool? enableLogging,
bool? useExtensionProviders,
List<String>? downloadFallbackExtensionIds,
bool clearDownloadFallbackExtensionIds = false,
String? searchProvider,
bool clearSearchProvider = false,
String? defaultSearchTab,
String? homeFeedProvider,
bool clearHomeFeedProvider = false,
bool? separateSingles,
String? singleFilenameFormat,
String? albumFolderStructure,
bool? showExtensionStore,
bool? heroAnimationsEnabled,
bool? forceBackdropBlur,
String? extensionVerificationBrowserMode,
String? locale,
String? lyricsMode,
bool? keepScreenOnLyrics,
bool? autoConvertDownloads,
bool? redownloadFakeHiRes,
String? autoConvertFormat,
String? autoConvertBitrate,
bool? useAllFilesAccess,
bool? autoExportFailedDownloads,
String? downloadNetworkMode,
bool? networkCompatibilityMode,
bool? allowLocalNetwork,
String? songLinkRegion,
bool? nativeDownloadWorkerEnabled,
int? concurrentDownloads,
bool? localLibraryEnabled,
String? localLibraryPath,
String? localLibraryBookmark,
bool? localLibraryShowDuplicates,
String? localLibraryAutoScan,
bool? hasCompletedTutorial,
List<String>? lyricsProviders,
bool? lyricsIncludeTranslationNetease,
bool? lyricsIncludeRomanizationNetease,
bool? lyricsMultiPersonWordByWord,
bool? lyricsAppleElrcWordSync,
String? musixmatchLanguage,
String? lastSeenVersion,
bool? deduplicateDownloads,
bool? allowQualityVariants,
bool? saveDownloadHistory,
String? playerMode,
bool? playerShowPronunciation,
bool? playerShowTranslation,
}) {
return AppSettings(
defaultService: defaultService ?? this.defaultService,
audioQuality: audioQuality ?? this.audioQuality,
filenameFormat: filenameFormat ?? this.filenameFormat,
downloadDirectory: downloadDirectory ?? this.downloadDirectory,
downloadDirectoryBookmark:
downloadDirectoryBookmark ?? this.downloadDirectoryBookmark,
storageMode: storageMode ?? this.storageMode,
downloadTreeUri: downloadTreeUri ?? this.downloadTreeUri,
autoFallback: autoFallback ?? this.autoFallback,
embedMetadata: embedMetadata ?? this.embedMetadata,
embeddedCoverMaxDimension:
embeddedCoverMaxDimension ?? this.embeddedCoverMaxDimension,
artistTagMode: artistTagMode ?? this.artistTagMode,
embedLyrics: embedLyrics ?? this.embedLyrics,
embedReplayGain: embedReplayGain ?? this.embedReplayGain,
playbackNormalization:
playbackNormalization ?? this.playbackNormalization,
autoMix: autoMix ?? this.autoMix,
usbBitPerfect: usbBitPerfect ?? this.usbBitPerfect,
usbDirect: usbDirect ?? this.usbDirect,
usbDsdOverPcm: usbDsdOverPcm ?? this.usbDsdOverPcm,
usbAllowFixedVolume: usbAllowFixedVolume ?? this.usbAllowFixedVolume,
dapExclusive: dapExclusive ?? this.dapExclusive,
autoplay: autoplay ?? this.autoplay,
isFirstLaunch: isFirstLaunch ?? this.isFirstLaunch,
checkForUpdates: checkForUpdates ?? this.checkForUpdates,
updateChannel: updateChannel ?? this.updateChannel,
hasSearchedBefore: hasSearchedBefore ?? this.hasSearchedBefore,
folderOrganization: folderOrganization ?? this.folderOrganization,
createPlaylistFolder: createPlaylistFolder ?? this.createPlaylistFolder,
useAlbumArtistForFolders:
useAlbumArtistForFolders ?? this.useAlbumArtistForFolders,
usePrimaryArtistOnly: usePrimaryArtistOnly ?? this.usePrimaryArtistOnly,
filterContributingArtistsInAlbumArtist:
filterContributingArtistsInAlbumArtist ??
this.filterContributingArtistsInAlbumArtist,
historyViewMode: historyViewMode ?? this.historyViewMode,
historyFilterMode: historyFilterMode ?? this.historyFilterMode,
defaultLibraryView: defaultLibraryView ?? this.defaultLibraryView,
libraryQualityLabelMode:
libraryQualityLabelMode ?? this.libraryQualityLabelMode,
askQualityBeforeDownload:
askQualityBeforeDownload ?? this.askQualityBeforeDownload,
enableLogging: enableLogging ?? this.enableLogging,
useExtensionProviders:
useExtensionProviders ?? this.useExtensionProviders,
downloadFallbackExtensionIds: clearDownloadFallbackExtensionIds
? null
: (downloadFallbackExtensionIds ?? this.downloadFallbackExtensionIds),
searchProvider: clearSearchProvider
? null
: (searchProvider ?? this.searchProvider),
defaultSearchTab: defaultSearchTab ?? this.defaultSearchTab,
homeFeedProvider: clearHomeFeedProvider
? null
: (homeFeedProvider ?? this.homeFeedProvider),
separateSingles: separateSingles ?? this.separateSingles,
singleFilenameFormat: singleFilenameFormat ?? this.singleFilenameFormat,
albumFolderStructure: albumFolderStructure ?? this.albumFolderStructure,
showExtensionStore: showExtensionStore ?? this.showExtensionStore,
heroAnimationsEnabled:
heroAnimationsEnabled ?? this.heroAnimationsEnabled,
forceBackdropBlur: forceBackdropBlur ?? this.forceBackdropBlur,
extensionVerificationBrowserMode:
extensionVerificationBrowserMode ??
this.extensionVerificationBrowserMode,
locale: locale ?? this.locale,
lyricsMode: lyricsMode ?? this.lyricsMode,
keepScreenOnLyrics: keepScreenOnLyrics ?? this.keepScreenOnLyrics,
autoConvertDownloads: autoConvertDownloads ?? this.autoConvertDownloads,
redownloadFakeHiRes: redownloadFakeHiRes ?? this.redownloadFakeHiRes,
autoConvertFormat: autoConvertFormat ?? this.autoConvertFormat,
autoConvertBitrate: autoConvertBitrate ?? this.autoConvertBitrate,
useAllFilesAccess: useAllFilesAccess ?? this.useAllFilesAccess,
autoExportFailedDownloads:
autoExportFailedDownloads ?? this.autoExportFailedDownloads,
downloadNetworkMode: downloadNetworkMode ?? this.downloadNetworkMode,
networkCompatibilityMode:
networkCompatibilityMode ?? this.networkCompatibilityMode,
allowLocalNetwork: allowLocalNetwork ?? this.allowLocalNetwork,
songLinkRegion: songLinkRegion ?? this.songLinkRegion,
nativeDownloadWorkerEnabled:
nativeDownloadWorkerEnabled ?? this.nativeDownloadWorkerEnabled,
concurrentDownloads: concurrentDownloads ?? this.concurrentDownloads,
localLibraryEnabled: localLibraryEnabled ?? this.localLibraryEnabled,
localLibraryPath: localLibraryPath ?? this.localLibraryPath,
localLibraryBookmark: localLibraryBookmark ?? this.localLibraryBookmark,
localLibraryShowDuplicates:
localLibraryShowDuplicates ?? this.localLibraryShowDuplicates,
localLibraryAutoScan: localLibraryAutoScan ?? this.localLibraryAutoScan,
hasCompletedTutorial: hasCompletedTutorial ?? this.hasCompletedTutorial,
lyricsProviders: lyricsProviders ?? this.lyricsProviders,
lyricsIncludeTranslationNetease:
lyricsIncludeTranslationNetease ??
this.lyricsIncludeTranslationNetease,
lyricsIncludeRomanizationNetease:
lyricsIncludeRomanizationNetease ??
this.lyricsIncludeRomanizationNetease,
lyricsMultiPersonWordByWord:
lyricsMultiPersonWordByWord ?? this.lyricsMultiPersonWordByWord,
lyricsAppleElrcWordSync:
lyricsAppleElrcWordSync ?? this.lyricsAppleElrcWordSync,
musixmatchLanguage: musixmatchLanguage ?? this.musixmatchLanguage,
lastSeenVersion: lastSeenVersion ?? this.lastSeenVersion,
deduplicateDownloads: deduplicateDownloads ?? this.deduplicateDownloads,
allowQualityVariants: allowQualityVariants ?? this.allowQualityVariants,
saveDownloadHistory: saveDownloadHistory ?? this.saveDownloadHistory,
playerMode: playerMode ?? this.playerMode,
playerShowPronunciation:
playerShowPronunciation ?? this.playerShowPronunciation,
playerShowTranslation:
playerShowTranslation ?? this.playerShowTranslation,
);
}
factory AppSettings.fromJson(Map<String, dynamic> json) =>
_$AppSettingsFromJson(json);
Map<String, dynamic> toJson() => _$AppSettingsToJson(this);
}