mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-07-02 11:05:38 +02:00
feat(backup): include installed extensions and settings in backup/restore
Back up the store registry URL plus each installed extension (id, version, enabled flag and settings) and restore them on a new device by reinstalling from the store and re-applying settings. Secret-flagged settings (tokens/API keys) are excluded by default behind an opt-in 'Include extension credentials' toggle. Device-bound signed sessions are never backed up. Settings are merged on restore so omitted secrets are not wiped; failed reinstalls are reported.
This commit is contained in:
@@ -5161,6 +5161,30 @@ abstract class AppLocalizations {
|
||||
/// **'{count, plural, =1{1 favorite artist} other{{count} favorite artists}}'**
|
||||
String backupContentsArtists(int count);
|
||||
|
||||
/// Backup contents row for installed extensions count
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{count, plural, =1{1 extension} other{{count} extensions}}'**
|
||||
String backupContentsExtensions(int count);
|
||||
|
||||
/// Toggle to include secret extension settings (tokens, API keys) in the backup
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Include extension credentials'**
|
||||
String get backupIncludeSecrets;
|
||||
|
||||
/// Explanation for the include-credentials toggle
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Tokens and API keys from extensions will be saved into the backup file. Keep the file private. When off, you re-enter them after restoring.'**
|
||||
String get backupIncludeSecretsDescription;
|
||||
|
||||
/// Snackbar/hint when some extensions failed to reinstall during restore
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{count} {count, plural, =1{extension} other{extensions}} could not be reinstalled. Install them manually from the store.'**
|
||||
String backupExtensionsRestoreFailed(int count);
|
||||
|
||||
/// Tooltip for the Love All button on album/playlist screens
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -5373,6 +5397,24 @@ abstract class AppLocalizations {
|
||||
/// **'Using standard network settings'**
|
||||
String get downloadNetworkCompatibilityModeDisabled;
|
||||
|
||||
/// Setting title for allowing requests to private/local network targets
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Allow Local Network Access'**
|
||||
String get downloadAllowLocalNetwork;
|
||||
|
||||
/// Subtitle when allow local network access is on
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Requests to local/private addresses are allowed (for local proxy or custom DNS)'**
|
||||
String get downloadAllowLocalNetworkEnabled;
|
||||
|
||||
/// Subtitle when allow local network access is off
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Local/private addresses are blocked for security'**
|
||||
String get downloadAllowLocalNetworkDisabled;
|
||||
|
||||
/// Subtitle when quality picker is disabled due to extension service
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
||||
@@ -3014,6 +3014,35 @@ class AppLocalizationsAr extends AppLocalizations {
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String backupContentsExtensions(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count extensions',
|
||||
one: '1 extension',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get backupIncludeSecrets => 'Include extension credentials';
|
||||
|
||||
@override
|
||||
String get backupIncludeSecretsDescription =>
|
||||
'Tokens and API keys from extensions will be saved into the backup file. Keep the file private. When off, you re-enter them after restoring.';
|
||||
|
||||
@override
|
||||
String backupExtensionsRestoreFailed(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'extensions',
|
||||
one: 'extension',
|
||||
);
|
||||
return '$count $_temp0 could not be reinstalled. Install them manually from the store.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get tooltipLoveAll => 'Love All';
|
||||
|
||||
@@ -3143,6 +3172,17 @@ class AppLocalizationsAr extends AppLocalizations {
|
||||
String get downloadNetworkCompatibilityModeDisabled =>
|
||||
'Using standard network settings';
|
||||
|
||||
@override
|
||||
String get downloadAllowLocalNetwork => 'Allow Local Network Access';
|
||||
|
||||
@override
|
||||
String get downloadAllowLocalNetworkEnabled =>
|
||||
'Requests to local/private addresses are allowed (for local proxy or custom DNS)';
|
||||
|
||||
@override
|
||||
String get downloadAllowLocalNetworkDisabled =>
|
||||
'Local/private addresses are blocked for security';
|
||||
|
||||
@override
|
||||
String get downloadSelectServiceToEnable =>
|
||||
'Select a provider with quality options to enable this option';
|
||||
|
||||
@@ -3051,6 +3051,35 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String backupContentsExtensions(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count extensions',
|
||||
one: '1 extension',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get backupIncludeSecrets => 'Include extension credentials';
|
||||
|
||||
@override
|
||||
String get backupIncludeSecretsDescription =>
|
||||
'Tokens and API keys from extensions will be saved into the backup file. Keep the file private. When off, you re-enter them after restoring.';
|
||||
|
||||
@override
|
||||
String backupExtensionsRestoreFailed(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'extensions',
|
||||
one: 'extension',
|
||||
);
|
||||
return '$count $_temp0 could not be reinstalled. Install them manually from the store.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get tooltipLoveAll => 'Alle lieben';
|
||||
|
||||
@@ -3183,6 +3212,17 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get downloadNetworkCompatibilityModeDisabled =>
|
||||
'Standard-Netzwerkeinstellungen verwenden';
|
||||
|
||||
@override
|
||||
String get downloadAllowLocalNetwork => 'Allow Local Network Access';
|
||||
|
||||
@override
|
||||
String get downloadAllowLocalNetworkEnabled =>
|
||||
'Requests to local/private addresses are allowed (for local proxy or custom DNS)';
|
||||
|
||||
@override
|
||||
String get downloadAllowLocalNetworkDisabled =>
|
||||
'Local/private addresses are blocked for security';
|
||||
|
||||
@override
|
||||
String get downloadSelectServiceToEnable =>
|
||||
'Select a provider with quality options to enable this option';
|
||||
|
||||
@@ -3014,6 +3014,35 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String backupContentsExtensions(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count extensions',
|
||||
one: '1 extension',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get backupIncludeSecrets => 'Include extension credentials';
|
||||
|
||||
@override
|
||||
String get backupIncludeSecretsDescription =>
|
||||
'Tokens and API keys from extensions will be saved into the backup file. Keep the file private. When off, you re-enter them after restoring.';
|
||||
|
||||
@override
|
||||
String backupExtensionsRestoreFailed(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'extensions',
|
||||
one: 'extension',
|
||||
);
|
||||
return '$count $_temp0 could not be reinstalled. Install them manually from the store.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get tooltipLoveAll => 'Love All';
|
||||
|
||||
@@ -3143,6 +3172,17 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get downloadNetworkCompatibilityModeDisabled =>
|
||||
'Using standard network settings';
|
||||
|
||||
@override
|
||||
String get downloadAllowLocalNetwork => 'Allow Local Network Access';
|
||||
|
||||
@override
|
||||
String get downloadAllowLocalNetworkEnabled =>
|
||||
'Requests to local/private addresses are allowed (for local proxy or custom DNS)';
|
||||
|
||||
@override
|
||||
String get downloadAllowLocalNetworkDisabled =>
|
||||
'Local/private addresses are blocked for security';
|
||||
|
||||
@override
|
||||
String get downloadSelectServiceToEnable =>
|
||||
'Select a provider with quality options to enable this option';
|
||||
|
||||
@@ -3014,6 +3014,35 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String backupContentsExtensions(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count extensions',
|
||||
one: '1 extension',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get backupIncludeSecrets => 'Include extension credentials';
|
||||
|
||||
@override
|
||||
String get backupIncludeSecretsDescription =>
|
||||
'Tokens and API keys from extensions will be saved into the backup file. Keep the file private. When off, you re-enter them after restoring.';
|
||||
|
||||
@override
|
||||
String backupExtensionsRestoreFailed(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'extensions',
|
||||
one: 'extension',
|
||||
);
|
||||
return '$count $_temp0 could not be reinstalled. Install them manually from the store.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get tooltipLoveAll => 'Love All';
|
||||
|
||||
@@ -3143,6 +3172,17 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
String get downloadNetworkCompatibilityModeDisabled =>
|
||||
'Using standard network settings';
|
||||
|
||||
@override
|
||||
String get downloadAllowLocalNetwork => 'Allow Local Network Access';
|
||||
|
||||
@override
|
||||
String get downloadAllowLocalNetworkEnabled =>
|
||||
'Requests to local/private addresses are allowed (for local proxy or custom DNS)';
|
||||
|
||||
@override
|
||||
String get downloadAllowLocalNetworkDisabled =>
|
||||
'Local/private addresses are blocked for security';
|
||||
|
||||
@override
|
||||
String get downloadSelectServiceToEnable =>
|
||||
'Select a provider with quality options to enable this option';
|
||||
|
||||
@@ -3091,6 +3091,35 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String backupContentsExtensions(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count extensions',
|
||||
one: '1 extension',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get backupIncludeSecrets => 'Include extension credentials';
|
||||
|
||||
@override
|
||||
String get backupIncludeSecretsDescription =>
|
||||
'Tokens and API keys from extensions will be saved into the backup file. Keep the file private. When off, you re-enter them after restoring.';
|
||||
|
||||
@override
|
||||
String backupExtensionsRestoreFailed(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'extensions',
|
||||
one: 'extension',
|
||||
);
|
||||
return '$count $_temp0 could not be reinstalled. Install them manually from the store.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get tooltipLoveAll => 'Tout aimer';
|
||||
|
||||
@@ -3228,6 +3257,17 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get downloadNetworkCompatibilityModeDisabled =>
|
||||
'Utilisation des paramètres réseau par défaut';
|
||||
|
||||
@override
|
||||
String get downloadAllowLocalNetwork => 'Allow Local Network Access';
|
||||
|
||||
@override
|
||||
String get downloadAllowLocalNetworkEnabled =>
|
||||
'Requests to local/private addresses are allowed (for local proxy or custom DNS)';
|
||||
|
||||
@override
|
||||
String get downloadAllowLocalNetworkDisabled =>
|
||||
'Local/private addresses are blocked for security';
|
||||
|
||||
@override
|
||||
String get downloadSelectServiceToEnable =>
|
||||
'Select a provider with quality options to enable this option';
|
||||
|
||||
@@ -3014,6 +3014,35 @@ class AppLocalizationsHi extends AppLocalizations {
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String backupContentsExtensions(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count extensions',
|
||||
one: '1 extension',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get backupIncludeSecrets => 'Include extension credentials';
|
||||
|
||||
@override
|
||||
String get backupIncludeSecretsDescription =>
|
||||
'Tokens and API keys from extensions will be saved into the backup file. Keep the file private. When off, you re-enter them after restoring.';
|
||||
|
||||
@override
|
||||
String backupExtensionsRestoreFailed(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'extensions',
|
||||
one: 'extension',
|
||||
);
|
||||
return '$count $_temp0 could not be reinstalled. Install them manually from the store.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get tooltipLoveAll => 'Love All';
|
||||
|
||||
@@ -3143,6 +3172,17 @@ class AppLocalizationsHi extends AppLocalizations {
|
||||
String get downloadNetworkCompatibilityModeDisabled =>
|
||||
'Using standard network settings';
|
||||
|
||||
@override
|
||||
String get downloadAllowLocalNetwork => 'Allow Local Network Access';
|
||||
|
||||
@override
|
||||
String get downloadAllowLocalNetworkEnabled =>
|
||||
'Requests to local/private addresses are allowed (for local proxy or custom DNS)';
|
||||
|
||||
@override
|
||||
String get downloadAllowLocalNetworkDisabled =>
|
||||
'Local/private addresses are blocked for security';
|
||||
|
||||
@override
|
||||
String get downloadSelectServiceToEnable =>
|
||||
'Select a provider with quality options to enable this option';
|
||||
|
||||
@@ -3004,6 +3004,29 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String backupContentsExtensions(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count extension',
|
||||
one: '1 extension',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get backupIncludeSecrets => 'Sertakan kredensial extension';
|
||||
|
||||
@override
|
||||
String get backupIncludeSecretsDescription =>
|
||||
'Token dan API key dari extension akan ikut disimpan ke file cadangan. Jaga kerahasiaan file-nya. Jika dimatikan, kamu perlu memasukkannya lagi setelah pemulihan.';
|
||||
|
||||
@override
|
||||
String backupExtensionsRestoreFailed(int count) {
|
||||
return '$count extension gagal dipasang ulang. Pasang manual dari store.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get tooltipLoveAll => 'Love All';
|
||||
|
||||
@@ -3133,6 +3156,17 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
String get downloadNetworkCompatibilityModeDisabled =>
|
||||
'Using standard network settings';
|
||||
|
||||
@override
|
||||
String get downloadAllowLocalNetwork => 'Izinkan Akses Jaringan Lokal';
|
||||
|
||||
@override
|
||||
String get downloadAllowLocalNetworkEnabled =>
|
||||
'Permintaan ke alamat lokal/privat diizinkan (untuk proxy lokal atau DNS kustom)';
|
||||
|
||||
@override
|
||||
String get downloadAllowLocalNetworkDisabled =>
|
||||
'Alamat lokal/privat diblokir demi keamanan';
|
||||
|
||||
@override
|
||||
String get downloadSelectServiceToEnable =>
|
||||
'Select a provider with quality options to enable this option';
|
||||
|
||||
@@ -3002,6 +3002,35 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String backupContentsExtensions(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count extensions',
|
||||
one: '1 extension',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get backupIncludeSecrets => 'Include extension credentials';
|
||||
|
||||
@override
|
||||
String get backupIncludeSecretsDescription =>
|
||||
'Tokens and API keys from extensions will be saved into the backup file. Keep the file private. When off, you re-enter them after restoring.';
|
||||
|
||||
@override
|
||||
String backupExtensionsRestoreFailed(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'extensions',
|
||||
one: 'extension',
|
||||
);
|
||||
return '$count $_temp0 could not be reinstalled. Install them manually from the store.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get tooltipLoveAll => 'Love All';
|
||||
|
||||
@@ -3131,6 +3160,17 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get downloadNetworkCompatibilityModeDisabled =>
|
||||
'Using standard network settings';
|
||||
|
||||
@override
|
||||
String get downloadAllowLocalNetwork => 'Allow Local Network Access';
|
||||
|
||||
@override
|
||||
String get downloadAllowLocalNetworkEnabled =>
|
||||
'Requests to local/private addresses are allowed (for local proxy or custom DNS)';
|
||||
|
||||
@override
|
||||
String get downloadAllowLocalNetworkDisabled =>
|
||||
'Local/private addresses are blocked for security';
|
||||
|
||||
@override
|
||||
String get downloadSelectServiceToEnable =>
|
||||
'Select a provider with quality options to enable this option';
|
||||
|
||||
@@ -2999,6 +2999,35 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String backupContentsExtensions(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count extensions',
|
||||
one: '1 extension',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get backupIncludeSecrets => 'Include extension credentials';
|
||||
|
||||
@override
|
||||
String get backupIncludeSecretsDescription =>
|
||||
'Tokens and API keys from extensions will be saved into the backup file. Keep the file private. When off, you re-enter them after restoring.';
|
||||
|
||||
@override
|
||||
String backupExtensionsRestoreFailed(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'extensions',
|
||||
one: 'extension',
|
||||
);
|
||||
return '$count $_temp0 could not be reinstalled. Install them manually from the store.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get tooltipLoveAll => 'Love All';
|
||||
|
||||
@@ -3128,6 +3157,17 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
String get downloadNetworkCompatibilityModeDisabled =>
|
||||
'Using standard network settings';
|
||||
|
||||
@override
|
||||
String get downloadAllowLocalNetwork => 'Allow Local Network Access';
|
||||
|
||||
@override
|
||||
String get downloadAllowLocalNetworkEnabled =>
|
||||
'Requests to local/private addresses are allowed (for local proxy or custom DNS)';
|
||||
|
||||
@override
|
||||
String get downloadAllowLocalNetworkDisabled =>
|
||||
'Local/private addresses are blocked for security';
|
||||
|
||||
@override
|
||||
String get downloadSelectServiceToEnable =>
|
||||
'Select a provider with quality options to enable this option';
|
||||
|
||||
@@ -3014,6 +3014,35 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String backupContentsExtensions(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count extensions',
|
||||
one: '1 extension',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get backupIncludeSecrets => 'Include extension credentials';
|
||||
|
||||
@override
|
||||
String get backupIncludeSecretsDescription =>
|
||||
'Tokens and API keys from extensions will be saved into the backup file. Keep the file private. When off, you re-enter them after restoring.';
|
||||
|
||||
@override
|
||||
String backupExtensionsRestoreFailed(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'extensions',
|
||||
one: 'extension',
|
||||
);
|
||||
return '$count $_temp0 could not be reinstalled. Install them manually from the store.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get tooltipLoveAll => 'Love All';
|
||||
|
||||
@@ -3143,6 +3172,17 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get downloadNetworkCompatibilityModeDisabled =>
|
||||
'Using standard network settings';
|
||||
|
||||
@override
|
||||
String get downloadAllowLocalNetwork => 'Allow Local Network Access';
|
||||
|
||||
@override
|
||||
String get downloadAllowLocalNetworkEnabled =>
|
||||
'Requests to local/private addresses are allowed (for local proxy or custom DNS)';
|
||||
|
||||
@override
|
||||
String get downloadAllowLocalNetworkDisabled =>
|
||||
'Local/private addresses are blocked for security';
|
||||
|
||||
@override
|
||||
String get downloadSelectServiceToEnable =>
|
||||
'Select a provider with quality options to enable this option';
|
||||
|
||||
@@ -3014,6 +3014,35 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String backupContentsExtensions(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count extensions',
|
||||
one: '1 extension',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get backupIncludeSecrets => 'Include extension credentials';
|
||||
|
||||
@override
|
||||
String get backupIncludeSecretsDescription =>
|
||||
'Tokens and API keys from extensions will be saved into the backup file. Keep the file private. When off, you re-enter them after restoring.';
|
||||
|
||||
@override
|
||||
String backupExtensionsRestoreFailed(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'extensions',
|
||||
one: 'extension',
|
||||
);
|
||||
return '$count $_temp0 could not be reinstalled. Install them manually from the store.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get tooltipLoveAll => 'Love All';
|
||||
|
||||
@@ -3143,6 +3172,17 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
String get downloadNetworkCompatibilityModeDisabled =>
|
||||
'Using standard network settings';
|
||||
|
||||
@override
|
||||
String get downloadAllowLocalNetwork => 'Allow Local Network Access';
|
||||
|
||||
@override
|
||||
String get downloadAllowLocalNetworkEnabled =>
|
||||
'Requests to local/private addresses are allowed (for local proxy or custom DNS)';
|
||||
|
||||
@override
|
||||
String get downloadAllowLocalNetworkDisabled =>
|
||||
'Local/private addresses are blocked for security';
|
||||
|
||||
@override
|
||||
String get downloadSelectServiceToEnable =>
|
||||
'Select a provider with quality options to enable this option';
|
||||
|
||||
@@ -3069,6 +3069,35 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String backupContentsExtensions(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count extensions',
|
||||
one: '1 extension',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get backupIncludeSecrets => 'Include extension credentials';
|
||||
|
||||
@override
|
||||
String get backupIncludeSecretsDescription =>
|
||||
'Tokens and API keys from extensions will be saved into the backup file. Keep the file private. When off, you re-enter them after restoring.';
|
||||
|
||||
@override
|
||||
String backupExtensionsRestoreFailed(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'extensions',
|
||||
one: 'extension',
|
||||
);
|
||||
return '$count $_temp0 could not be reinstalled. Install them manually from the store.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get tooltipLoveAll => 'Love All';
|
||||
|
||||
@@ -3198,6 +3227,17 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
String get downloadNetworkCompatibilityModeDisabled =>
|
||||
'Using standard network settings';
|
||||
|
||||
@override
|
||||
String get downloadAllowLocalNetwork => 'Allow Local Network Access';
|
||||
|
||||
@override
|
||||
String get downloadAllowLocalNetworkEnabled =>
|
||||
'Requests to local/private addresses are allowed (for local proxy or custom DNS)';
|
||||
|
||||
@override
|
||||
String get downloadAllowLocalNetworkDisabled =>
|
||||
'Local/private addresses are blocked for security';
|
||||
|
||||
@override
|
||||
String get downloadSelectServiceToEnable =>
|
||||
'Select a provider with quality options to enable this option';
|
||||
|
||||
@@ -3043,6 +3043,35 @@ class AppLocalizationsTr extends AppLocalizations {
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String backupContentsExtensions(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count extensions',
|
||||
one: '1 extension',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get backupIncludeSecrets => 'Include extension credentials';
|
||||
|
||||
@override
|
||||
String get backupIncludeSecretsDescription =>
|
||||
'Tokens and API keys from extensions will be saved into the backup file. Keep the file private. When off, you re-enter them after restoring.';
|
||||
|
||||
@override
|
||||
String backupExtensionsRestoreFailed(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'extensions',
|
||||
one: 'extension',
|
||||
);
|
||||
return '$count $_temp0 could not be reinstalled. Install them manually from the store.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get tooltipLoveAll => 'Love All';
|
||||
|
||||
@@ -3175,6 +3204,17 @@ class AppLocalizationsTr extends AppLocalizations {
|
||||
String get downloadNetworkCompatibilityModeDisabled =>
|
||||
'Using standard network settings';
|
||||
|
||||
@override
|
||||
String get downloadAllowLocalNetwork => 'Allow Local Network Access';
|
||||
|
||||
@override
|
||||
String get downloadAllowLocalNetworkEnabled =>
|
||||
'Requests to local/private addresses are allowed (for local proxy or custom DNS)';
|
||||
|
||||
@override
|
||||
String get downloadAllowLocalNetworkDisabled =>
|
||||
'Local/private addresses are blocked for security';
|
||||
|
||||
@override
|
||||
String get downloadSelectServiceToEnable =>
|
||||
'Select a provider with quality options to enable this option';
|
||||
|
||||
@@ -3058,6 +3058,35 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String backupContentsExtensions(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count extensions',
|
||||
one: '1 extension',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get backupIncludeSecrets => 'Include extension credentials';
|
||||
|
||||
@override
|
||||
String get backupIncludeSecretsDescription =>
|
||||
'Tokens and API keys from extensions will be saved into the backup file. Keep the file private. When off, you re-enter them after restoring.';
|
||||
|
||||
@override
|
||||
String backupExtensionsRestoreFailed(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'extensions',
|
||||
one: 'extension',
|
||||
);
|
||||
return '$count $_temp0 could not be reinstalled. Install them manually from the store.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get tooltipLoveAll => 'Уподобати всіх';
|
||||
|
||||
@@ -3190,6 +3219,17 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
String get downloadNetworkCompatibilityModeDisabled =>
|
||||
'Using standard network settings';
|
||||
|
||||
@override
|
||||
String get downloadAllowLocalNetwork => 'Allow Local Network Access';
|
||||
|
||||
@override
|
||||
String get downloadAllowLocalNetworkEnabled =>
|
||||
'Requests to local/private addresses are allowed (for local proxy or custom DNS)';
|
||||
|
||||
@override
|
||||
String get downloadAllowLocalNetworkDisabled =>
|
||||
'Local/private addresses are blocked for security';
|
||||
|
||||
@override
|
||||
String get downloadSelectServiceToEnable =>
|
||||
'Select a provider with quality options to enable this option';
|
||||
|
||||
@@ -3014,6 +3014,35 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String backupContentsExtensions(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count extensions',
|
||||
one: '1 extension',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get backupIncludeSecrets => 'Include extension credentials';
|
||||
|
||||
@override
|
||||
String get backupIncludeSecretsDescription =>
|
||||
'Tokens and API keys from extensions will be saved into the backup file. Keep the file private. When off, you re-enter them after restoring.';
|
||||
|
||||
@override
|
||||
String backupExtensionsRestoreFailed(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'extensions',
|
||||
one: 'extension',
|
||||
);
|
||||
return '$count $_temp0 could not be reinstalled. Install them manually from the store.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get tooltipLoveAll => 'Love All';
|
||||
|
||||
@@ -3143,6 +3172,17 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get downloadNetworkCompatibilityModeDisabled =>
|
||||
'Using standard network settings';
|
||||
|
||||
@override
|
||||
String get downloadAllowLocalNetwork => 'Allow Local Network Access';
|
||||
|
||||
@override
|
||||
String get downloadAllowLocalNetworkEnabled =>
|
||||
'Requests to local/private addresses are allowed (for local proxy or custom DNS)';
|
||||
|
||||
@override
|
||||
String get downloadAllowLocalNetworkDisabled =>
|
||||
'Local/private addresses are blocked for security';
|
||||
|
||||
@override
|
||||
String get downloadSelectServiceToEnable =>
|
||||
'Select a provider with quality options to enable this option';
|
||||
|
||||
@@ -3964,6 +3964,32 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"backupContentsExtensions": "{count, plural, =1{1 extension} other{{count} extensions}}",
|
||||
"@backupContentsExtensions": {
|
||||
"description": "Backup contents row for installed extensions count",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"backupIncludeSecrets": "Include extension credentials",
|
||||
"@backupIncludeSecrets": {
|
||||
"description": "Toggle to include secret extension settings (tokens, API keys) in the backup"
|
||||
},
|
||||
"backupIncludeSecretsDescription": "Tokens and API keys from extensions will be saved into the backup file. Keep the file private. When off, you re-enter them after restoring.",
|
||||
"@backupIncludeSecretsDescription": {
|
||||
"description": "Explanation for the include-credentials toggle"
|
||||
},
|
||||
"backupExtensionsRestoreFailed": "{count} {count, plural, =1{extension} other{extensions}} could not be reinstalled. Install them manually from the store.",
|
||||
"@backupExtensionsRestoreFailed": {
|
||||
"description": "Snackbar/hint when some extensions failed to reinstall during restore",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tooltipLoveAll": "Love All",
|
||||
"@tooltipLoveAll": {
|
||||
"description": "Tooltip for the Love All button on album/playlist screens"
|
||||
@@ -4120,6 +4146,18 @@
|
||||
"@downloadNetworkCompatibilityModeDisabled": {
|
||||
"description": "Subtitle when network compatibility mode is off"
|
||||
},
|
||||
"downloadAllowLocalNetwork": "Allow Local Network Access",
|
||||
"@downloadAllowLocalNetwork": {
|
||||
"description": "Setting title for allowing requests to private/local network targets"
|
||||
},
|
||||
"downloadAllowLocalNetworkEnabled": "Requests to local/private addresses are allowed (for local proxy or custom DNS)",
|
||||
"@downloadAllowLocalNetworkEnabled": {
|
||||
"description": "Subtitle when allow local network access is on"
|
||||
},
|
||||
"downloadAllowLocalNetworkDisabled": "Local/private addresses are blocked for security",
|
||||
"@downloadAllowLocalNetworkDisabled": {
|
||||
"description": "Subtitle when allow local network access is off"
|
||||
},
|
||||
"downloadSelectServiceToEnable": "Select a provider with quality options to enable this option",
|
||||
"@downloadSelectServiceToEnable": {
|
||||
"description": "Subtitle when quality picker is disabled due to extension service"
|
||||
|
||||
@@ -3752,6 +3752,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"backupContentsExtensions": "{count, plural, =1{1 extension} other{{count} extension}}",
|
||||
"@backupContentsExtensions": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"backupIncludeSecrets": "Sertakan kredensial extension",
|
||||
"backupIncludeSecretsDescription": "Token dan API key dari extension akan ikut disimpan ke file cadangan. Jaga kerahasiaan file-nya. Jika dimatikan, kamu perlu memasukkannya lagi setelah pemulihan.",
|
||||
"backupExtensionsRestoreFailed": "{count} extension gagal dipasang ulang. Pasang manual dari store.",
|
||||
"@backupExtensionsRestoreFailed": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tooltipLoveAll": "Love All",
|
||||
"@tooltipLoveAll": {
|
||||
"description": "Tooltip for the Love All button on album/playlist screens"
|
||||
@@ -3892,6 +3910,18 @@
|
||||
"@downloadNetworkCompatibilityModeDisabled": {
|
||||
"description": "Subtitle when network compatibility mode is disabled"
|
||||
},
|
||||
"downloadAllowLocalNetwork": "Izinkan Akses Jaringan Lokal",
|
||||
"@downloadAllowLocalNetwork": {
|
||||
"description": "Setting title for allowing requests to private/local network targets"
|
||||
},
|
||||
"downloadAllowLocalNetworkEnabled": "Permintaan ke alamat lokal/privat diizinkan (untuk proxy lokal atau DNS kustom)",
|
||||
"@downloadAllowLocalNetworkEnabled": {
|
||||
"description": "Subtitle when allow local network access is on"
|
||||
},
|
||||
"downloadAllowLocalNetworkDisabled": "Alamat lokal/privat diblokir demi keamanan",
|
||||
"@downloadAllowLocalNetworkDisabled": {
|
||||
"description": "Subtitle when allow local network access is off"
|
||||
},
|
||||
"downloadSelectServiceToEnable": "Select a provider with quality options to enable this option",
|
||||
"@downloadSelectServiceToEnable": {
|
||||
"description": "Hint shown instead of Ask-quality subtitle when selected provider has no quality options"
|
||||
|
||||
@@ -14,6 +14,22 @@ final _log = AppLogger('ExtensionProvider');
|
||||
const _metadataProviderPriorityKey = 'metadata_provider_priority';
|
||||
const _providerPriorityKey = 'provider_priority';
|
||||
const _spotifyWebExtensionId = 'spotify-web';
|
||||
const _storeRegistryUrlPrefKey = 'store_registry_url';
|
||||
|
||||
/// Result of restoring extensions from a backup.
|
||||
class ExtensionRestoreResult {
|
||||
final int installed;
|
||||
final int alreadyPresent;
|
||||
final int failed;
|
||||
final List<String> failedIds;
|
||||
|
||||
const ExtensionRestoreResult({
|
||||
this.installed = 0,
|
||||
this.alreadyPresent = 0,
|
||||
this.failed = 0,
|
||||
this.failedIds = const [],
|
||||
});
|
||||
}
|
||||
|
||||
bool _stringListEquals(List<String> a, List<String> b) {
|
||||
if (identical(a, b)) return true;
|
||||
@@ -1722,6 +1738,205 @@ class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
.where((ext) => ext.enabled && ext.hasCustomSearch)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// Collects the keys flagged as `secret` in an extension's manifest schema
|
||||
/// (top-level settings and quality-specific settings).
|
||||
Set<String> _secretKeysFromManifest(Map<String, dynamic> raw) {
|
||||
final keys = <String>{};
|
||||
|
||||
void scan(Object? settingsList) {
|
||||
if (settingsList is! List) return;
|
||||
for (final entry in settingsList) {
|
||||
if (entry is Map &&
|
||||
entry['secret'] == true &&
|
||||
entry['key'] is String) {
|
||||
keys.add(entry['key'] as String);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scan(raw['settings']);
|
||||
final quality = raw['quality_options'];
|
||||
if (quality is List) {
|
||||
for (final option in quality) {
|
||||
if (option is Map) {
|
||||
scan(option['settings']);
|
||||
}
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
/// Builds the extensions section of a backup: the store registry URL plus the
|
||||
/// installed extensions with their id, version, enabled flag and settings.
|
||||
/// Secret-flagged settings (tokens, API keys) are only included when
|
||||
/// [includeSecrets] is true.
|
||||
Future<Map<String, dynamic>> exportBackup({
|
||||
required bool includeSecrets,
|
||||
}) async {
|
||||
if (!PlatformBridge.supportsExtensionSystem) {
|
||||
return {'registry_url': '', 'items': const <Map<String, dynamic>>[]};
|
||||
}
|
||||
|
||||
String registryUrl = '';
|
||||
try {
|
||||
registryUrl = await PlatformBridge.getStoreRegistryUrl();
|
||||
} catch (_) {}
|
||||
|
||||
List<Map<String, dynamic>> installed;
|
||||
try {
|
||||
installed = await PlatformBridge.getInstalledExtensions();
|
||||
} catch (e) {
|
||||
_log.w('Backup: failed to list extensions: $e');
|
||||
installed = const [];
|
||||
}
|
||||
|
||||
final items = <Map<String, dynamic>>[];
|
||||
for (final raw in installed) {
|
||||
final id = raw['id'] as String?;
|
||||
if (id == null || id.isEmpty) continue;
|
||||
final secretKeys = _secretKeysFromManifest(raw);
|
||||
|
||||
Map<String, dynamic> settings = {};
|
||||
try {
|
||||
settings = await PlatformBridge.getExtensionSettings(id);
|
||||
} catch (_) {}
|
||||
|
||||
final filtered = <String, dynamic>{};
|
||||
var omittedSecret = false;
|
||||
settings.forEach((key, value) {
|
||||
if (secretKeys.contains(key)) {
|
||||
if (!includeSecrets) {
|
||||
omittedSecret = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
filtered[key] = value;
|
||||
});
|
||||
|
||||
items.add({
|
||||
'id': id,
|
||||
'version': raw['version']?.toString() ?? '',
|
||||
'enabled': raw['enabled'] == true,
|
||||
'settings': filtered,
|
||||
if (omittedSecret) 'secrets_omitted': true,
|
||||
});
|
||||
}
|
||||
|
||||
return {'registry_url': registryUrl, 'items': items};
|
||||
}
|
||||
|
||||
/// Restores extensions from a backup section produced by [exportBackup]:
|
||||
/// re-applies the store registry URL, reinstalls each extension from the
|
||||
/// store when missing, then merges settings and restores the enabled flag.
|
||||
/// Missing settings (e.g. omitted secrets) are merged with the current values
|
||||
/// so they are not wiped.
|
||||
Future<ExtensionRestoreResult> restoreFromBackup(
|
||||
Map<String, dynamic> data,
|
||||
) async {
|
||||
if (!PlatformBridge.supportsExtensionSystem) {
|
||||
return const ExtensionRestoreResult();
|
||||
}
|
||||
|
||||
final registryUrl = (data['registry_url'] as String?)?.trim() ?? '';
|
||||
final itemsRaw = data['items'];
|
||||
final items = itemsRaw is List
|
||||
? itemsRaw
|
||||
.whereType<Map<Object?, Object?>>()
|
||||
.map((e) => Map<String, dynamic>.from(e))
|
||||
.toList()
|
||||
: <Map<String, dynamic>>[];
|
||||
|
||||
Directory? destDir;
|
||||
try {
|
||||
final tmp = await getTemporaryDirectory();
|
||||
destDir = await Directory(
|
||||
'${tmp.path}/spotiflac_restore_ext',
|
||||
).create(recursive: true);
|
||||
await PlatformBridge.initExtensionStore(destDir.path);
|
||||
if (registryUrl.isNotEmpty) {
|
||||
await PlatformBridge.setStoreRegistryUrl(registryUrl);
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_storeRegistryUrlPrefKey, registryUrl);
|
||||
}
|
||||
} catch (e) {
|
||||
_log.w('Restore: failed to prepare extension store: $e');
|
||||
}
|
||||
|
||||
await refreshExtensions();
|
||||
final installedIds = state.extensions
|
||||
.map((e) => e.id.toLowerCase())
|
||||
.toSet();
|
||||
|
||||
var installedCount = 0;
|
||||
var alreadyPresent = 0;
|
||||
var failed = 0;
|
||||
final failedIds = <String>[];
|
||||
|
||||
for (final item in items) {
|
||||
final id = item['id'] as String?;
|
||||
if (id == null || id.isEmpty) continue;
|
||||
final enabled = item['enabled'] != false;
|
||||
var present = installedIds.contains(id.toLowerCase());
|
||||
|
||||
if (!present) {
|
||||
if (destDir == null) {
|
||||
failed++;
|
||||
failedIds.add(id);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
final path = await PlatformBridge.downloadStoreExtension(
|
||||
id,
|
||||
destDir.path,
|
||||
);
|
||||
final ok = await installExtension(path);
|
||||
if (ok) {
|
||||
installedCount++;
|
||||
present = true;
|
||||
} else {
|
||||
failed++;
|
||||
failedIds.add(id);
|
||||
}
|
||||
} catch (e) {
|
||||
_log.w('Restore: failed to install extension $id: $e');
|
||||
failed++;
|
||||
failedIds.add(id);
|
||||
}
|
||||
} else {
|
||||
alreadyPresent++;
|
||||
}
|
||||
|
||||
if (!present) continue;
|
||||
|
||||
final settings = item['settings'];
|
||||
if (settings is Map && settings.isNotEmpty) {
|
||||
try {
|
||||
final current = await PlatformBridge.getExtensionSettings(id);
|
||||
final merged = <String, dynamic>{
|
||||
...current,
|
||||
...Map<String, dynamic>.from(settings),
|
||||
};
|
||||
await PlatformBridge.setExtensionSettings(id, merged);
|
||||
} catch (e) {
|
||||
_log.w('Restore: failed to apply settings for $id: $e');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await setExtensionEnabled(id, enabled);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
await refreshExtensions();
|
||||
|
||||
return ExtensionRestoreResult(
|
||||
installed: installedCount,
|
||||
alreadyPresent: alreadyPresent,
|
||||
failed: failed,
|
||||
failedIds: failedIds,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final extensionProvider = NotifierProvider<ExtensionNotifier, ExtensionState>(
|
||||
|
||||
@@ -6,6 +6,7 @@ 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/extension_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';
|
||||
@@ -25,6 +26,7 @@ class _BackupRestorePageState extends ConsumerState<BackupRestorePage> {
|
||||
|
||||
bool _isExporting = false;
|
||||
bool _isImporting = false;
|
||||
bool _includeSecrets = false;
|
||||
|
||||
bool get _isBusy => _isExporting || _isImporting;
|
||||
|
||||
@@ -41,12 +43,16 @@ class _BackupRestorePageState extends ConsumerState<BackupRestorePage> {
|
||||
);
|
||||
final collections = await collectionsNotifier.exportCollections();
|
||||
final covers = await collectionsNotifier.exportPlaylistCovers();
|
||||
final extensions = await ref
|
||||
.read(extensionProvider.notifier)
|
||||
.exportBackup(includeSecrets: _includeSecrets);
|
||||
|
||||
final envelope = BackupService.buildEnvelope(
|
||||
settings: settings,
|
||||
history: history,
|
||||
collections: collections,
|
||||
playlistCovers: covers,
|
||||
extensions: extensions,
|
||||
);
|
||||
|
||||
final file = await BackupService.writeBackupFile(envelope);
|
||||
@@ -117,10 +123,26 @@ class _BackupRestorePageState extends ConsumerState<BackupRestorePage> {
|
||||
coverImages: bundle.playlistCovers,
|
||||
);
|
||||
|
||||
ExtensionRestoreResult? extResult;
|
||||
if (bundle.hasExtensions) {
|
||||
extResult = await ref
|
||||
.read(extensionProvider.notifier)
|
||||
.restoreFromBackup(bundle.extensions);
|
||||
}
|
||||
|
||||
final message = StringBuffer(l10n.backupRestored)
|
||||
..write('\n')
|
||||
..write(l10n.backupRestoreRestartHint);
|
||||
if (extResult != null && extResult.failed > 0) {
|
||||
message
|
||||
..write('\n')
|
||||
..write(l10n.backupExtensionsRestoreFailed(extResult.failed));
|
||||
}
|
||||
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('${l10n.backupRestored}\n${l10n.backupRestoreRestartHint}'),
|
||||
duration: const Duration(seconds: 5),
|
||||
content: Text(message.toString()),
|
||||
duration: const Duration(seconds: 6),
|
||||
),
|
||||
);
|
||||
} catch (e, stack) {
|
||||
@@ -182,6 +204,13 @@ class _BackupRestorePageState extends ConsumerState<BackupRestorePage> {
|
||||
bundle.favoriteArtistCount,
|
||||
),
|
||||
),
|
||||
if (bundle.extensionCount > 0)
|
||||
_ContentRow(
|
||||
icon: Icons.extension_outlined,
|
||||
label: l10n.backupContentsExtensions(
|
||||
bundle.extensionCount,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
@@ -259,6 +288,20 @@ class _BackupRestorePageState extends ConsumerState<BackupRestorePage> {
|
||||
buttonIcon: Icons.ios_share,
|
||||
isBusy: _isExporting,
|
||||
onPressed: _isBusy ? null : _createBackup,
|
||||
extra: Padding(
|
||||
padding: const EdgeInsets.only(top: 4, bottom: 4),
|
||||
child: SwitchListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
value: _includeSecrets,
|
||||
onChanged: _isBusy
|
||||
? null
|
||||
: (value) =>
|
||||
setState(() => _includeSecrets = value),
|
||||
title: Text(l10n.backupIncludeSecrets),
|
||||
subtitle: Text(l10n.backupIncludeSecretsDescription),
|
||||
isThreeLine: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_ActionCard(
|
||||
@@ -288,6 +331,7 @@ class _ActionCard extends StatelessWidget {
|
||||
final IconData buttonIcon;
|
||||
final bool isBusy;
|
||||
final VoidCallback? onPressed;
|
||||
final Widget? extra;
|
||||
|
||||
const _ActionCard({
|
||||
required this.icon,
|
||||
@@ -297,6 +341,7 @@ class _ActionCard extends StatelessWidget {
|
||||
required this.buttonIcon,
|
||||
required this.isBusy,
|
||||
required this.onPressed,
|
||||
this.extra,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -333,8 +378,8 @@ class _ActionCard extends StatelessWidget {
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
FilledButton.icon(
|
||||
?extra,
|
||||
const SizedBox(height: 16), FilledButton.icon(
|
||||
onPressed: onPressed,
|
||||
icon: isBusy
|
||||
? const SizedBox(
|
||||
|
||||
@@ -25,6 +25,9 @@ class BackupBundle {
|
||||
/// Playlist cover images keyed by playlist id: `{ id: { ext, data } }`.
|
||||
final Map<String, dynamic> playlistCovers;
|
||||
|
||||
/// Extensions section: `{ registry_url, items: [ {id, version, enabled, settings} ] }`.
|
||||
final Map<String, dynamic> extensions;
|
||||
|
||||
const BackupBundle({
|
||||
required this.formatVersion,
|
||||
required this.appVersion,
|
||||
@@ -33,6 +36,7 @@ class BackupBundle {
|
||||
required this.history,
|
||||
required this.collections,
|
||||
required this.playlistCovers,
|
||||
required this.extensions,
|
||||
});
|
||||
|
||||
bool get hasSettings => settings != null && settings!.isNotEmpty;
|
||||
@@ -49,13 +53,21 @@ class BackupBundle {
|
||||
int get playlistCount => _collectionListCount('playlists');
|
||||
int get favoriteArtistCount => _collectionListCount('favoriteArtists');
|
||||
|
||||
int get extensionCount {
|
||||
final items = extensions['items'];
|
||||
return items is List ? items.length : 0;
|
||||
}
|
||||
|
||||
bool get hasExtensions => extensionCount > 0;
|
||||
|
||||
bool get isEmpty =>
|
||||
!hasSettings &&
|
||||
historyCount == 0 &&
|
||||
likedCount == 0 &&
|
||||
wishlistCount == 0 &&
|
||||
playlistCount == 0 &&
|
||||
favoriteArtistCount == 0;
|
||||
favoriteArtistCount == 0 &&
|
||||
extensionCount == 0;
|
||||
}
|
||||
|
||||
/// Builds and parses SpotiFLAC backup files (a single JSON document containing
|
||||
@@ -73,6 +85,7 @@ class BackupService {
|
||||
required List<Map<String, dynamic>> history,
|
||||
required Map<String, dynamic> collections,
|
||||
required Map<String, dynamic> playlistCovers,
|
||||
required Map<String, dynamic> extensions,
|
||||
}) {
|
||||
return {
|
||||
'magic': magic,
|
||||
@@ -85,6 +98,7 @@ class BackupService {
|
||||
'history': history,
|
||||
'collections': collections,
|
||||
'playlist_covers': playlistCovers,
|
||||
'extensions': extensions,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -165,6 +179,11 @@ class BackupService {
|
||||
? Map<String, dynamic>.from(coversRaw)
|
||||
: <String, dynamic>{};
|
||||
|
||||
final extensionsRaw = data['extensions'];
|
||||
final extensions = extensionsRaw is Map
|
||||
? Map<String, dynamic>.from(extensionsRaw)
|
||||
: <String, dynamic>{};
|
||||
|
||||
return BackupBundle(
|
||||
formatVersion: (root['format_version'] as num?)?.toInt() ?? 1,
|
||||
appVersion: root['app_version'] as String? ?? '',
|
||||
@@ -173,6 +192,7 @@ class BackupService {
|
||||
history: history,
|
||||
collections: collections,
|
||||
playlistCovers: playlistCovers,
|
||||
extensions: extensions,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user