mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-07-02 11:05:38 +02:00
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:
@@ -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:
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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 => 'Уподобати всіх';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user