diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 97652455..1bbec51c 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -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: diff --git a/lib/l10n/app_localizations_ar.dart b/lib/l10n/app_localizations_ar.dart index d95a94a8..4213d446 100644 --- a/lib/l10n/app_localizations_ar.dart +++ b/lib/l10n/app_localizations_ar.dart @@ -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'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index ca6cb9fe..91ed36b3 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -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'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 0dae1cfa..be94ed08 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -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'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 9d563d0d..0c754567 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -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'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index ae7e6e48..47c32dbf 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -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'; diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart index d9ab9c3a..b055c5bd 100644 --- a/lib/l10n/app_localizations_hi.dart +++ b/lib/l10n/app_localizations_hi.dart @@ -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'; diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 62f18ee9..6e054ff6 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -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'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 5a0b9c04..631f470b 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -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'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 63ae3f73..a9bcb50e 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -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'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index e576faad..98b2c6a2 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -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'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index a95f93c4..5fb4580a 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -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'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index e7c2283c..5f2217fd 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -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'; diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index 2af388d9..3f246b06 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -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'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 1b99e0b2..de8210a4 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -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 => 'Уподобати всіх'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index b329b782..12ef5585 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -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'; diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 823f696e..2644f161 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -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" diff --git a/lib/l10n/arb/app_id.arb b/lib/l10n/arb/app_id.arb index 14418a9d..f8a04bfa 100644 --- a/lib/l10n/arb/app_id.arb +++ b/lib/l10n/arb/app_id.arb @@ -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" diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index 05e234b4..8ffce48e 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -1634,6 +1634,17 @@ class DownloadHistoryNotifier extends Notifier { Future 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 restoreFromBackup(List> items) async { + await _db.clearAll(); + if (items.isNotEmpty) { + await _db.upsertBatch(items); + } + await reloadFromStorage(); + } } final downloadHistoryProvider = diff --git a/lib/providers/library_collections_provider.dart b/lib/providers/library_collections_provider.dart index c43f9dba..7a33bfc7 100644 --- a/lib/providers/library_collections_provider.dart +++ b/lib/providers/library_collections_provider.dart @@ -953,6 +953,90 @@ class LibraryCollectionsNotifier extends Notifier { }); _invalidatePlaylistPickerSummaries(); } + + /// Returns the full collections snapshot (wishlist, loved, playlists, + /// favorite artists) for a backup, ensuring data is loaded first. + Future> 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>> exportPlaylistCovers() async { + await _ensureLoaded(); + final covers = >{}; + 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 restoreFromBackup( + Map collectionsJson, { + Map? coverImages, + }) async { + final normalized = Map.from(collectionsJson); + final coversDir = await _playlistCoversDir(); + + final playlistsRaw = normalized['playlists']; + if (playlistsRaw is List) { + final rewritten = >[]; + for (final entry in playlistsRaw.whereType>()) { + final playlist = Map.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 = diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index 530d82ee..13bf20cf 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -194,6 +194,40 @@ class SettingsNotifier extends Notifier { } } + /// 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 restoreFromBackup(Map json) async { + final current = state; + AppSettings restored; + try { + restored = AppSettings.fromJson(Map.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 _normalizeIosDownloadDirectoryIfNeeded() async { if (!Platform.isIOS) return; diff --git a/lib/screens/settings/backup_restore_page.dart b/lib/screens/settings/backup_restore_page.dart new file mode 100644 index 00000000..cf58a3fd --- /dev/null +++ b/lib/screens/settings/backup_restore_page.dart @@ -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 createState() => _BackupRestorePageState(); +} + +class _BackupRestorePageState extends ConsumerState { + static final _log = AppLogger('BackupRestorePage'); + + bool _isExporting = false; + bool _isImporting = false; + + bool get _isBusy => _isExporting || _isImporting; + + Future _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 _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 _confirmRestore(BackupBundle bundle) { + final l10n = context.l10n; + return showDialog( + 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)), + ], + ), + ); + } +} diff --git a/lib/screens/settings/settings_tab.dart b/lib/screens/settings/settings_tab.dart index 25df703c..c117f3b7 100644 --- a/lib/screens/settings/settings_tab.dart +++ b/lib/screens/settings/settings_tab.dart @@ -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, diff --git a/lib/services/backup_service.dart b/lib/services/backup_service.dart new file mode 100644 index 00000000..3d18a987 --- /dev/null +++ b/lib/services/backup_service.dart @@ -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? settings; + + /// History items in `DownloadHistoryItem.toJson()` shape. + final List> history; + + /// Collections in `LibraryCollectionsState.toJson()` shape + /// (wishlist / loved / playlists / favoriteArtists). + final Map collections; + + /// Playlist cover images keyed by playlist id: `{ id: { ext, data } }`. + final Map 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 buildEnvelope({ + required Map? settings, + required List> history, + required Map collections, + required Map 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 writeBackupFile(Map 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.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.from(dataRaw); + + Map? settings; + final settingsRaw = data['settings']; + if (settingsRaw is Map) { + settings = Map.from(settingsRaw); + } + + final history = >[]; + final historyRaw = data['history']; + if (historyRaw is List) { + for (final item in historyRaw) { + if (item is Map) { + history.add(Map.from(item)); + } + } + } + + final collectionsRaw = data['collections']; + final collections = collectionsRaw is Map + ? Map.from(collectionsRaw) + : {}; + + final coversRaw = data['playlist_covers']; + final playlistCovers = coversRaw is Map + ? Map.from(coversRaw) + : {}; + + 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, + ); + } +} diff --git a/lib/services/library_collections_database.dart b/lib/services/library_collections_database.dart index e77b0541..c7ab8e11 100644 --- a/lib/services/library_collections_database.dart +++ b/lib/services/library_collections_database.dart @@ -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 replaceAllFromBackup(Map collectionsJson) async { + final nowIso = DateTime.now().toIso8601String(); + + List> listOf(String key) { + final raw = collectionsJson[key]; + if (raw is! List) return const []; + return raw + .whereType>() + .map((e) => Map.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 insertTrackEntries( + String table, + List> 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>()) { + final entry = Map.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'); + } }