feat(settings): add backup and restore for settings, history and library

Add a Backup & Restore page that exports app settings, download history, liked tracks, wishlist, playlists (with cover images) and favorite artists into a single JSON file, and restores them on another device. Settings restore preserves device-specific storage location (SAF tree URI, download dir). Includes EN strings and ID translations.
This commit is contained in:
zarzet
2026-06-26 19:54:20 +07:00
parent f236d72a19
commit b2705cb2ae
25 changed files with 3068 additions and 0 deletions
+168
View File
@@ -4993,6 +4993,174 @@ abstract class AppLocalizations {
/// **'Buy the developer a coffee'**
String get settingsDonateSubtitle;
/// Settings menu item - backup and restore page
///
/// In en, this message translates to:
/// **'Backup & Restore'**
String get settingsBackup;
/// Subtitle for backup and restore settings item
///
/// In en, this message translates to:
/// **'Move your library, history and settings to a new device'**
String get settingsBackupSubtitle;
/// App bar title for the backup and restore page
///
/// In en, this message translates to:
/// **'Backup & Restore'**
String get backupTitle;
/// Section title for the export/backup card
///
/// In en, this message translates to:
/// **'Create backup'**
String get backupExportSectionTitle;
/// Description of what a backup contains
///
/// In en, this message translates to:
/// **'Save your settings, download history, liked tracks, wishlist, favorite artists and playlists into a single file you can keep or move to another phone.'**
String get backupExportSectionDescription;
/// Button to create and share a backup file
///
/// In en, this message translates to:
/// **'Create backup file'**
String get backupExportButton;
/// Section title for the import/restore card
///
/// In en, this message translates to:
/// **'Restore backup'**
String get backupImportSectionTitle;
/// Description for the restore action
///
/// In en, this message translates to:
/// **'Pick a backup file to restore your data. This replaces the current settings, history and library on this device.'**
String get backupImportSectionDescription;
/// Button to pick a backup file to restore
///
/// In en, this message translates to:
/// **'Choose backup file'**
String get backupImportButton;
/// Progress text while a backup is being created
///
/// In en, this message translates to:
/// **'Creating backup...'**
String get backupCreating;
/// Snackbar after a backup file is created
///
/// In en, this message translates to:
/// **'Backup created'**
String get backupCreated;
/// Snackbar when backup creation fails
///
/// In en, this message translates to:
/// **'Failed to create backup'**
String get backupCreateFailed;
/// Snackbar when there is no data to back up
///
/// In en, this message translates to:
/// **'There is nothing to back up yet'**
String get backupEmpty;
/// Confirmation dialog title before restoring a backup
///
/// In en, this message translates to:
/// **'Restore this backup?'**
String get backupRestoreConfirmTitle;
/// Confirmation dialog message before restoring a backup
///
/// In en, this message translates to:
/// **'This will replace your current settings, download history, liked tracks, wishlist and playlists with the contents of the backup. This cannot be undone.'**
String get backupRestoreConfirmMessage;
/// Confirm button to proceed with restore
///
/// In en, this message translates to:
/// **'Restore'**
String get backupRestoreConfirmButton;
/// Progress text while restoring a backup
///
/// In en, this message translates to:
/// **'Restoring backup...'**
String get backupRestoring;
/// Snackbar after a successful restore
///
/// In en, this message translates to:
/// **'Backup restored successfully'**
String get backupRestored;
/// Snackbar when restore fails
///
/// In en, this message translates to:
/// **'Failed to restore backup'**
String get backupRestoreFailed;
/// Snackbar when the chosen file is not a valid backup
///
/// In en, this message translates to:
/// **'This file is not a valid SpotiFLAC backup'**
String get backupInvalidFile;
/// Hint shown after restoring that an app restart is recommended
///
/// In en, this message translates to:
/// **'Restart the app to make sure every change is applied.'**
String get backupRestoreRestartHint;
/// Header above the list summarizing what the backup contains
///
/// In en, this message translates to:
/// **'Backup contents'**
String get backupContentsTitle;
/// Backup contents row label for settings
///
/// In en, this message translates to:
/// **'App settings'**
String get backupContentsSettings;
/// Backup contents row for history count
///
/// In en, this message translates to:
/// **'{count} history {count, plural, =1{item} other{items}}'**
String backupContentsHistory(int count);
/// Backup contents row for liked tracks count
///
/// In en, this message translates to:
/// **'{count} liked {count, plural, =1{track} other{tracks}}'**
String backupContentsLiked(int count);
/// Backup contents row for wishlist tracks count
///
/// In en, this message translates to:
/// **'{count} wishlist {count, plural, =1{track} other{tracks}}'**
String backupContentsWishlist(int count);
/// Backup contents row for playlist count
///
/// In en, this message translates to:
/// **'{count, plural, =1{1 playlist} other{{count} playlists}}'**
String backupContentsPlaylists(int count);
/// Backup contents row for favorite artists count
///
/// In en, this message translates to:
/// **'{count, plural, =1{1 favorite artist} other{{count} favorite artists}}'**
String backupContentsArtists(int count);
/// Tooltip for the Love All button on album/playlist screens
///
/// In en, this message translates to:
+129
View File
@@ -2885,6 +2885,135 @@ class AppLocalizationsAr extends AppLocalizations {
@override
String get settingsDonateSubtitle => 'Buy the developer a coffee';
@override
String get settingsBackup => 'Backup & Restore';
@override
String get settingsBackupSubtitle =>
'Move your library, history and settings to a new device';
@override
String get backupTitle => 'Backup & Restore';
@override
String get backupExportSectionTitle => 'Create backup';
@override
String get backupExportSectionDescription =>
'Save your settings, download history, liked tracks, wishlist, favorite artists and playlists into a single file you can keep or move to another phone.';
@override
String get backupExportButton => 'Create backup file';
@override
String get backupImportSectionTitle => 'Restore backup';
@override
String get backupImportSectionDescription =>
'Pick a backup file to restore your data. This replaces the current settings, history and library on this device.';
@override
String get backupImportButton => 'Choose backup file';
@override
String get backupCreating => 'Creating backup...';
@override
String get backupCreated => 'Backup created';
@override
String get backupCreateFailed => 'Failed to create backup';
@override
String get backupEmpty => 'There is nothing to back up yet';
@override
String get backupRestoreConfirmTitle => 'Restore this backup?';
@override
String get backupRestoreConfirmMessage =>
'This will replace your current settings, download history, liked tracks, wishlist and playlists with the contents of the backup. This cannot be undone.';
@override
String get backupRestoreConfirmButton => 'Restore';
@override
String get backupRestoring => 'Restoring backup...';
@override
String get backupRestored => 'Backup restored successfully';
@override
String get backupRestoreFailed => 'Failed to restore backup';
@override
String get backupInvalidFile => 'This file is not a valid SpotiFLAC backup';
@override
String get backupRestoreRestartHint =>
'Restart the app to make sure every change is applied.';
@override
String get backupContentsTitle => 'Backup contents';
@override
String get backupContentsSettings => 'App settings';
@override
String backupContentsHistory(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'items',
one: 'item',
);
return '$count history $_temp0';
}
@override
String backupContentsLiked(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'tracks',
one: 'track',
);
return '$count liked $_temp0';
}
@override
String backupContentsWishlist(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'tracks',
one: 'track',
);
return '$count wishlist $_temp0';
}
@override
String backupContentsPlaylists(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count playlists',
one: '1 playlist',
);
return '$_temp0';
}
@override
String backupContentsArtists(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count favorite artists',
one: '1 favorite artist',
);
return '$_temp0';
}
@override
String get tooltipLoveAll => 'Love All';
+129
View File
@@ -2922,6 +2922,135 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get settingsDonateSubtitle => 'Kaufe dem Entwickler einen Kaffee';
@override
String get settingsBackup => 'Backup & Restore';
@override
String get settingsBackupSubtitle =>
'Move your library, history and settings to a new device';
@override
String get backupTitle => 'Backup & Restore';
@override
String get backupExportSectionTitle => 'Create backup';
@override
String get backupExportSectionDescription =>
'Save your settings, download history, liked tracks, wishlist, favorite artists and playlists into a single file you can keep or move to another phone.';
@override
String get backupExportButton => 'Create backup file';
@override
String get backupImportSectionTitle => 'Restore backup';
@override
String get backupImportSectionDescription =>
'Pick a backup file to restore your data. This replaces the current settings, history and library on this device.';
@override
String get backupImportButton => 'Choose backup file';
@override
String get backupCreating => 'Creating backup...';
@override
String get backupCreated => 'Backup created';
@override
String get backupCreateFailed => 'Failed to create backup';
@override
String get backupEmpty => 'There is nothing to back up yet';
@override
String get backupRestoreConfirmTitle => 'Restore this backup?';
@override
String get backupRestoreConfirmMessage =>
'This will replace your current settings, download history, liked tracks, wishlist and playlists with the contents of the backup. This cannot be undone.';
@override
String get backupRestoreConfirmButton => 'Restore';
@override
String get backupRestoring => 'Restoring backup...';
@override
String get backupRestored => 'Backup restored successfully';
@override
String get backupRestoreFailed => 'Failed to restore backup';
@override
String get backupInvalidFile => 'This file is not a valid SpotiFLAC backup';
@override
String get backupRestoreRestartHint =>
'Restart the app to make sure every change is applied.';
@override
String get backupContentsTitle => 'Backup contents';
@override
String get backupContentsSettings => 'App settings';
@override
String backupContentsHistory(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'items',
one: 'item',
);
return '$count history $_temp0';
}
@override
String backupContentsLiked(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'tracks',
one: 'track',
);
return '$count liked $_temp0';
}
@override
String backupContentsWishlist(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'tracks',
one: 'track',
);
return '$count wishlist $_temp0';
}
@override
String backupContentsPlaylists(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count playlists',
one: '1 playlist',
);
return '$_temp0';
}
@override
String backupContentsArtists(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count favorite artists',
one: '1 favorite artist',
);
return '$_temp0';
}
@override
String get tooltipLoveAll => 'Alle lieben';
+129
View File
@@ -2885,6 +2885,135 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get settingsDonateSubtitle => 'Buy the developer a coffee';
@override
String get settingsBackup => 'Backup & Restore';
@override
String get settingsBackupSubtitle =>
'Move your library, history and settings to a new device';
@override
String get backupTitle => 'Backup & Restore';
@override
String get backupExportSectionTitle => 'Create backup';
@override
String get backupExportSectionDescription =>
'Save your settings, download history, liked tracks, wishlist, favorite artists and playlists into a single file you can keep or move to another phone.';
@override
String get backupExportButton => 'Create backup file';
@override
String get backupImportSectionTitle => 'Restore backup';
@override
String get backupImportSectionDescription =>
'Pick a backup file to restore your data. This replaces the current settings, history and library on this device.';
@override
String get backupImportButton => 'Choose backup file';
@override
String get backupCreating => 'Creating backup...';
@override
String get backupCreated => 'Backup created';
@override
String get backupCreateFailed => 'Failed to create backup';
@override
String get backupEmpty => 'There is nothing to back up yet';
@override
String get backupRestoreConfirmTitle => 'Restore this backup?';
@override
String get backupRestoreConfirmMessage =>
'This will replace your current settings, download history, liked tracks, wishlist and playlists with the contents of the backup. This cannot be undone.';
@override
String get backupRestoreConfirmButton => 'Restore';
@override
String get backupRestoring => 'Restoring backup...';
@override
String get backupRestored => 'Backup restored successfully';
@override
String get backupRestoreFailed => 'Failed to restore backup';
@override
String get backupInvalidFile => 'This file is not a valid SpotiFLAC backup';
@override
String get backupRestoreRestartHint =>
'Restart the app to make sure every change is applied.';
@override
String get backupContentsTitle => 'Backup contents';
@override
String get backupContentsSettings => 'App settings';
@override
String backupContentsHistory(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'items',
one: 'item',
);
return '$count history $_temp0';
}
@override
String backupContentsLiked(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'tracks',
one: 'track',
);
return '$count liked $_temp0';
}
@override
String backupContentsWishlist(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'tracks',
one: 'track',
);
return '$count wishlist $_temp0';
}
@override
String backupContentsPlaylists(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count playlists',
one: '1 playlist',
);
return '$_temp0';
}
@override
String backupContentsArtists(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count favorite artists',
one: '1 favorite artist',
);
return '$_temp0';
}
@override
String get tooltipLoveAll => 'Love All';
+129
View File
@@ -2885,6 +2885,135 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get settingsDonateSubtitle => 'Buy the developer a coffee';
@override
String get settingsBackup => 'Backup & Restore';
@override
String get settingsBackupSubtitle =>
'Move your library, history and settings to a new device';
@override
String get backupTitle => 'Backup & Restore';
@override
String get backupExportSectionTitle => 'Create backup';
@override
String get backupExportSectionDescription =>
'Save your settings, download history, liked tracks, wishlist, favorite artists and playlists into a single file you can keep or move to another phone.';
@override
String get backupExportButton => 'Create backup file';
@override
String get backupImportSectionTitle => 'Restore backup';
@override
String get backupImportSectionDescription =>
'Pick a backup file to restore your data. This replaces the current settings, history and library on this device.';
@override
String get backupImportButton => 'Choose backup file';
@override
String get backupCreating => 'Creating backup...';
@override
String get backupCreated => 'Backup created';
@override
String get backupCreateFailed => 'Failed to create backup';
@override
String get backupEmpty => 'There is nothing to back up yet';
@override
String get backupRestoreConfirmTitle => 'Restore this backup?';
@override
String get backupRestoreConfirmMessage =>
'This will replace your current settings, download history, liked tracks, wishlist and playlists with the contents of the backup. This cannot be undone.';
@override
String get backupRestoreConfirmButton => 'Restore';
@override
String get backupRestoring => 'Restoring backup...';
@override
String get backupRestored => 'Backup restored successfully';
@override
String get backupRestoreFailed => 'Failed to restore backup';
@override
String get backupInvalidFile => 'This file is not a valid SpotiFLAC backup';
@override
String get backupRestoreRestartHint =>
'Restart the app to make sure every change is applied.';
@override
String get backupContentsTitle => 'Backup contents';
@override
String get backupContentsSettings => 'App settings';
@override
String backupContentsHistory(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'items',
one: 'item',
);
return '$count history $_temp0';
}
@override
String backupContentsLiked(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'tracks',
one: 'track',
);
return '$count liked $_temp0';
}
@override
String backupContentsWishlist(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'tracks',
one: 'track',
);
return '$count wishlist $_temp0';
}
@override
String backupContentsPlaylists(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count playlists',
one: '1 playlist',
);
return '$_temp0';
}
@override
String backupContentsArtists(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count favorite artists',
one: '1 favorite artist',
);
return '$_temp0';
}
@override
String get tooltipLoveAll => 'Love All';
+129
View File
@@ -2962,6 +2962,135 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get settingsDonateSubtitle => 'Offrez un café au développeur';
@override
String get settingsBackup => 'Backup & Restore';
@override
String get settingsBackupSubtitle =>
'Move your library, history and settings to a new device';
@override
String get backupTitle => 'Backup & Restore';
@override
String get backupExportSectionTitle => 'Create backup';
@override
String get backupExportSectionDescription =>
'Save your settings, download history, liked tracks, wishlist, favorite artists and playlists into a single file you can keep or move to another phone.';
@override
String get backupExportButton => 'Create backup file';
@override
String get backupImportSectionTitle => 'Restore backup';
@override
String get backupImportSectionDescription =>
'Pick a backup file to restore your data. This replaces the current settings, history and library on this device.';
@override
String get backupImportButton => 'Choose backup file';
@override
String get backupCreating => 'Creating backup...';
@override
String get backupCreated => 'Backup created';
@override
String get backupCreateFailed => 'Failed to create backup';
@override
String get backupEmpty => 'There is nothing to back up yet';
@override
String get backupRestoreConfirmTitle => 'Restore this backup?';
@override
String get backupRestoreConfirmMessage =>
'This will replace your current settings, download history, liked tracks, wishlist and playlists with the contents of the backup. This cannot be undone.';
@override
String get backupRestoreConfirmButton => 'Restore';
@override
String get backupRestoring => 'Restoring backup...';
@override
String get backupRestored => 'Backup restored successfully';
@override
String get backupRestoreFailed => 'Failed to restore backup';
@override
String get backupInvalidFile => 'This file is not a valid SpotiFLAC backup';
@override
String get backupRestoreRestartHint =>
'Restart the app to make sure every change is applied.';
@override
String get backupContentsTitle => 'Backup contents';
@override
String get backupContentsSettings => 'App settings';
@override
String backupContentsHistory(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'items',
one: 'item',
);
return '$count history $_temp0';
}
@override
String backupContentsLiked(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'tracks',
one: 'track',
);
return '$count liked $_temp0';
}
@override
String backupContentsWishlist(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'tracks',
one: 'track',
);
return '$count wishlist $_temp0';
}
@override
String backupContentsPlaylists(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count playlists',
one: '1 playlist',
);
return '$_temp0';
}
@override
String backupContentsArtists(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count favorite artists',
one: '1 favorite artist',
);
return '$_temp0';
}
@override
String get tooltipLoveAll => 'Tout aimer';
+129
View File
@@ -2885,6 +2885,135 @@ class AppLocalizationsHi extends AppLocalizations {
@override
String get settingsDonateSubtitle => 'Buy the developer a coffee';
@override
String get settingsBackup => 'Backup & Restore';
@override
String get settingsBackupSubtitle =>
'Move your library, history and settings to a new device';
@override
String get backupTitle => 'Backup & Restore';
@override
String get backupExportSectionTitle => 'Create backup';
@override
String get backupExportSectionDescription =>
'Save your settings, download history, liked tracks, wishlist, favorite artists and playlists into a single file you can keep or move to another phone.';
@override
String get backupExportButton => 'Create backup file';
@override
String get backupImportSectionTitle => 'Restore backup';
@override
String get backupImportSectionDescription =>
'Pick a backup file to restore your data. This replaces the current settings, history and library on this device.';
@override
String get backupImportButton => 'Choose backup file';
@override
String get backupCreating => 'Creating backup...';
@override
String get backupCreated => 'Backup created';
@override
String get backupCreateFailed => 'Failed to create backup';
@override
String get backupEmpty => 'There is nothing to back up yet';
@override
String get backupRestoreConfirmTitle => 'Restore this backup?';
@override
String get backupRestoreConfirmMessage =>
'This will replace your current settings, download history, liked tracks, wishlist and playlists with the contents of the backup. This cannot be undone.';
@override
String get backupRestoreConfirmButton => 'Restore';
@override
String get backupRestoring => 'Restoring backup...';
@override
String get backupRestored => 'Backup restored successfully';
@override
String get backupRestoreFailed => 'Failed to restore backup';
@override
String get backupInvalidFile => 'This file is not a valid SpotiFLAC backup';
@override
String get backupRestoreRestartHint =>
'Restart the app to make sure every change is applied.';
@override
String get backupContentsTitle => 'Backup contents';
@override
String get backupContentsSettings => 'App settings';
@override
String backupContentsHistory(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'items',
one: 'item',
);
return '$count history $_temp0';
}
@override
String backupContentsLiked(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'tracks',
one: 'track',
);
return '$count liked $_temp0';
}
@override
String backupContentsWishlist(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'tracks',
one: 'track',
);
return '$count wishlist $_temp0';
}
@override
String backupContentsPlaylists(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count playlists',
one: '1 playlist',
);
return '$_temp0';
}
@override
String backupContentsArtists(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count favorite artists',
one: '1 favorite artist',
);
return '$_temp0';
}
@override
String get tooltipLoveAll => 'Love All';
+112
View File
@@ -2892,6 +2892,118 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get settingsDonateSubtitle => 'Buy the developer a coffee';
@override
String get settingsBackup => 'Cadangkan & Pulihkan';
@override
String get settingsBackupSubtitle =>
'Pindahkan pustaka, riwayat, dan pengaturan ke perangkat baru';
@override
String get backupTitle => 'Cadangkan & Pulihkan';
@override
String get backupExportSectionTitle => 'Buat cadangan';
@override
String get backupExportSectionDescription =>
'Simpan pengaturan, riwayat unduhan, lagu disukai, wishlist, artis favorit, dan playlist ke dalam satu file yang bisa kamu simpan atau pindahkan ke ponsel lain.';
@override
String get backupExportButton => 'Buat file cadangan';
@override
String get backupImportSectionTitle => 'Pulihkan cadangan';
@override
String get backupImportSectionDescription =>
'Pilih file cadangan untuk memulihkan data. Ini akan menggantikan pengaturan, riwayat, dan pustaka di perangkat ini.';
@override
String get backupImportButton => 'Pilih file cadangan';
@override
String get backupCreating => 'Membuat cadangan...';
@override
String get backupCreated => 'Cadangan berhasil dibuat';
@override
String get backupCreateFailed => 'Gagal membuat cadangan';
@override
String get backupEmpty => 'Belum ada data untuk dicadangkan';
@override
String get backupRestoreConfirmTitle => 'Pulihkan cadangan ini?';
@override
String get backupRestoreConfirmMessage =>
'Ini akan menggantikan pengaturan, riwayat unduhan, lagu disukai, wishlist, dan playlist saat ini dengan isi cadangan. Tindakan ini tidak bisa dibatalkan.';
@override
String get backupRestoreConfirmButton => 'Pulihkan';
@override
String get backupRestoring => 'Memulihkan cadangan...';
@override
String get backupRestored => 'Cadangan berhasil dipulihkan';
@override
String get backupRestoreFailed => 'Gagal memulihkan cadangan';
@override
String get backupInvalidFile =>
'File ini bukan cadangan SpotiFLAC yang valid';
@override
String get backupRestoreRestartHint =>
'Mulai ulang aplikasi untuk memastikan semua perubahan diterapkan.';
@override
String get backupContentsTitle => 'Isi cadangan';
@override
String get backupContentsSettings => 'Pengaturan aplikasi';
@override
String backupContentsHistory(int count) {
return '$count item riwayat';
}
@override
String backupContentsLiked(int count) {
return '$count lagu disukai';
}
@override
String backupContentsWishlist(int count) {
return '$count lagu di wishlist';
}
@override
String backupContentsPlaylists(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count playlist',
one: '1 playlist',
);
return '$_temp0';
}
@override
String backupContentsArtists(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count artis favorit',
one: '1 artis favorit',
);
return '$_temp0';
}
@override
String get tooltipLoveAll => 'Love All';
+129
View File
@@ -2873,6 +2873,135 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get settingsDonateSubtitle => 'Buy the developer a coffee';
@override
String get settingsBackup => 'Backup & Restore';
@override
String get settingsBackupSubtitle =>
'Move your library, history and settings to a new device';
@override
String get backupTitle => 'Backup & Restore';
@override
String get backupExportSectionTitle => 'Create backup';
@override
String get backupExportSectionDescription =>
'Save your settings, download history, liked tracks, wishlist, favorite artists and playlists into a single file you can keep or move to another phone.';
@override
String get backupExportButton => 'Create backup file';
@override
String get backupImportSectionTitle => 'Restore backup';
@override
String get backupImportSectionDescription =>
'Pick a backup file to restore your data. This replaces the current settings, history and library on this device.';
@override
String get backupImportButton => 'Choose backup file';
@override
String get backupCreating => 'Creating backup...';
@override
String get backupCreated => 'Backup created';
@override
String get backupCreateFailed => 'Failed to create backup';
@override
String get backupEmpty => 'There is nothing to back up yet';
@override
String get backupRestoreConfirmTitle => 'Restore this backup?';
@override
String get backupRestoreConfirmMessage =>
'This will replace your current settings, download history, liked tracks, wishlist and playlists with the contents of the backup. This cannot be undone.';
@override
String get backupRestoreConfirmButton => 'Restore';
@override
String get backupRestoring => 'Restoring backup...';
@override
String get backupRestored => 'Backup restored successfully';
@override
String get backupRestoreFailed => 'Failed to restore backup';
@override
String get backupInvalidFile => 'This file is not a valid SpotiFLAC backup';
@override
String get backupRestoreRestartHint =>
'Restart the app to make sure every change is applied.';
@override
String get backupContentsTitle => 'Backup contents';
@override
String get backupContentsSettings => 'App settings';
@override
String backupContentsHistory(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'items',
one: 'item',
);
return '$count history $_temp0';
}
@override
String backupContentsLiked(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'tracks',
one: 'track',
);
return '$count liked $_temp0';
}
@override
String backupContentsWishlist(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'tracks',
one: 'track',
);
return '$count wishlist $_temp0';
}
@override
String backupContentsPlaylists(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count playlists',
one: '1 playlist',
);
return '$_temp0';
}
@override
String backupContentsArtists(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count favorite artists',
one: '1 favorite artist',
);
return '$_temp0';
}
@override
String get tooltipLoveAll => 'Love All';
+129
View File
@@ -2870,6 +2870,135 @@ class AppLocalizationsKo extends AppLocalizations {
@override
String get settingsDonateSubtitle => 'Buy the developer a coffee';
@override
String get settingsBackup => 'Backup & Restore';
@override
String get settingsBackupSubtitle =>
'Move your library, history and settings to a new device';
@override
String get backupTitle => 'Backup & Restore';
@override
String get backupExportSectionTitle => 'Create backup';
@override
String get backupExportSectionDescription =>
'Save your settings, download history, liked tracks, wishlist, favorite artists and playlists into a single file you can keep or move to another phone.';
@override
String get backupExportButton => 'Create backup file';
@override
String get backupImportSectionTitle => 'Restore backup';
@override
String get backupImportSectionDescription =>
'Pick a backup file to restore your data. This replaces the current settings, history and library on this device.';
@override
String get backupImportButton => 'Choose backup file';
@override
String get backupCreating => 'Creating backup...';
@override
String get backupCreated => 'Backup created';
@override
String get backupCreateFailed => 'Failed to create backup';
@override
String get backupEmpty => 'There is nothing to back up yet';
@override
String get backupRestoreConfirmTitle => 'Restore this backup?';
@override
String get backupRestoreConfirmMessage =>
'This will replace your current settings, download history, liked tracks, wishlist and playlists with the contents of the backup. This cannot be undone.';
@override
String get backupRestoreConfirmButton => 'Restore';
@override
String get backupRestoring => 'Restoring backup...';
@override
String get backupRestored => 'Backup restored successfully';
@override
String get backupRestoreFailed => 'Failed to restore backup';
@override
String get backupInvalidFile => 'This file is not a valid SpotiFLAC backup';
@override
String get backupRestoreRestartHint =>
'Restart the app to make sure every change is applied.';
@override
String get backupContentsTitle => 'Backup contents';
@override
String get backupContentsSettings => 'App settings';
@override
String backupContentsHistory(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'items',
one: 'item',
);
return '$count history $_temp0';
}
@override
String backupContentsLiked(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'tracks',
one: 'track',
);
return '$count liked $_temp0';
}
@override
String backupContentsWishlist(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'tracks',
one: 'track',
);
return '$count wishlist $_temp0';
}
@override
String backupContentsPlaylists(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count playlists',
one: '1 playlist',
);
return '$_temp0';
}
@override
String backupContentsArtists(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count favorite artists',
one: '1 favorite artist',
);
return '$_temp0';
}
@override
String get tooltipLoveAll => 'Love All';
+129
View File
@@ -2885,6 +2885,135 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get settingsDonateSubtitle => 'Buy the developer a coffee';
@override
String get settingsBackup => 'Backup & Restore';
@override
String get settingsBackupSubtitle =>
'Move your library, history and settings to a new device';
@override
String get backupTitle => 'Backup & Restore';
@override
String get backupExportSectionTitle => 'Create backup';
@override
String get backupExportSectionDescription =>
'Save your settings, download history, liked tracks, wishlist, favorite artists and playlists into a single file you can keep or move to another phone.';
@override
String get backupExportButton => 'Create backup file';
@override
String get backupImportSectionTitle => 'Restore backup';
@override
String get backupImportSectionDescription =>
'Pick a backup file to restore your data. This replaces the current settings, history and library on this device.';
@override
String get backupImportButton => 'Choose backup file';
@override
String get backupCreating => 'Creating backup...';
@override
String get backupCreated => 'Backup created';
@override
String get backupCreateFailed => 'Failed to create backup';
@override
String get backupEmpty => 'There is nothing to back up yet';
@override
String get backupRestoreConfirmTitle => 'Restore this backup?';
@override
String get backupRestoreConfirmMessage =>
'This will replace your current settings, download history, liked tracks, wishlist and playlists with the contents of the backup. This cannot be undone.';
@override
String get backupRestoreConfirmButton => 'Restore';
@override
String get backupRestoring => 'Restoring backup...';
@override
String get backupRestored => 'Backup restored successfully';
@override
String get backupRestoreFailed => 'Failed to restore backup';
@override
String get backupInvalidFile => 'This file is not a valid SpotiFLAC backup';
@override
String get backupRestoreRestartHint =>
'Restart the app to make sure every change is applied.';
@override
String get backupContentsTitle => 'Backup contents';
@override
String get backupContentsSettings => 'App settings';
@override
String backupContentsHistory(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'items',
one: 'item',
);
return '$count history $_temp0';
}
@override
String backupContentsLiked(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'tracks',
one: 'track',
);
return '$count liked $_temp0';
}
@override
String backupContentsWishlist(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'tracks',
one: 'track',
);
return '$count wishlist $_temp0';
}
@override
String backupContentsPlaylists(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count playlists',
one: '1 playlist',
);
return '$_temp0';
}
@override
String backupContentsArtists(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count favorite artists',
one: '1 favorite artist',
);
return '$_temp0';
}
@override
String get tooltipLoveAll => 'Love All';
+129
View File
@@ -2885,6 +2885,135 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get settingsDonateSubtitle => 'Buy the developer a coffee';
@override
String get settingsBackup => 'Backup & Restore';
@override
String get settingsBackupSubtitle =>
'Move your library, history and settings to a new device';
@override
String get backupTitle => 'Backup & Restore';
@override
String get backupExportSectionTitle => 'Create backup';
@override
String get backupExportSectionDescription =>
'Save your settings, download history, liked tracks, wishlist, favorite artists and playlists into a single file you can keep or move to another phone.';
@override
String get backupExportButton => 'Create backup file';
@override
String get backupImportSectionTitle => 'Restore backup';
@override
String get backupImportSectionDescription =>
'Pick a backup file to restore your data. This replaces the current settings, history and library on this device.';
@override
String get backupImportButton => 'Choose backup file';
@override
String get backupCreating => 'Creating backup...';
@override
String get backupCreated => 'Backup created';
@override
String get backupCreateFailed => 'Failed to create backup';
@override
String get backupEmpty => 'There is nothing to back up yet';
@override
String get backupRestoreConfirmTitle => 'Restore this backup?';
@override
String get backupRestoreConfirmMessage =>
'This will replace your current settings, download history, liked tracks, wishlist and playlists with the contents of the backup. This cannot be undone.';
@override
String get backupRestoreConfirmButton => 'Restore';
@override
String get backupRestoring => 'Restoring backup...';
@override
String get backupRestored => 'Backup restored successfully';
@override
String get backupRestoreFailed => 'Failed to restore backup';
@override
String get backupInvalidFile => 'This file is not a valid SpotiFLAC backup';
@override
String get backupRestoreRestartHint =>
'Restart the app to make sure every change is applied.';
@override
String get backupContentsTitle => 'Backup contents';
@override
String get backupContentsSettings => 'App settings';
@override
String backupContentsHistory(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'items',
one: 'item',
);
return '$count history $_temp0';
}
@override
String backupContentsLiked(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'tracks',
one: 'track',
);
return '$count liked $_temp0';
}
@override
String backupContentsWishlist(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'tracks',
one: 'track',
);
return '$count wishlist $_temp0';
}
@override
String backupContentsPlaylists(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count playlists',
one: '1 playlist',
);
return '$_temp0';
}
@override
String backupContentsArtists(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count favorite artists',
one: '1 favorite artist',
);
return '$_temp0';
}
@override
String get tooltipLoveAll => 'Love All';
+129
View File
@@ -2940,6 +2940,135 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get settingsDonateSubtitle => 'Buy the developer a coffee';
@override
String get settingsBackup => 'Backup & Restore';
@override
String get settingsBackupSubtitle =>
'Move your library, history and settings to a new device';
@override
String get backupTitle => 'Backup & Restore';
@override
String get backupExportSectionTitle => 'Create backup';
@override
String get backupExportSectionDescription =>
'Save your settings, download history, liked tracks, wishlist, favorite artists and playlists into a single file you can keep or move to another phone.';
@override
String get backupExportButton => 'Create backup file';
@override
String get backupImportSectionTitle => 'Restore backup';
@override
String get backupImportSectionDescription =>
'Pick a backup file to restore your data. This replaces the current settings, history and library on this device.';
@override
String get backupImportButton => 'Choose backup file';
@override
String get backupCreating => 'Creating backup...';
@override
String get backupCreated => 'Backup created';
@override
String get backupCreateFailed => 'Failed to create backup';
@override
String get backupEmpty => 'There is nothing to back up yet';
@override
String get backupRestoreConfirmTitle => 'Restore this backup?';
@override
String get backupRestoreConfirmMessage =>
'This will replace your current settings, download history, liked tracks, wishlist and playlists with the contents of the backup. This cannot be undone.';
@override
String get backupRestoreConfirmButton => 'Restore';
@override
String get backupRestoring => 'Restoring backup...';
@override
String get backupRestored => 'Backup restored successfully';
@override
String get backupRestoreFailed => 'Failed to restore backup';
@override
String get backupInvalidFile => 'This file is not a valid SpotiFLAC backup';
@override
String get backupRestoreRestartHint =>
'Restart the app to make sure every change is applied.';
@override
String get backupContentsTitle => 'Backup contents';
@override
String get backupContentsSettings => 'App settings';
@override
String backupContentsHistory(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'items',
one: 'item',
);
return '$count history $_temp0';
}
@override
String backupContentsLiked(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'tracks',
one: 'track',
);
return '$count liked $_temp0';
}
@override
String backupContentsWishlist(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'tracks',
one: 'track',
);
return '$count wishlist $_temp0';
}
@override
String backupContentsPlaylists(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count playlists',
one: '1 playlist',
);
return '$_temp0';
}
@override
String backupContentsArtists(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count favorite artists',
one: '1 favorite artist',
);
return '$_temp0';
}
@override
String get tooltipLoveAll => 'Love All';
+129
View File
@@ -2914,6 +2914,135 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get settingsDonateSubtitle => 'Buy the developer a coffee';
@override
String get settingsBackup => 'Backup & Restore';
@override
String get settingsBackupSubtitle =>
'Move your library, history and settings to a new device';
@override
String get backupTitle => 'Backup & Restore';
@override
String get backupExportSectionTitle => 'Create backup';
@override
String get backupExportSectionDescription =>
'Save your settings, download history, liked tracks, wishlist, favorite artists and playlists into a single file you can keep or move to another phone.';
@override
String get backupExportButton => 'Create backup file';
@override
String get backupImportSectionTitle => 'Restore backup';
@override
String get backupImportSectionDescription =>
'Pick a backup file to restore your data. This replaces the current settings, history and library on this device.';
@override
String get backupImportButton => 'Choose backup file';
@override
String get backupCreating => 'Creating backup...';
@override
String get backupCreated => 'Backup created';
@override
String get backupCreateFailed => 'Failed to create backup';
@override
String get backupEmpty => 'There is nothing to back up yet';
@override
String get backupRestoreConfirmTitle => 'Restore this backup?';
@override
String get backupRestoreConfirmMessage =>
'This will replace your current settings, download history, liked tracks, wishlist and playlists with the contents of the backup. This cannot be undone.';
@override
String get backupRestoreConfirmButton => 'Restore';
@override
String get backupRestoring => 'Restoring backup...';
@override
String get backupRestored => 'Backup restored successfully';
@override
String get backupRestoreFailed => 'Failed to restore backup';
@override
String get backupInvalidFile => 'This file is not a valid SpotiFLAC backup';
@override
String get backupRestoreRestartHint =>
'Restart the app to make sure every change is applied.';
@override
String get backupContentsTitle => 'Backup contents';
@override
String get backupContentsSettings => 'App settings';
@override
String backupContentsHistory(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'items',
one: 'item',
);
return '$count history $_temp0';
}
@override
String backupContentsLiked(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'tracks',
one: 'track',
);
return '$count liked $_temp0';
}
@override
String backupContentsWishlist(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'tracks',
one: 'track',
);
return '$count wishlist $_temp0';
}
@override
String backupContentsPlaylists(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count playlists',
one: '1 playlist',
);
return '$_temp0';
}
@override
String backupContentsArtists(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count favorite artists',
one: '1 favorite artist',
);
return '$_temp0';
}
@override
String get tooltipLoveAll => 'Love All';
+129
View File
@@ -2929,6 +2929,135 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get settingsDonateSubtitle => 'Buy the developer a coffee';
@override
String get settingsBackup => 'Backup & Restore';
@override
String get settingsBackupSubtitle =>
'Move your library, history and settings to a new device';
@override
String get backupTitle => 'Backup & Restore';
@override
String get backupExportSectionTitle => 'Create backup';
@override
String get backupExportSectionDescription =>
'Save your settings, download history, liked tracks, wishlist, favorite artists and playlists into a single file you can keep or move to another phone.';
@override
String get backupExportButton => 'Create backup file';
@override
String get backupImportSectionTitle => 'Restore backup';
@override
String get backupImportSectionDescription =>
'Pick a backup file to restore your data. This replaces the current settings, history and library on this device.';
@override
String get backupImportButton => 'Choose backup file';
@override
String get backupCreating => 'Creating backup...';
@override
String get backupCreated => 'Backup created';
@override
String get backupCreateFailed => 'Failed to create backup';
@override
String get backupEmpty => 'There is nothing to back up yet';
@override
String get backupRestoreConfirmTitle => 'Restore this backup?';
@override
String get backupRestoreConfirmMessage =>
'This will replace your current settings, download history, liked tracks, wishlist and playlists with the contents of the backup. This cannot be undone.';
@override
String get backupRestoreConfirmButton => 'Restore';
@override
String get backupRestoring => 'Restoring backup...';
@override
String get backupRestored => 'Backup restored successfully';
@override
String get backupRestoreFailed => 'Failed to restore backup';
@override
String get backupInvalidFile => 'This file is not a valid SpotiFLAC backup';
@override
String get backupRestoreRestartHint =>
'Restart the app to make sure every change is applied.';
@override
String get backupContentsTitle => 'Backup contents';
@override
String get backupContentsSettings => 'App settings';
@override
String backupContentsHistory(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'items',
one: 'item',
);
return '$count history $_temp0';
}
@override
String backupContentsLiked(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'tracks',
one: 'track',
);
return '$count liked $_temp0';
}
@override
String backupContentsWishlist(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'tracks',
one: 'track',
);
return '$count wishlist $_temp0';
}
@override
String backupContentsPlaylists(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count playlists',
one: '1 playlist',
);
return '$_temp0';
}
@override
String backupContentsArtists(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count favorite artists',
one: '1 favorite artist',
);
return '$_temp0';
}
@override
String get tooltipLoveAll => 'Уподобати всіх';
+129
View File
@@ -2885,6 +2885,135 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get settingsDonateSubtitle => 'Buy the developer a coffee';
@override
String get settingsBackup => 'Backup & Restore';
@override
String get settingsBackupSubtitle =>
'Move your library, history and settings to a new device';
@override
String get backupTitle => 'Backup & Restore';
@override
String get backupExportSectionTitle => 'Create backup';
@override
String get backupExportSectionDescription =>
'Save your settings, download history, liked tracks, wishlist, favorite artists and playlists into a single file you can keep or move to another phone.';
@override
String get backupExportButton => 'Create backup file';
@override
String get backupImportSectionTitle => 'Restore backup';
@override
String get backupImportSectionDescription =>
'Pick a backup file to restore your data. This replaces the current settings, history and library on this device.';
@override
String get backupImportButton => 'Choose backup file';
@override
String get backupCreating => 'Creating backup...';
@override
String get backupCreated => 'Backup created';
@override
String get backupCreateFailed => 'Failed to create backup';
@override
String get backupEmpty => 'There is nothing to back up yet';
@override
String get backupRestoreConfirmTitle => 'Restore this backup?';
@override
String get backupRestoreConfirmMessage =>
'This will replace your current settings, download history, liked tracks, wishlist and playlists with the contents of the backup. This cannot be undone.';
@override
String get backupRestoreConfirmButton => 'Restore';
@override
String get backupRestoring => 'Restoring backup...';
@override
String get backupRestored => 'Backup restored successfully';
@override
String get backupRestoreFailed => 'Failed to restore backup';
@override
String get backupInvalidFile => 'This file is not a valid SpotiFLAC backup';
@override
String get backupRestoreRestartHint =>
'Restart the app to make sure every change is applied.';
@override
String get backupContentsTitle => 'Backup contents';
@override
String get backupContentsSettings => 'App settings';
@override
String backupContentsHistory(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'items',
one: 'item',
);
return '$count history $_temp0';
}
@override
String backupContentsLiked(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'tracks',
one: 'track',
);
return '$count liked $_temp0';
}
@override
String backupContentsWishlist(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'tracks',
one: 'track',
);
return '$count wishlist $_temp0';
}
@override
String backupContentsPlaylists(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count playlists',
one: '1 playlist',
);
return '$_temp0';
}
@override
String backupContentsArtists(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count favorite artists',
one: '1 favorite artist',
);
return '$_temp0';
}
@override
String get tooltipLoveAll => 'Love All';
+137
View File
@@ -3827,6 +3827,143 @@
"@settingsDonateSubtitle": {
"description": "Subtitle for donate menu item"
},
"settingsBackup": "Backup & Restore",
"@settingsBackup": {
"description": "Settings menu item - backup and restore page"
},
"settingsBackupSubtitle": "Move your library, history and settings to a new device",
"@settingsBackupSubtitle": {
"description": "Subtitle for backup and restore settings item"
},
"backupTitle": "Backup & Restore",
"@backupTitle": {
"description": "App bar title for the backup and restore page"
},
"backupExportSectionTitle": "Create backup",
"@backupExportSectionTitle": {
"description": "Section title for the export/backup card"
},
"backupExportSectionDescription": "Save your settings, download history, liked tracks, wishlist, favorite artists and playlists into a single file you can keep or move to another phone.",
"@backupExportSectionDescription": {
"description": "Description of what a backup contains"
},
"backupExportButton": "Create backup file",
"@backupExportButton": {
"description": "Button to create and share a backup file"
},
"backupImportSectionTitle": "Restore backup",
"@backupImportSectionTitle": {
"description": "Section title for the import/restore card"
},
"backupImportSectionDescription": "Pick a backup file to restore your data. This replaces the current settings, history and library on this device.",
"@backupImportSectionDescription": {
"description": "Description for the restore action"
},
"backupImportButton": "Choose backup file",
"@backupImportButton": {
"description": "Button to pick a backup file to restore"
},
"backupCreating": "Creating backup...",
"@backupCreating": {
"description": "Progress text while a backup is being created"
},
"backupCreated": "Backup created",
"@backupCreated": {
"description": "Snackbar after a backup file is created"
},
"backupCreateFailed": "Failed to create backup",
"@backupCreateFailed": {
"description": "Snackbar when backup creation fails"
},
"backupEmpty": "There is nothing to back up yet",
"@backupEmpty": {
"description": "Snackbar when there is no data to back up"
},
"backupRestoreConfirmTitle": "Restore this backup?",
"@backupRestoreConfirmTitle": {
"description": "Confirmation dialog title before restoring a backup"
},
"backupRestoreConfirmMessage": "This will replace your current settings, download history, liked tracks, wishlist and playlists with the contents of the backup. This cannot be undone.",
"@backupRestoreConfirmMessage": {
"description": "Confirmation dialog message before restoring a backup"
},
"backupRestoreConfirmButton": "Restore",
"@backupRestoreConfirmButton": {
"description": "Confirm button to proceed with restore"
},
"backupRestoring": "Restoring backup...",
"@backupRestoring": {
"description": "Progress text while restoring a backup"
},
"backupRestored": "Backup restored successfully",
"@backupRestored": {
"description": "Snackbar after a successful restore"
},
"backupRestoreFailed": "Failed to restore backup",
"@backupRestoreFailed": {
"description": "Snackbar when restore fails"
},
"backupInvalidFile": "This file is not a valid SpotiFLAC backup",
"@backupInvalidFile": {
"description": "Snackbar when the chosen file is not a valid backup"
},
"backupRestoreRestartHint": "Restart the app to make sure every change is applied.",
"@backupRestoreRestartHint": {
"description": "Hint shown after restoring that an app restart is recommended"
},
"backupContentsTitle": "Backup contents",
"@backupContentsTitle": {
"description": "Header above the list summarizing what the backup contains"
},
"backupContentsSettings": "App settings",
"@backupContentsSettings": {
"description": "Backup contents row label for settings"
},
"backupContentsHistory": "{count} history {count, plural, =1{item} other{items}}",
"@backupContentsHistory": {
"description": "Backup contents row for history count",
"placeholders": {
"count": {
"type": "int"
}
}
},
"backupContentsLiked": "{count} liked {count, plural, =1{track} other{tracks}}",
"@backupContentsLiked": {
"description": "Backup contents row for liked tracks count",
"placeholders": {
"count": {
"type": "int"
}
}
},
"backupContentsWishlist": "{count} wishlist {count, plural, =1{track} other{tracks}}",
"@backupContentsWishlist": {
"description": "Backup contents row for wishlist tracks count",
"placeholders": {
"count": {
"type": "int"
}
}
},
"backupContentsPlaylists": "{count, plural, =1{1 playlist} other{{count} playlists}}",
"@backupContentsPlaylists": {
"description": "Backup contents row for playlist count",
"placeholders": {
"count": {
"type": "int"
}
}
},
"backupContentsArtists": "{count, plural, =1{1 favorite artist} other{{count} favorite artists}}",
"@backupContentsArtists": {
"description": "Backup contents row for favorite artists count",
"placeholders": {
"count": {
"type": "int"
}
}
},
"tooltipLoveAll": "Love All",
"@tooltipLoveAll": {
"description": "Tooltip for the Love All button on album/playlist screens"
+63
View File
@@ -3689,6 +3689,69 @@
"@settingsDonateSubtitle": {
"description": "Subtitle for donate menu item"
},
"settingsBackup": "Cadangkan & Pulihkan",
"settingsBackupSubtitle": "Pindahkan pustaka, riwayat, dan pengaturan ke perangkat baru",
"backupTitle": "Cadangkan & Pulihkan",
"backupExportSectionTitle": "Buat cadangan",
"backupExportSectionDescription": "Simpan pengaturan, riwayat unduhan, lagu disukai, wishlist, artis favorit, dan playlist ke dalam satu file yang bisa kamu simpan atau pindahkan ke ponsel lain.",
"backupExportButton": "Buat file cadangan",
"backupImportSectionTitle": "Pulihkan cadangan",
"backupImportSectionDescription": "Pilih file cadangan untuk memulihkan data. Ini akan menggantikan pengaturan, riwayat, dan pustaka di perangkat ini.",
"backupImportButton": "Pilih file cadangan",
"backupCreating": "Membuat cadangan...",
"backupCreated": "Cadangan berhasil dibuat",
"backupCreateFailed": "Gagal membuat cadangan",
"backupEmpty": "Belum ada data untuk dicadangkan",
"backupRestoreConfirmTitle": "Pulihkan cadangan ini?",
"backupRestoreConfirmMessage": "Ini akan menggantikan pengaturan, riwayat unduhan, lagu disukai, wishlist, dan playlist saat ini dengan isi cadangan. Tindakan ini tidak bisa dibatalkan.",
"backupRestoreConfirmButton": "Pulihkan",
"backupRestoring": "Memulihkan cadangan...",
"backupRestored": "Cadangan berhasil dipulihkan",
"backupRestoreFailed": "Gagal memulihkan cadangan",
"backupInvalidFile": "File ini bukan cadangan SpotiFLAC yang valid",
"backupRestoreRestartHint": "Mulai ulang aplikasi untuk memastikan semua perubahan diterapkan.",
"backupContentsTitle": "Isi cadangan",
"backupContentsSettings": "Pengaturan aplikasi",
"backupContentsHistory": "{count} item riwayat",
"@backupContentsHistory": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"backupContentsLiked": "{count} lagu disukai",
"@backupContentsLiked": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"backupContentsWishlist": "{count} lagu di wishlist",
"@backupContentsWishlist": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"backupContentsPlaylists": "{count, plural, =1{1 playlist} other{{count} playlist}}",
"@backupContentsPlaylists": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"backupContentsArtists": "{count, plural, =1{1 artis favorit} other{{count} artis favorit}}",
"@backupContentsArtists": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"tooltipLoveAll": "Love All",
"@tooltipLoveAll": {
"description": "Tooltip for the Love All button on album/playlist screens"
@@ -1634,6 +1634,17 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
Future<int> getDatabaseCount() async {
return await _db.getCount();
}
/// Replaces all download history with [items] (each in the
/// [DownloadHistoryItem.toJson] shape) from a restored backup, then reloads
/// the in-memory state from storage.
Future<void> restoreFromBackup(List<Map<String, dynamic>> items) async {
await _db.clearAll();
if (items.isNotEmpty) {
await _db.upsertBatch(items);
}
await reloadFromStorage();
}
}
final downloadHistoryProvider =
@@ -953,6 +953,90 @@ class LibraryCollectionsNotifier extends Notifier<LibraryCollectionsState> {
});
_invalidatePlaylistPickerSummaries();
}
/// Returns the full collections snapshot (wishlist, loved, playlists,
/// favorite artists) for a backup, ensuring data is loaded first.
Future<Map<String, dynamic>> exportCollections() async {
await _ensureLoaded();
return state.toJson();
}
/// Exports custom playlist cover images as base64, keyed by playlist id.
/// Each value contains the original file extension and the encoded bytes so a
/// restore on another device can recreate the cover files.
Future<Map<String, Map<String, String>>> exportPlaylistCovers() async {
await _ensureLoaded();
final covers = <String, Map<String, String>>{};
for (final playlist in state.playlists) {
final path = playlist.coverImagePath;
if (path == null || path.isEmpty) continue;
try {
final file = File(path);
if (!await file.exists()) continue;
final bytes = await file.readAsBytes();
if (bytes.isEmpty) continue;
covers[playlist.id] = {
'ext': p.extension(path).toLowerCase(),
'data': base64Encode(bytes),
};
} catch (_) {
// Skip unreadable cover; the rest of the backup still succeeds.
}
}
return covers;
}
/// Replaces all collections (wishlist, loved, playlists, favorite artists)
/// with the contents of a backup. [collectionsJson] uses the
/// [LibraryCollectionsState.toJson] shape; [coverImages] is the map produced
/// by [exportPlaylistCovers]. Cover images are rewritten into this device's
/// covers directory and their paths fixed up before persisting.
Future<void> restoreFromBackup(
Map<String, dynamic> collectionsJson, {
Map<String, dynamic>? coverImages,
}) async {
final normalized = Map<String, dynamic>.from(collectionsJson);
final coversDir = await _playlistCoversDir();
final playlistsRaw = normalized['playlists'];
if (playlistsRaw is List) {
final rewritten = <Map<String, dynamic>>[];
for (final entry in playlistsRaw.whereType<Map<Object?, Object?>>()) {
final playlist = Map<String, dynamic>.from(entry);
final id = playlist['id'] as String?;
String? newCoverPath;
final coverEntry = (id != null && coverImages != null)
? coverImages[id]
: null;
if (id != null && coverEntry is Map) {
final data = coverEntry['data'] as String?;
final ext = (coverEntry['ext'] as String?) ?? '.jpg';
if (data != null && data.isNotEmpty) {
try {
final destPath = p.join(coversDir.path, '$id$ext');
await File(destPath).writeAsBytes(base64Decode(data));
newCoverPath = destPath;
} catch (_) {
newCoverPath = null;
}
}
}
// Always replace the backup's device-specific path: either with the
// freshly written local cover, or drop it so a stale path is not kept.
if (newCoverPath != null) {
playlist['coverImagePath'] = newCoverPath;
} else {
playlist.remove('coverImagePath');
}
rewritten.add(playlist);
}
normalized['playlists'] = rewritten;
}
await _db.replaceAllFromBackup(normalized);
await _load();
_invalidatePlaylistPickerSummaries();
}
}
final libraryCollectionsProvider =
+34
View File
@@ -194,6 +194,40 @@ class SettingsNotifier extends Notifier<AppSettings> {
}
}
/// Restores settings from a backup payload (the map produced by
/// [AppSettings.toJson]). Device-specific storage location fields
/// (download directory and SAF tree URI) are intentionally preserved from the
/// current device, because a SAF tree URI from another phone is not valid
/// here and would break downloads.
Future<void> restoreFromBackup(Map<String, dynamic> json) async {
final current = state;
AppSettings restored;
try {
restored = AppSettings.fromJson(Map<String, dynamic>.from(json));
} catch (e, stack) {
_log.e('Failed to parse settings from backup: $e', e, stack);
rethrow;
}
state = restored.copyWith(
// Always keep extension providers enabled (matches _loadSettings).
useExtensionProviders: true,
// Preserve this device's storage location; the backup's values point at
// the original device and would not resolve here.
downloadDirectory: current.downloadDirectory,
downloadDirectoryBookmark: current.downloadDirectoryBookmark,
storageMode: current.storageMode,
downloadTreeUri: current.downloadTreeUri,
);
await _saveSettings();
LogBuffer.loggingEnabled = state.enableLogging;
_syncLyricsSettingsToBackend();
_syncNetworkCompatibilitySettingsToBackend();
_syncExtensionFallbackSettingsToBackend();
}
Future<void> _normalizeIosDownloadDirectoryIfNeeded() async {
if (!Platform.isIOS) return;
@@ -0,0 +1,374 @@
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:share_plus/share_plus.dart' show ShareParams, SharePlus, XFile;
import 'package:spotiflac_android/l10n/l10n.dart';
import 'package:spotiflac_android/providers/download_queue_provider.dart';
import 'package:spotiflac_android/providers/library_collections_provider.dart';
import 'package:spotiflac_android/providers/settings_provider.dart';
import 'package:spotiflac_android/services/backup_service.dart';
import 'package:spotiflac_android/services/history_database.dart';
import 'package:spotiflac_android/utils/app_bar_layout.dart';
import 'package:spotiflac_android/utils/logger.dart';
class BackupRestorePage extends ConsumerStatefulWidget {
const BackupRestorePage({super.key});
@override
ConsumerState<BackupRestorePage> createState() => _BackupRestorePageState();
}
class _BackupRestorePageState extends ConsumerState<BackupRestorePage> {
static final _log = AppLogger('BackupRestorePage');
bool _isExporting = false;
bool _isImporting = false;
bool get _isBusy => _isExporting || _isImporting;
Future<void> _createBackup() async {
if (_isBusy) return;
setState(() => _isExporting = true);
final l10n = context.l10n;
final messenger = ScaffoldMessenger.of(context);
try {
final settings = ref.read(settingsProvider).toJson();
final history = await HistoryDatabase.instance.getAll();
final collectionsNotifier = ref.read(
libraryCollectionsProvider.notifier,
);
final collections = await collectionsNotifier.exportCollections();
final covers = await collectionsNotifier.exportPlaylistCovers();
final envelope = BackupService.buildEnvelope(
settings: settings,
history: history,
collections: collections,
playlistCovers: covers,
);
final file = await BackupService.writeBackupFile(envelope);
messenger.showSnackBar(SnackBar(content: Text(l10n.backupCreated)));
await SharePlus.instance.share(
ShareParams(files: [XFile(file.path)], text: l10n.backupTitle),
);
} catch (e, stack) {
_log.e('Failed to create backup: $e', e, stack);
messenger.showSnackBar(
SnackBar(content: Text(l10n.backupCreateFailed)),
);
} finally {
if (mounted) setState(() => _isExporting = false);
}
}
Future<void> _restoreBackup() async {
if (_isBusy) return;
final l10n = context.l10n;
final messenger = ScaffoldMessenger.of(context);
String? content;
try {
final result = await FilePicker.pickFiles(
type: FileType.custom,
allowedExtensions: ['json', BackupService.fileExtension],
);
final path = result?.files.single.path;
if (path == null) return;
content = await File(path).readAsString();
} catch (e) {
_log.e('Failed to read backup file: $e');
messenger.showSnackBar(
SnackBar(content: Text(l10n.backupInvalidFile)),
);
return;
}
final bundle = BackupService.parse(content);
if (bundle == null) {
messenger.showSnackBar(
SnackBar(content: Text(l10n.backupInvalidFile)),
);
return;
}
if (!mounted) return;
final confirmed = await _confirmRestore(bundle);
if (confirmed != true || !mounted) return;
setState(() => _isImporting = true);
try {
if (bundle.hasSettings) {
await ref
.read(settingsProvider.notifier)
.restoreFromBackup(bundle.settings!);
}
await ref
.read(downloadHistoryProvider.notifier)
.restoreFromBackup(bundle.history);
await ref
.read(libraryCollectionsProvider.notifier)
.restoreFromBackup(
bundle.collections,
coverImages: bundle.playlistCovers,
);
messenger.showSnackBar(
SnackBar(
content: Text('${l10n.backupRestored}\n${l10n.backupRestoreRestartHint}'),
duration: const Duration(seconds: 5),
),
);
} catch (e, stack) {
_log.e('Failed to restore backup: $e', e, stack);
messenger.showSnackBar(
SnackBar(content: Text(l10n.backupRestoreFailed)),
);
} finally {
if (mounted) setState(() => _isImporting = false);
}
}
Future<bool?> _confirmRestore(BackupBundle bundle) {
final l10n = context.l10n;
return showDialog<bool>(
context: context,
builder: (dialogContext) {
final theme = Theme.of(dialogContext);
return AlertDialog(
title: Text(l10n.backupRestoreConfirmTitle),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(l10n.backupRestoreConfirmMessage),
const SizedBox(height: 16),
Text(
l10n.backupContentsTitle,
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 6),
if (bundle.hasSettings)
_ContentRow(
icon: Icons.settings_outlined,
label: l10n.backupContentsSettings,
),
_ContentRow(
icon: Icons.history,
label: l10n.backupContentsHistory(bundle.historyCount),
),
_ContentRow(
icon: Icons.favorite_outline,
label: l10n.backupContentsLiked(bundle.likedCount),
),
_ContentRow(
icon: Icons.bookmark_outline,
label: l10n.backupContentsWishlist(bundle.wishlistCount),
),
_ContentRow(
icon: Icons.queue_music_outlined,
label: l10n.backupContentsPlaylists(bundle.playlistCount),
),
if (bundle.favoriteArtistCount > 0)
_ContentRow(
icon: Icons.person_outline,
label: l10n.backupContentsArtists(
bundle.favoriteArtistCount,
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(false),
child: Text(l10n.dialogCancel),
),
FilledButton(
onPressed: () => Navigator.of(dialogContext).pop(true),
child: Text(l10n.backupRestoreConfirmButton),
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final topPadding = normalizedHeaderTopPadding(context);
final l10n = context.l10n;
return Scaffold(
body: CustomScrollView(
slivers: [
SliverAppBar(
expandedHeight: 120 + topPadding,
collapsedHeight: kToolbarHeight,
floating: false,
pinned: true,
backgroundColor: colorScheme.surface,
surfaceTintColor: Colors.transparent,
leading: IconButton(
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.pop(context),
),
flexibleSpace: LayoutBuilder(
builder: (context, constraints) {
final maxHeight = 120 + topPadding;
final minHeight = kToolbarHeight + topPadding;
final expandRatio =
((constraints.maxHeight - minHeight) /
(maxHeight - minHeight))
.clamp(0.0, 1.0);
final leftPadding = 56 - (32 * expandRatio);
return FlexibleSpaceBar(
expandedTitleScale: 1.0,
titlePadding: EdgeInsets.only(left: leftPadding, bottom: 16),
title: Text(
l10n.backupTitle,
style: TextStyle(
fontSize: 20 + (8 * expandRatio),
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
),
);
},
),
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_ActionCard(
icon: Icons.backup_outlined,
title: l10n.backupExportSectionTitle,
description: l10n.backupExportSectionDescription,
buttonLabel: l10n.backupExportButton,
buttonIcon: Icons.ios_share,
isBusy: _isExporting,
onPressed: _isBusy ? null : _createBackup,
),
const SizedBox(height: 16),
_ActionCard(
icon: Icons.settings_backup_restore,
title: l10n.backupImportSectionTitle,
description: l10n.backupImportSectionDescription,
buttonLabel: l10n.backupImportButton,
buttonIcon: Icons.folder_open_outlined,
isBusy: _isImporting,
onPressed: _isBusy ? null : _restoreBackup,
),
],
),
),
),
],
),
);
}
}
class _ActionCard extends StatelessWidget {
final IconData icon;
final String title;
final String description;
final String buttonLabel;
final IconData buttonIcon;
final bool isBusy;
final VoidCallback? onPressed;
const _ActionCard({
required this.icon,
required this.title,
required this.description,
required this.buttonLabel,
required this.buttonIcon,
required this.isBusy,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHighest.withValues(alpha: 0.35),
borderRadius: BorderRadius.circular(18),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(icon, color: colorScheme.primary),
const SizedBox(width: 12),
Expanded(
child: Text(
title,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w700,
),
),
),
],
),
const SizedBox(height: 10),
Text(
description,
style: theme.textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 16),
FilledButton.icon(
onPressed: onPressed,
icon: isBusy
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Icon(buttonIcon),
label: Text(buttonLabel),
),
],
),
);
}
}
class _ContentRow extends StatelessWidget {
final IconData icon;
final String label;
const _ContentRow({required this.icon, required this.label});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 3),
child: Row(
children: [
Icon(icon, size: 18, color: theme.colorScheme.onSurfaceVariant),
const SizedBox(width: 10),
Expanded(child: Text(label, style: theme.textTheme.bodyMedium)),
],
),
);
}
}
+8
View File
@@ -12,6 +12,7 @@ import 'package:spotiflac_android/screens/settings/library_settings_page.dart';
import 'package:spotiflac_android/screens/settings/app_settings_page.dart';
import 'package:spotiflac_android/screens/settings/about_page.dart';
import 'package:spotiflac_android/screens/settings/cache_management_page.dart';
import 'package:spotiflac_android/screens/settings/backup_restore_page.dart';
import 'package:spotiflac_android/screens/settings/donate_page.dart';
import 'package:spotiflac_android/screens/settings/log_screen.dart';
import 'package:spotiflac_android/utils/app_bar_layout.dart';
@@ -160,6 +161,13 @@ class SettingsTab extends ConsumerWidget {
onTap: () =>
_navigateTo(context, const AppSettingsPage()),
),
SettingsItem(
icon: Icons.settings_backup_restore,
title: l10n.settingsBackup,
subtitle: l10n.settingsBackupSubtitle,
onTap: () =>
_navigateTo(context, const BackupRestorePage()),
),
SettingsItem(
icon: Icons.article_outlined,
title: l10n.logTitle,
+178
View File
@@ -0,0 +1,178 @@
import 'dart:convert';
import 'dart:io';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:spotiflac_android/constants/app_info.dart';
import 'package:spotiflac_android/utils/logger.dart';
/// Parsed contents of a backup file.
class BackupBundle {
final int formatVersion;
final String appVersion;
final DateTime? createdAt;
/// Raw `AppSettings.toJson()` map, or null when not present.
final Map<String, dynamic>? settings;
/// History items in `DownloadHistoryItem.toJson()` shape.
final List<Map<String, dynamic>> history;
/// Collections in `LibraryCollectionsState.toJson()` shape
/// (wishlist / loved / playlists / favoriteArtists).
final Map<String, dynamic> collections;
/// Playlist cover images keyed by playlist id: `{ id: { ext, data } }`.
final Map<String, dynamic> playlistCovers;
const BackupBundle({
required this.formatVersion,
required this.appVersion,
required this.createdAt,
required this.settings,
required this.history,
required this.collections,
required this.playlistCovers,
});
bool get hasSettings => settings != null && settings!.isNotEmpty;
int get historyCount => history.length;
int _collectionListCount(String key) {
final value = collections[key];
return value is List ? value.length : 0;
}
int get likedCount => _collectionListCount('loved');
int get wishlistCount => _collectionListCount('wishlist');
int get playlistCount => _collectionListCount('playlists');
int get favoriteArtistCount => _collectionListCount('favoriteArtists');
bool get isEmpty =>
!hasSettings &&
historyCount == 0 &&
likedCount == 0 &&
wishlistCount == 0 &&
playlistCount == 0 &&
favoriteArtistCount == 0;
}
/// Builds and parses SpotiFLAC backup files (a single JSON document containing
/// settings, download history and the user library).
class BackupService {
static final _log = AppLogger('BackupService');
static const String magic = 'spotiflac-backup';
static const int formatVersion = 1;
static const String fileExtension = 'json';
/// Builds the backup envelope written to disk.
static Map<String, dynamic> buildEnvelope({
required Map<String, dynamic>? settings,
required List<Map<String, dynamic>> history,
required Map<String, dynamic> collections,
required Map<String, dynamic> playlistCovers,
}) {
return {
'magic': magic,
'format_version': formatVersion,
'app': 'SpotiFLAC Mobile',
'app_version': AppInfo.displayVersion,
'created_at': DateTime.now().toIso8601String(),
'data': {
'settings': settings,
'history': history,
'collections': collections,
'playlist_covers': playlistCovers,
},
};
}
/// Writes [envelope] to a timestamped file under the app documents directory
/// and returns the created file.
static Future<File> writeBackupFile(Map<String, dynamic> envelope) async {
final dir = await getApplicationDocumentsDirectory();
final backupsDir = Directory(p.join(dir.path, 'backups'));
if (!await backupsDir.exists()) {
await backupsDir.create(recursive: true);
}
final now = DateTime.now();
String two(int v) => v.toString().padLeft(2, '0');
final stamp =
'${now.year}${two(now.month)}${two(now.day)}_${two(now.hour)}${two(now.minute)}${two(now.second)}';
final fileName = 'spotiflac_backup_$stamp.$fileExtension';
final file = File(p.join(backupsDir.path, fileName));
await file.writeAsString(jsonEncode(envelope), flush: true);
_log.i('Backup written to ${file.path}');
return file;
}
/// Parses and validates a backup file's contents. Returns null when the
/// content is not a recognizable SpotiFLAC backup.
static BackupBundle? parse(String content) {
dynamic decoded;
try {
decoded = jsonDecode(content);
} catch (e) {
_log.w('Backup parse failed: not valid JSON ($e)');
return null;
}
if (decoded is! Map) {
_log.w('Backup parse failed: root is not an object');
return null;
}
final root = Map<String, dynamic>.from(decoded);
if (root['magic'] != magic) {
_log.w('Backup parse failed: magic marker missing');
return null;
}
final dataRaw = root['data'];
if (dataRaw is! Map) {
_log.w('Backup parse failed: missing data section');
return null;
}
final data = Map<String, dynamic>.from(dataRaw);
Map<String, dynamic>? settings;
final settingsRaw = data['settings'];
if (settingsRaw is Map) {
settings = Map<String, dynamic>.from(settingsRaw);
}
final history = <Map<String, dynamic>>[];
final historyRaw = data['history'];
if (historyRaw is List) {
for (final item in historyRaw) {
if (item is Map) {
history.add(Map<String, dynamic>.from(item));
}
}
}
final collectionsRaw = data['collections'];
final collections = collectionsRaw is Map
? Map<String, dynamic>.from(collectionsRaw)
: <String, dynamic>{};
final coversRaw = data['playlist_covers'];
final playlistCovers = coversRaw is Map
? Map<String, dynamic>.from(coversRaw)
: <String, dynamic>{};
return BackupBundle(
formatVersion: (root['format_version'] as num?)?.toInt() ?? 1,
appVersion: root['app_version'] as String? ?? '',
createdAt: DateTime.tryParse(root['created_at'] as String? ?? ''),
settings: settings,
history: history,
collections: collections,
playlistCovers: playlistCovers,
);
}
}
@@ -595,4 +595,97 @@ class LibraryCollectionsDatabase {
);
});
}
/// Wipes every collection table and rewrites them from a restored backup.
///
/// [collectionsJson] must use the same shape as
/// `LibraryCollectionsState.toJson()` (wishlist/loved/playlists/favoriteArtists).
/// Track entries carry a nested `track` map (stored as `track_json`); favorite
/// artist entries are stored whole as `artist_json`.
Future<void> replaceAllFromBackup(Map<String, dynamic> collectionsJson) async {
final nowIso = DateTime.now().toIso8601String();
List<Map<String, dynamic>> listOf(String key) {
final raw = collectionsJson[key];
if (raw is! List) return const [];
return raw
.whereType<Map<Object?, Object?>>()
.map((e) => Map<String, dynamic>.from(e))
.toList(growable: false);
}
final wishlist = listOf('wishlist');
final loved = listOf('loved');
final playlists = listOf('playlists');
final favoriteArtists = listOf('favoriteArtists');
final db = await database;
await db.transaction((txn) async {
await txn.delete(_tablePlaylistTracks);
await txn.delete(_tablePlaylists);
await txn.delete(_tableWishlist);
await txn.delete(_tableLoved);
await txn.delete(_tableFavoriteArtists);
Future<void> insertTrackEntries(
String table,
List<Map<String, dynamic>> entries,
) async {
for (final entry in entries) {
final key = entry['key'] as String?;
final track = entry['track'];
if (key == null || key.isEmpty || track is! Map) continue;
await txn.insert(table, {
'track_key': key,
'track_json': jsonEncode(track),
'added_at': (entry['addedAt'] as String?) ?? nowIso,
}, conflictAlgorithm: ConflictAlgorithm.replace);
}
}
await insertTrackEntries(_tableWishlist, wishlist);
await insertTrackEntries(_tableLoved, loved);
for (final artist in favoriteArtists) {
final key = artist['key'] as String?;
if (key == null || key.isEmpty) continue;
await txn.insert(_tableFavoriteArtists, {
'artist_key': key,
'artist_json': jsonEncode(artist),
'added_at': (artist['addedAt'] as String?) ?? nowIso,
}, conflictAlgorithm: ConflictAlgorithm.replace);
}
for (final playlist in playlists) {
final id = playlist['id'] as String?;
if (id == null || id.isEmpty) continue;
final createdAt = (playlist['createdAt'] as String?) ?? nowIso;
final updatedAt = (playlist['updatedAt'] as String?) ?? createdAt;
await txn.insert(_tablePlaylists, {
'id': id,
'name': (playlist['name'] as String?) ?? '',
'cover_image_path': playlist['coverImagePath'] as String?,
'created_at': createdAt,
'updated_at': updatedAt,
}, conflictAlgorithm: ConflictAlgorithm.replace);
final tracksRaw = playlist['tracks'];
if (tracksRaw is! List) continue;
for (final trackEntry in tracksRaw.whereType<Map<Object?, Object?>>()) {
final entry = Map<String, dynamic>.from(trackEntry);
final key = entry['key'] as String?;
final track = entry['track'];
if (key == null || key.isEmpty || track is! Map) continue;
await txn.insert(_tablePlaylistTracks, {
'playlist_id': id,
'track_key': key,
'track_json': jsonEncode(track),
'added_at': (entry['addedAt'] as String?) ?? nowIso,
}, conflictAlgorithm: ConflictAlgorithm.replace);
}
}
});
_log.i('Restored collections from backup');
}
}