From 4792f5aac82885c1e0f6b6c4fefea8f26cf73934 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 22 Aug 2026 23:13:01 +0200 Subject: [PATCH] Improve: Backup screen clearer and easier to understand --- CHANGELOG.md | 1 + .../generated/app_localizations.dart | 116 +++++++- .../generated/app_localizations_de.dart | 72 ++++- .../generated/app_localizations_en.dart | 70 ++++- lib/src/localization/translations | 2 +- .../components/cloud_backup_promo.comp.dart | 106 +++++-- .../visual/views/memories/memories.view.dart | 10 +- .../settings/backup/backup_settings.view.dart | 281 +++++++++++------- .../settings/backup/backup_setup.view.dart | 4 +- .../views/settings/backup/backup_utils.dart | 111 +++++++ .../backup/components/recovery_card.comp.dart | 87 ++++++ .../backup/memories_backup_detail.view.dart | 105 +------ 12 files changed, 705 insertions(+), 260 deletions(-) create mode 100644 lib/src/visual/views/settings/backup/backup_utils.dart create mode 100644 lib/src/visual/views/settings/backup/components/recovery_card.comp.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 8927671f..ab405fd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 0.5.1 - Improve: Show delivery and read receipt indicators for text messages +- Improve: Backup screen clearer and easier to understand - Fix: Background audio correctly pauses and resumes when viewing videos ## 0.5.0 diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index 69d947a8..1f0b67fc 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -1601,7 +1601,7 @@ abstract class AppLocalizations { /// No description provided for @backupSelectStrongPassword. /// /// In en, this message translates to: - /// **'Choose a secure password. This is required if you want to restore your backup.'** + /// **'Select a secure password. It is recommended to generate and store it via your password manager of choice.'** String get backupSelectStrongPassword; /// No description provided for @password. @@ -1631,7 +1631,7 @@ abstract class AppLocalizations { /// No description provided for @backupEnableBackup. /// /// In en, this message translates to: - /// **'Activate automatic backup'** + /// **'Enable Password Backup'** String get backupEnableBackup; /// No description provided for @backupTwonlySaveNow. @@ -3713,7 +3713,7 @@ abstract class AppLocalizations { /// No description provided for @passwordlessRecovery. /// /// In en, this message translates to: - /// **'Password Recovery'** + /// **'Trusted Friends'** String get passwordlessRecovery; /// No description provided for @passwordlessRecoveryNotConfigured. @@ -3833,7 +3833,7 @@ abstract class AppLocalizations { /// No description provided for @passwordlessRecoveryEnableSuccess. /// /// In en, this message translates to: - /// **'Passwordless recovery successfully enabled!'** + /// **'Trusted Friends successfully set up!'** String get passwordlessRecoveryEnableSuccess; /// No description provided for @passwordlessRecoveryEnterPin. @@ -3851,19 +3851,19 @@ abstract class AppLocalizations { /// No description provided for @passwordlessRecoveryEnableBtn. /// /// In en, this message translates to: - /// **'Enable Passwordless Recovery'** + /// **'Enable Trusted Friends'** String get passwordlessRecoveryEnableBtn; /// No description provided for @passwordlessRecoveryRecoverBtn. /// /// In en, this message translates to: - /// **'Recover passwordless'** + /// **'Recover via Trusted Friends'** String get passwordlessRecoveryRecoverBtn; /// No description provided for @passwordlessRecoveryModifyBtn. /// /// In en, this message translates to: - /// **'Modify Passwordless Recovery'** + /// **'Modify Trusted Friends'** String get passwordlessRecoveryModifyBtn; /// No description provided for @passwordlessRecoveryStatusEnabled. @@ -4435,6 +4435,108 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Dark'** String get themeDark; + + /// No description provided for @backupRecoverySectionTitle. + /// + /// In en, this message translates to: + /// **'Secure Account Access'** + String get backupRecoverySectionTitle; + + /// No description provided for @backupRecoverySectionDescNone. + /// + /// In en, this message translates to: + /// **'You must select at least one recovery option, as otherwise no one can help you if you lose access to your device.'** + String get backupRecoverySectionDescNone; + + /// No description provided for @backupRecoverySectionDescSome. + /// + /// In en, this message translates to: + /// **'You can recover your account with the recovery options below. It is recommended to enable both options.'** + String get backupRecoverySectionDescSome; + + /// No description provided for @backupRecoveryOptionAFriends. + /// + /// In en, this message translates to: + /// **'Via trusted friends'** + String get backupRecoveryOptionAFriends; + + /// No description provided for @backupRecoveryOptionAMicrocopy. + /// + /// In en, this message translates to: + /// **'Select friends who can help you recover your account.'** + String get backupRecoveryOptionAMicrocopy; + + /// No description provided for @backupRecoveryOptionBPassword. + /// + /// In en, this message translates to: + /// **'Classic Password'** + String get backupRecoveryOptionBPassword; + + /// No description provided for @backupRecoveryOptionBMicrocopy. + /// + /// In en, this message translates to: + /// **'Set a password for your access.'** + String get backupRecoveryOptionBMicrocopy; + + /// No description provided for @backupCloudSectionTitle. + /// + /// In en, this message translates to: + /// **'Backup Your Data'** + String get backupCloudSectionTitle; + + /// No description provided for @backupCloudSectionDesc. + /// + /// In en, this message translates to: + /// **'Backup your content so nothing gets lost.'** + String get backupCloudSectionDesc; + + /// No description provided for @backupCloudContactsMessages. + /// + /// In en, this message translates to: + /// **'Contacts & Messages'** + String get backupCloudContactsMessages; + + /// No description provided for @backupCloudFreeActive. + /// + /// In en, this message translates to: + /// **'Free Active'** + String get backupCloudFreeActive; + + /// No description provided for @backupCloudImagesMedia. + /// + /// In en, this message translates to: + /// **'Images & Media'** + String get backupCloudImagesMedia; + + /// No description provided for @backupCloudProBadge. + /// + /// In en, this message translates to: + /// **'Pro'** + String get backupCloudProBadge; + + /// No description provided for @memoriesAddingToFavorites. + /// + /// In en, this message translates to: + /// **'Adding to favorites...'** + String get memoriesAddingToFavorites; + + /// No description provided for @memoriesRemovingFromFavorites. + /// + /// In en, this message translates to: + /// **'Removing from favorites...'** + String get memoriesRemovingFromFavorites; + + /// No description provided for @memoriesDeletingProgress. + /// + /// In en, this message translates to: + /// **'Deleting memories...'** + String get memoriesDeletingProgress; + + /// No description provided for @memoriesExportingProgress. + /// + /// In en, this message translates to: + /// **'Exporting memories...'** + String get memoriesExportingProgress; } class _AppLocalizationsDelegate diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 0ef9b545..3a8fb0db 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -850,7 +850,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get backupSelectStrongPassword => - 'Wähle ein sicheres Passwort. Dies ist erforderlich, wenn du dein Backup wiederherstellen möchtest.'; + 'Wähle ein sicheres Passwort. Es wird empfohlen, dieses über deinen bevorzugten Passwort-Manager zu generieren und zu speichern.'; @override String get password => 'Passwort'; @@ -866,7 +866,7 @@ class AppLocalizationsDe extends AppLocalizations { 'Das Passwort muss mindestens 10 Zeichen lang sein.'; @override - String get backupEnableBackup => 'Automatische Sicherung aktivieren'; + String get backupEnableBackup => 'Passwort-Backup aktivieren'; @override String get backupTwonlySaveNow => 'Jetzt speichern'; @@ -2144,7 +2144,7 @@ class AppLocalizationsDe extends AppLocalizations { String get avatarCustomizeReset => 'Zurücksetzen'; @override - String get passwordlessRecovery => 'Passwort vergessen'; + String get passwordlessRecovery => 'Vertraute Freunde'; @override String get passwordlessRecoveryNotConfigured => 'Nicht konfiguriert'; @@ -2210,7 +2210,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get passwordlessRecoveryEnableSuccess => - '\"Passwort vergessen\" erfolgreich aktiviert!'; + 'Vertraute Freunde erfolgreich eingerichtet!'; @override String get passwordlessRecoveryEnterPin => 'Bitte gib eine PIN ein.'; @@ -2220,16 +2220,14 @@ class AppLocalizationsDe extends AppLocalizations { 'Bitte gib eine E-Mail-Adresse ein.'; @override - String get passwordlessRecoveryEnableBtn => - '\"Passwort vergessen\" aktivieren'; + String get passwordlessRecoveryEnableBtn => 'Vertraute Freunde aktivieren'; @override String get passwordlessRecoveryRecoverBtn => - 'Mit \"Passwort vergessen\" wiederherstellen'; + 'Mithilfe von vertrauten Freunden wiederherstellen'; @override - String get passwordlessRecoveryModifyBtn => - '\"Passwort vergessen\" bearbeiten'; + String get passwordlessRecoveryModifyBtn => 'Vertraute Freunde bearbeiten'; @override String passwordlessRecoveryStatusEnabled(num count) { @@ -2568,4 +2566,60 @@ class AppLocalizationsDe extends AppLocalizations { @override String get themeDark => 'Dunkel'; + + @override + String get backupRecoverySectionTitle => 'Konto-Zugang sichern'; + + @override + String get backupRecoverySectionDescNone => + 'Du musst mindestens eine Wiederherstellungsoption auswählen, da dir sonst niemand helfen kann, falls du den Zugriff auf dein Gerät verlierst.'; + + @override + String get backupRecoverySectionDescSome => + 'Mit den untenstehenden Wiederherstellungsoptionen kannst du dein Konto wiederherstellen. Es wird empfohlen, beide Optionen zu aktivieren.'; + + @override + String get backupRecoveryOptionAFriends => 'Über vertraute Freunde'; + + @override + String get backupRecoveryOptionAMicrocopy => + 'Wähle Freunde aus, die dir helfen, dein Konto wiederherzustellen.'; + + @override + String get backupRecoveryOptionBPassword => 'Klassisches Passwort'; + + @override + String get backupRecoveryOptionBMicrocopy => + 'Lege ein Passwort für deinen Zugang fest.'; + + @override + String get backupCloudSectionTitle => 'Deine Daten sichern'; + + @override + String get backupCloudSectionDesc => + 'Sichere deine Inhalte, damit nichts verloren geht.'; + + @override + String get backupCloudContactsMessages => 'Kontakte & Nachrichten'; + + @override + String get backupCloudFreeActive => 'Kostenlos aktiv'; + + @override + String get backupCloudImagesMedia => 'Bilder & Medien'; + + @override + String get backupCloudProBadge => 'Pro'; + + @override + String get memoriesAddingToFavorites => 'Zu Favoriten hinzufügen...'; + + @override + String get memoriesRemovingFromFavorites => 'Aus Favoriten entfernen...'; + + @override + String get memoriesDeletingProgress => 'Erinnerungen werden gelöscht...'; + + @override + String get memoriesExportingProgress => 'Erinnerungen werden exportiert...'; } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index a51297d4..90201697 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -845,7 +845,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String get backupSelectStrongPassword => - 'Choose a secure password. This is required if you want to restore your backup.'; + 'Select a secure password. It is recommended to generate and store it via your password manager of choice.'; @override String get password => 'Password'; @@ -861,7 +861,7 @@ class AppLocalizationsEn extends AppLocalizations { 'Password must be at least 10 characters long.'; @override - String get backupEnableBackup => 'Activate automatic backup'; + String get backupEnableBackup => 'Enable Password Backup'; @override String get backupTwonlySaveNow => 'Save now'; @@ -2129,7 +2129,7 @@ class AppLocalizationsEn extends AppLocalizations { String get avatarCustomizeReset => 'Reset'; @override - String get passwordlessRecovery => 'Password Recovery'; + String get passwordlessRecovery => 'Trusted Friends'; @override String get passwordlessRecoveryNotConfigured => 'Not configured'; @@ -2195,7 +2195,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String get passwordlessRecoveryEnableSuccess => - 'Passwordless recovery successfully enabled!'; + 'Trusted Friends successfully set up!'; @override String get passwordlessRecoveryEnterPin => 'Please enter a PIN.'; @@ -2204,13 +2204,13 @@ class AppLocalizationsEn extends AppLocalizations { String get passwordlessRecoveryEnterEmail => 'Please enter an email address.'; @override - String get passwordlessRecoveryEnableBtn => 'Enable Passwordless Recovery'; + String get passwordlessRecoveryEnableBtn => 'Enable Trusted Friends'; @override - String get passwordlessRecoveryRecoverBtn => 'Recover passwordless'; + String get passwordlessRecoveryRecoverBtn => 'Recover via Trusted Friends'; @override - String get passwordlessRecoveryModifyBtn => 'Modify Passwordless Recovery'; + String get passwordlessRecoveryModifyBtn => 'Modify Trusted Friends'; @override String passwordlessRecoveryStatusEnabled(num count) { @@ -2541,4 +2541,60 @@ class AppLocalizationsEn extends AppLocalizations { @override String get themeDark => 'Dark'; + + @override + String get backupRecoverySectionTitle => 'Secure Account Access'; + + @override + String get backupRecoverySectionDescNone => + 'You must select at least one recovery option, as otherwise no one can help you if you lose access to your device.'; + + @override + String get backupRecoverySectionDescSome => + 'You can recover your account with the recovery options below. It is recommended to enable both options.'; + + @override + String get backupRecoveryOptionAFriends => 'Via trusted friends'; + + @override + String get backupRecoveryOptionAMicrocopy => + 'Select friends who can help you recover your account.'; + + @override + String get backupRecoveryOptionBPassword => 'Classic Password'; + + @override + String get backupRecoveryOptionBMicrocopy => + 'Set a password for your access.'; + + @override + String get backupCloudSectionTitle => 'Backup Your Data'; + + @override + String get backupCloudSectionDesc => + 'Backup your content so nothing gets lost.'; + + @override + String get backupCloudContactsMessages => 'Contacts & Messages'; + + @override + String get backupCloudFreeActive => 'Free Active'; + + @override + String get backupCloudImagesMedia => 'Images & Media'; + + @override + String get backupCloudProBadge => 'Pro'; + + @override + String get memoriesAddingToFavorites => 'Adding to favorites...'; + + @override + String get memoriesRemovingFromFavorites => 'Removing from favorites...'; + + @override + String get memoriesDeletingProgress => 'Deleting memories...'; + + @override + String get memoriesExportingProgress => 'Exporting memories...'; } diff --git a/lib/src/localization/translations b/lib/src/localization/translations index ce51a6d0..50cc5cb6 160000 --- a/lib/src/localization/translations +++ b/lib/src/localization/translations @@ -1 +1 @@ -Subproject commit ce51a6d084db162b4dfb7cbba1c0cda6d8c477d5 +Subproject commit 50cc5cb69dd91a5c626534fe4c015e50d2a0a486 diff --git a/lib/src/visual/views/memories/components/cloud_backup_promo.comp.dart b/lib/src/visual/views/memories/components/cloud_backup_promo.comp.dart index 47db82ea..65534d27 100644 --- a/lib/src/visual/views/memories/components/cloud_backup_promo.comp.dart +++ b/lib/src/visual/views/memories/components/cloud_backup_promo.comp.dart @@ -1,7 +1,14 @@ import 'dart:async'; + import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; import 'package:twonly/locator.dart'; +import 'package:twonly/src/constants/routes.keys.dart'; +import 'package:twonly/src/providers/purchases.provider.dart'; import 'package:twonly/src/services/memories/memories_cloud.service.dart'; +import 'package:twonly/src/services/subscription.service.dart'; import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/elements/my_button.element.dart'; @@ -11,6 +18,9 @@ class MemoriesCloudBackupPromoComp extends StatelessWidget { @override Widget build(BuildContext context) { + final isFreePlan = + context.watch().plan == SubscriptionPlan.Free; + return StreamBuilder( stream: userService.onUserUpdated, builder: (context, snapshot) { @@ -24,55 +34,99 @@ class MemoriesCloudBackupPromoComp extends StatelessWidget { return SliverToBoxAdapter( child: Container( margin: const EdgeInsets.symmetric( - horizontal: 8, + horizontal: 12, vertical: 8, ), decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - gradient: LinearGradient( - colors: [ - context.color.primaryContainer.withValues(alpha: 0.15), - context.color.primaryContainer.withValues(alpha: 0.05), - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), + color: context.color.surfaceContainerLow, + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], border: Border.all( - color: context.color.primary.withValues(alpha: 0.1), - width: 1.5, + color: context.color.primary.withValues(alpha: 0.15), ), ), child: Padding( padding: const EdgeInsets.all(16), child: Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - context.lang.memoriesBackupTitle, - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - color: context.color.onSurface, - ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + context.lang.memoriesBackupTitle, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: context.color.onSurface, + ), + ), + ), + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: Colors.amber.shade700, + borderRadius: BorderRadius.circular(6), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const FaIcon( + FontAwesomeIcons.star, + size: 10, + color: Colors.white, + ), + const SizedBox(width: 4), + Text( + context.lang.backupCloudProBadge, + style: const TextStyle( + fontSize: 10, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ], ), - const SizedBox(height: 4), + const SizedBox(height: 6), Text( context.lang.settingsStorageNoCloudBackupCard, style: TextStyle( fontSize: 13, color: context.color.onSurfaceVariant, - height: 1.3, + height: 1.4, ), ), - const SizedBox(height: 12), + const SizedBox(height: 16), Row( children: [ MyButton( variant: MyButtonVariant.primaryDense, onPressed: () async { + if (isFreePlan) { + unawaited( + context.push(Routes.settingsSubscription), + ); + return; + } await UserService.update( (u) => u.isCloudBackupEnabled = true, ); @@ -80,9 +134,7 @@ class MemoriesCloudBackupPromoComp extends StatelessWidget { }, child: Text(context.lang.enable), ), - const SizedBox(width: 8), - MyButton( - variant: MyButtonVariant.secondaryDense, + TextButton( onPressed: () async { await UserService.update( (u) => u.hideMemoriesBackupPromo = true, @@ -90,6 +142,10 @@ class MemoriesCloudBackupPromoComp extends StatelessWidget { }, child: Text( context.lang.settingsStorageHidePromo, + style: TextStyle( + color: context.color.onSurfaceVariant, + fontWeight: FontWeight.w600, + ), ), ), ], diff --git a/lib/src/visual/views/memories/memories.view.dart b/lib/src/visual/views/memories/memories.view.dart index 4664cb63..4c16b273 100644 --- a/lib/src/visual/views/memories/memories.view.dart +++ b/lib/src/visual/views/memories/memories.view.dart @@ -306,11 +306,11 @@ class MemoriesViewState extends State if (!confirmed) return; } - if (deleteCompletely == null) return; + if (deleteCompletely == null || !mounted) return; final isCompletely = deleteCompletely; await _showProgressDialog( - 'Deleting memories...', + context.lang.memoriesDeletingProgress, (setProgress) async { for (var i = 0; i < selectedList.length; i++) { final mediaId = selectedList[i]; @@ -350,7 +350,7 @@ class MemoriesViewState extends State try { await _showProgressDialog( - 'Exporting memories...', + context.lang.memoriesExportingProgress, (setProgress) async { for (var i = 0; i < selectedList.length; i++) { final mediaId = selectedList[i]; @@ -411,7 +411,9 @@ class MemoriesViewState extends State final targetFav = !areAllFav; await _showProgressDialog( - targetFav ? 'Adding to favorites...' : 'Removing from favorites...', + targetFav + ? context.lang.memoriesAddingToFavorites + : context.lang.memoriesRemovingFromFavorites, (setProgress) async { for (var i = 0; i < selectedList.length; i++) { final mediaId = selectedList[i]; diff --git a/lib/src/visual/views/settings/backup/backup_settings.view.dart b/lib/src/visual/views/settings/backup/backup_settings.view.dart index 3487b821..3c2dcff4 100644 --- a/lib/src/visual/views/settings/backup/backup_settings.view.dart +++ b/lib/src/visual/views/settings/backup/backup_settings.view.dart @@ -17,6 +17,8 @@ import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/elements/better_list_title.element.dart'; import 'package:twonly/src/visual/elements/my_button.element.dart'; +import 'package:twonly/src/visual/views/settings/backup/backup_utils.dart'; +import 'package:twonly/src/visual/views/settings/backup/components/recovery_card.comp.dart'; import 'package:twonly/src/visual/views/settings/backup/memories_backup_detail.view.dart'; import 'package:twonly/src/visual/views/settings/backup/passwordless_recovery/components/status.passwordless_recovery.comp.dart'; import 'package:twonly/src/visual/views/settings/backup/passwordless_recovery/setup.passwordless_recovery.view.dart'; @@ -74,6 +76,9 @@ class _BackupViewState extends State { Widget build(BuildContext context) { final currentPlan = context.watch().plan; final isFreePlan = currentPlan == SubscriptionPlan.Free; + final hasPasswordless = + userService.currentUser.passwordLessRecovery != null; + final hasPassword = userService.currentUser.isBackupEnabled; return StreamBuilder( stream: userService.onUserUpdated, @@ -86,7 +91,33 @@ class _BackupViewState extends State { padding: const EdgeInsets.symmetric(vertical: 16), child: ListView( children: [ - if (userService.currentUser.passwordLessRecovery != null) + // --- BEREICH 1: Konto-Wiederherstellung --- + Text( + context.lang.backupRecoverySectionTitle, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + (!hasPasswordless && !hasPassword) + ? context.lang.backupRecoverySectionDescNone + : context.lang.backupRecoverySectionDescSome, + textAlign: TextAlign.center, + style: TextStyle( + color: (!hasPasswordless && !hasPassword) + ? context.color.error + : context.color.onSurfaceVariant, + fontSize: 13, + ), + ), + ), + const SizedBox(height: 24), + + if (hasPasswordless) const Padding( padding: EdgeInsets.symmetric(horizontal: 16), child: PasswordLessRecoveryStatus(), @@ -94,20 +125,46 @@ class _BackupViewState extends State { else Padding( padding: const EdgeInsets.symmetric(horizontal: 16), - child: Center( - child: MyButton( - variant: MyButtonVariant.primaryMiddle, - onPressed: () => - context.navPush(const PasswordLessRecoverySetup()), - child: Text(context.lang.passwordlessRecoveryEnableBtn), - ), + child: RecoveryCard( + icon: FontAwesomeIcons.shieldHeart, + title: context.lang.backupRecoveryOptionAFriends, + subtitle: context.lang.backupRecoveryOptionAMicrocopy, + onTap: () => + context.navPush(const PasswordLessRecoverySetup()), ), ), - const SizedBox(height: 16), + const SizedBox(height: 12), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: RecoveryCard( + isEnabled: hasPassword, + icon: FontAwesomeIcons.key, + title: context.lang.backupRecoveryOptionBPassword, + subtitle: hasPassword + ? context.lang.backupChangePassword + : context.lang.backupRecoveryOptionBMicrocopy, + onTap: () => + context.push(Routes.settingsBackupSetup, extra: true), + ), + ), + + const SizedBox(height: 32), + const Divider(), + const SizedBox(height: 32), + + // --- BEREICH 2: Cloud-Backup --- + Text( + context.lang.backupCloudSectionTitle, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Text( - context.lang.backupTwonlySafeDesc, + context.lang.backupCloudSectionDesc, textAlign: TextAlign.center, style: TextStyle( color: context.color.onSurfaceVariant, @@ -115,109 +172,125 @@ class _BackupViewState extends State { ), ), ), - const SizedBox(height: 20), - const Divider(), - const SizedBox(height: 8), + const SizedBox(height: 16), - if (userService.currentUser.isBackupEnabled) ...[ - // 1. Identity tile - BetterListTile( - icon: FontAwesomeIcons.userCheck, - text: context.lang.backupIdentityHeader, - subtitle: Text( - _buildTileSubtitle( - _backupStatus?.identityLastSuccessFull, - _backupStatus?.identitySize, - ), + // Kontakte & Nachrichten + BetterListTile( + icon: FontAwesomeIcons.comments, + text: context.lang.backupCloudContactsMessages, + subtitle: Text( + _buildTileSubtitle( + _backupStatus?.archiveLastSuccessFull, + _backupStatus?.archiveSize, ), ), + trailing: const Icon(Icons.check_circle, color: Colors.green), + ), - // 2. Contacts & Messages tile - BetterListTile( - icon: FontAwesomeIcons.comments, - text: context.lang.backupArchiveHeader, - subtitle: Text( - _buildTileSubtitle( - _backupStatus?.archiveLastSuccessFull, - _backupStatus?.archiveSize, - ), - ), + // Bilder & Medien + BetterListTile( + icon: FontAwesomeIcons.photoFilm, + text: context.lang.backupCloudImagesMedia, + subtitle: Text( + isFreePlan + ? context.lang.backupMemoriesUpgradeRequired + : (!userService.currentUser.isCloudBackupEnabled + ? context.lang.backupMemoriesNotEnabled + : (_memoriesUsage != null + ? '${formatBytes(_memoriesUsage!.currentBytes.toInt())} / ${formatBytes(_memoriesUsage!.maxBytes.toInt())}' + : '-')), ), + trailing: isFreePlan + ? Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: Colors.amber.shade700, + borderRadius: BorderRadius.circular(4), + ), + child: Row( + children: [ + const FaIcon( + FontAwesomeIcons.star, + size: 10, + color: Colors.white, + ), + const SizedBox(width: 4), + Text( + context.lang.backupCloudProBadge, + style: const TextStyle( + fontSize: 10, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + const SizedBox(width: 8), + const Switch( + value: false, + onChanged: null, + ), + ], + ) + : Switch( + value: userService.currentUser.isCloudBackupEnabled, + onChanged: (val) async { + if (!val) { + final disabled = + await promptAndDisableMemoriesBackup(context); + if (disabled && mounted) setState(() {}); + } else { + await UserService.update( + (u) => u.isCloudBackupEnabled = val, + ); + if (mounted) setState(() {}); + unawaited(memoriesCloudService.checkUploads()); + } + }, + ), + onTap: isFreePlan + ? () async { + await context.push(Routes.settingsSubscription); + } + : () async { + if (userService.currentUser.isCloudBackupEnabled) { + await context.navPush( + const MemoriesBackupDetailView(), + ); + } else { + await UserService.update( + (u) => u.isCloudBackupEnabled = true, + ); + if (mounted) setState(() {}); + unawaited(memoriesCloudService.checkUploads()); + } + }, + ), - // 3. Memories tile - BetterListTile( - icon: FontAwesomeIcons.photoFilm, - text: context.lang.memoriesBackupTitle, - subtitle: Text( - isFreePlan - ? context.lang.backupMemoriesUpgradeRequired - : (!userService.currentUser.isCloudBackupEnabled - ? context.lang.backupMemoriesNotEnabled - : (_memoriesUsage != null - ? '${formatBytes(_memoriesUsage!.currentBytes.toInt())} / ${formatBytes(_memoriesUsage!.maxBytes.toInt())}' - : '-')), - ), - trailing: Icon( - Icons.chevron_right_rounded, - color: context.color.onSurfaceVariant, - ), - onTap: () async { - if (isFreePlan) { - await context.push(Routes.settingsSubscription); - } else if (!userService - .currentUser - .isCloudBackupEnabled) { - await UserService.update( - (u) => u.isCloudBackupEnabled = true, - ); - if (mounted) setState(() {}); - unawaited(memoriesCloudService.checkUploads()); - } else { - await context.navPush(const MemoriesBackupDetailView()); - } - }, - ), - const SizedBox(height: 16), - const Divider(), - const SizedBox(height: 20), - ], - + const SizedBox(height: 32), Center( - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - if (userService.currentUser.isBackupEnabled) ...[ - MyButton( - variant: MyButtonVariant.secondaryDense, - onPressed: _isLoading - ? null - : () async { - setState(() { - _isLoading = true; - }); - await BackupService.makeBackup(force: true); - await _loadBackupStatus(); - }, - child: Text(context.lang.backupTwonlySaveNow), - ), - const SizedBox(width: 12), - ], - MyButton( - variant: MyButtonVariant.secondaryDense, - onPressed: () => context.push( - Routes.settingsBackupSetup, - extra: true, - ), - child: Text( - !userService.currentUser.isBackupEnabled - ? context.lang.backupEnableBackup - : context.lang.backupChangePassword, - ), - ), - ], + child: MyButton( + variant: MyButtonVariant.secondaryDense, + onPressed: _isLoading + ? null + : () async { + setState(() { + _isLoading = true; + }); + await BackupService.makeBackup(force: true); + await _loadBackupStatus(); + }, + child: Text(context.lang.backupTwonlySaveNow), ), ), + const SizedBox(height: 24), ], ), ), diff --git a/lib/src/visual/views/settings/backup/backup_setup.view.dart b/lib/src/visual/views/settings/backup/backup_setup.view.dart index 7a8d4277..08eef593 100644 --- a/lib/src/visual/views/settings/backup/backup_setup.view.dart +++ b/lib/src/visual/views/settings/backup/backup_setup.view.dart @@ -154,8 +154,8 @@ class _SetupBackupViewState extends State { const SizedBox(width: 8), Text( userService.currentUser.isBackupEnabled - ? context.lang.backupEnableBackup - : context.lang.backupChangePassword, + ? context.lang.backupChangePassword + : context.lang.backupEnableBackup, ), ], ), diff --git a/lib/src/visual/views/settings/backup/backup_utils.dart b/lib/src/visual/views/settings/backup/backup_utils.dart new file mode 100644 index 00000000..67c09e08 --- /dev/null +++ b/lib/src/visual/views/settings/backup/backup_utils.dart @@ -0,0 +1,111 @@ +import 'package:drift/drift.dart' hide Column; +import 'package:flutter/material.dart'; +import 'package:twonly/locator.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; +import 'package:twonly/src/services/user.service.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/components/snackbar.dart'; +import 'package:twonly/src/visual/elements/my_button.element.dart'; + +Future promptAndDisableMemoriesBackup(BuildContext context) async { + final cloudOnlyCount = + await twonlyDB.mediaFilesDao.getCloudOnlyMemoriesCount(); + + if (!context.mounted) return false; + + final confirmed = await showDialog( + context: context, + builder: (context) => Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24), + ), + backgroundColor: Theme.of(context).colorScheme.surface, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + context.lang.settingsStorageDisableBackupTitle, + textAlign: TextAlign.center, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), + const SizedBox(height: 16), + Text.rich( + TextSpan( + children: formattedText( + context, + context.lang.settingsStorageDisableBackupBody( + cloudOnlyCount, + ), + textColor: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.8), + ), + ), + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 15), + ), + const SizedBox(height: 24), + Row( + children: [ + Expanded( + child: MyButton( + variant: MyButtonVariant.secondaryMiddle, + onPressed: () => Navigator.pop(context, false), + child: Text(context.lang.galleryCancel), + ), + ), + const SizedBox(width: 12), + Expanded( + child: MyButton( + variant: MyButtonVariant.errorMiddle, + onPressed: () => Navigator.pop(context, true), + child: Text( + context.lang.settingsStorageDisableBackupBtn, + ), + ), + ), + ], + ), + ], + ), + ), + ), + ); + + if (confirmed == true) { + try { + await apiService.disableMemoriesBackup(); + final allMedias = await (twonlyDB.select( + twonlyDB.mediaFiles, + )..where((t) => t.stored.equals(true))).get(); + for (final media in allMedias) { + final ms = MediaFileService(media); + if (!ms.storedPath.existsSync()) { + ms.fullMediaRemoval(); + await twonlyDB.mediaFilesDao.deleteMediaFile(media.mediaId); + } + } + await twonlyDB.mediaFilesDao.updateAllMediaFiles( + const MediaFilesCompanion( + cloudState: Value(CloudState.none), + ), + ); + await UserService.update((u) => u.isCloudBackupEnabled = false); + return true; + } catch (e) { + if (context.mounted) { + showSnackbar(context, e.toString()); + } + return false; + } + } + return false; +} diff --git a/lib/src/visual/views/settings/backup/components/recovery_card.comp.dart b/lib/src/visual/views/settings/backup/components/recovery_card.comp.dart new file mode 100644 index 00000000..abf29333 --- /dev/null +++ b/lib/src/visual/views/settings/backup/components/recovery_card.comp.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:twonly/src/utils/misc.dart'; + +class RecoveryCard extends StatelessWidget { + const RecoveryCard({ + required this.icon, + required this.title, + required this.subtitle, + required this.onTap, + super.key, + this.isEnabled = false, + }); + final dynamic icon; + final String title; + final String subtitle; + final VoidCallback onTap; + final bool isEnabled; + + @override + Widget build(BuildContext context) { + final effectiveColor = isEnabled ? context.color.primary : Colors.red; + + return Card( + elevation: 0, + color: context.color.surfaceContainer, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: InkWell( + borderRadius: BorderRadius.circular(16), + onTap: onTap, + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: effectiveColor.withValues(alpha: 0.1), + shape: BoxShape.circle, + ), + child: icon is IconData + ? Icon( + icon as IconData, + color: effectiveColor, + size: 24, + ) + : FaIcon( + icon as FaIconData?, + color: effectiveColor, + size: 24, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + subtitle, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: context.color.onSurfaceVariant, + ), + ), + ], + ), + ), + const SizedBox(width: 3), + Icon( + Icons.chevron_right_rounded, + color: context.color.onSurfaceVariant, + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/src/visual/views/settings/backup/memories_backup_detail.view.dart b/lib/src/visual/views/settings/backup/memories_backup_detail.view.dart index b0c6b761..98f4d4c4 100644 --- a/lib/src/visual/views/settings/backup/memories_backup_detail.view.dart +++ b/lib/src/visual/views/settings/backup/memories_backup_detail.view.dart @@ -1,17 +1,13 @@ import 'dart:async'; -import 'package:drift/drift.dart' hide Column; import 'package:flutter/material.dart'; import 'package:twonly/locator.dart'; -import 'package:twonly/src/database/tables/mediafiles.table.dart'; -import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart' as server; -import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/memories/memories_cloud.service.dart'; -import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/components/snackbar.dart'; import 'package:twonly/src/visual/elements/my_button.element.dart'; +import 'package:twonly/src/visual/views/settings/backup/backup_utils.dart'; import 'package:twonly/src/visual/views/settings/data_and_storage/storage_contents.view.dart'; class MemoriesBackupDetailView extends StatefulWidget { @@ -24,7 +20,6 @@ class MemoriesBackupDetailView extends StatefulWidget { class _MemoriesBackupDetailViewState extends State { server.Response_MemoriesUsage? _memoriesUsage; - int _cloudOnlyCount = 0; bool _isLoading = true; @override @@ -35,110 +30,18 @@ class _MemoriesBackupDetailViewState extends State { Future _loadStats() async { final memoriesUsage = await apiService.getMemoriesUsage(); - final cloudOnlyCount = await twonlyDB.mediaFilesDao - .getCloudOnlyMemoriesCount(); if (mounted) { setState(() { _memoriesUsage = memoriesUsage; - _cloudOnlyCount = cloudOnlyCount; _isLoading = false; }); } } Future _disableBackup() async { - final confirmed = await showDialog( - context: context, - builder: (context) => Dialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(24), - ), - backgroundColor: Theme.of(context).colorScheme.surface, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - context.lang.settingsStorageDisableBackupTitle, - textAlign: TextAlign.center, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - ), - ), - const SizedBox(height: 16), - Text.rich( - TextSpan( - children: formattedText( - context, - context.lang.settingsStorageDisableBackupBody( - _cloudOnlyCount, - ), - textColor: Theme.of( - context, - ).colorScheme.onSurface.withValues(alpha: 0.8), - ), - ), - textAlign: TextAlign.center, - style: const TextStyle(fontSize: 15), - ), - const SizedBox(height: 24), - Row( - children: [ - Expanded( - child: MyButton( - variant: MyButtonVariant.secondaryMiddle, - onPressed: () => Navigator.pop(context, false), - child: Text(context.lang.galleryCancel), - ), - ), - const SizedBox(width: 12), - Expanded( - child: MyButton( - variant: MyButtonVariant.errorMiddle, - onPressed: () => Navigator.pop(context, true), - child: Text( - context.lang.settingsStorageDisableBackupBtn, - ), - ), - ), - ], - ), - ], - ), - ), - ), - ); - - if (confirmed == true) { - try { - await apiService.disableMemoriesBackup(); - final allMedias = await (twonlyDB.select( - twonlyDB.mediaFiles, - )..where((t) => t.stored.equals(true))).get(); - for (final media in allMedias) { - final ms = MediaFileService(media); - if (!ms.storedPath.existsSync()) { - ms.fullMediaRemoval(); - await twonlyDB.mediaFilesDao.deleteMediaFile(media.mediaId); - } - } - await twonlyDB.mediaFilesDao.updateAllMediaFiles( - const MediaFilesCompanion( - cloudState: Value(CloudState.none), - ), - ); - await UserService.update((u) => u.isCloudBackupEnabled = false); - if (mounted) { - Navigator.pop(context); - } - } catch (e) { - if (mounted) { - showSnackbar(context, e.toString()); - } - } + final disabled = await promptAndDisableMemoriesBackup(context); + if (disabled && mounted) { + Navigator.pop(context); } }