diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index 348d0259..e7ec988e 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -4171,6 +4171,18 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Share'** String get galleryActionShare; + + /// No description provided for @settingsStorageHidePromo. + /// + /// In en, this message translates to: + /// **'Hide'** + String get settingsStorageHidePromo; + + /// No description provided for @memoriesBackupTitle. + /// + /// In en, this message translates to: + /// **'Memories Backup'** + String get memoriesBackupTitle; } class _AppLocalizationsDelegate diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 88402311..68185e49 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -2415,4 +2415,10 @@ class AppLocalizationsDe extends AppLocalizations { @override String get galleryActionShare => 'Teilen'; + + @override + String get settingsStorageHidePromo => 'Ausblenden'; + + @override + String get memoriesBackupTitle => 'Memories Backup'; } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 1c4ab096..9e93e569 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -2394,4 +2394,10 @@ class AppLocalizationsEn extends AppLocalizations { @override String get galleryActionShare => 'Share'; + + @override + String get settingsStorageHidePromo => 'Hide'; + + @override + String get memoriesBackupTitle => 'Memories Backup'; } diff --git a/lib/src/localization/translations b/lib/src/localization/translations index 9d05713a..f72c6178 160000 --- a/lib/src/localization/translations +++ b/lib/src/localization/translations @@ -1 +1 @@ -Subproject commit 9d05713a1f69e7e4de742c414df7df6c4c7925a7 +Subproject commit f72c61787096a44e2252058c08e4c14a0a8e5cff diff --git a/lib/src/model/json/userdata.model.dart b/lib/src/model/json/userdata.model.dart index 1a05106c..8ee4b648 100644 --- a/lib/src/model/json/userdata.model.dart +++ b/lib/src/model/json/userdata.model.dart @@ -103,6 +103,9 @@ class UserData { @JsonKey(defaultValue: false) bool screenLockEnabled = false; + @JsonKey(defaultValue: false) + bool isCloudBackupEnabled = false; + // > User Discovery Configurations @JsonKey(defaultValue: false) @@ -139,6 +142,9 @@ class UserData { @JsonKey(defaultValue: true) bool hideChangeLog = true; + @JsonKey(defaultValue: false) + bool hideMemoriesBackupPromo = false; + @JsonKey(defaultValue: true) bool updateFCMToken = true; @@ -150,8 +156,8 @@ class UserData { @Deprecated('Use the secure storage in rust') TwonlySafeBackup? twonlySafeBackup; - @JsonKey(defaultValue: true) - bool isBackupEnabled = true; + @JsonKey(defaultValue: false) + bool isBackupEnabled = false; PasswordLessRecovery? passwordLessRecovery; diff --git a/lib/src/model/json/userdata.model.g.dart b/lib/src/model/json/userdata.model.g.dart index b560d6d4..08825f09 100644 --- a/lib/src/model/json/userdata.model.g.dart +++ b/lib/src/model/json/userdata.model.g.dart @@ -68,6 +68,7 @@ UserData _$UserDataFromJson(Map json) => ..allowErrorTrackingViaSentry = json['allowErrorTrackingViaSentry'] as bool? ?? false ..screenLockEnabled = json['screenLockEnabled'] as bool? ?? false + ..isCloudBackupEnabled = json['isCloudBackupEnabled'] as bool? ?? false ..isUserDiscoveryEnabled = json['isUserDiscoveryEnabled'] as bool? ?? false ..requiredSendImages = (json['requiredSendImages'] as num?)?.toInt() ?? 4 @@ -88,6 +89,8 @@ UserData _$UserDataFromJson(Map json) => ?.map((e) => (e as num).toInt()) .toList() ..hideChangeLog = json['hideChangeLog'] as bool? ?? true + ..hideMemoriesBackupPromo = + json['hideMemoriesBackupPromo'] as bool? ?? false ..updateFCMToken = json['updateFCMToken'] as bool? ?? true ..canUseLoginTokenForAuth = json['canUseLoginTokenForAuth'] as bool? ?? true @@ -96,7 +99,7 @@ UserData _$UserDataFromJson(Map json) => : TwonlySafeBackup.fromJson( json['twonlySafeBackup'] as Map, ) - ..isBackupEnabled = json['isBackupEnabled'] as bool? ?? true + ..isBackupEnabled = json['isBackupEnabled'] as bool? ?? false ..passwordLessRecovery = json['passwordLessRecovery'] == null ? null : PasswordLessRecovery.fromJson( @@ -153,6 +156,7 @@ Map _$UserDataToJson(UserData instance) => { ?.toIso8601String(), 'allowErrorTrackingViaSentry': instance.allowErrorTrackingViaSentry, 'screenLockEnabled': instance.screenLockEnabled, + 'isCloudBackupEnabled': instance.isCloudBackupEnabled, 'isUserDiscoveryEnabled': instance.isUserDiscoveryEnabled, 'requiredSendImages': instance.requiredSendImages, 'userDiscoveryThreshold': instance.userDiscoveryThreshold, @@ -165,6 +169,7 @@ Map _$UserDataToJson(UserData instance) => { 'currentSignedPreKeyIndexStart': instance.currentSignedPreKeyIndexStart, 'lastChangeLogHash': instance.lastChangeLogHash, 'hideChangeLog': instance.hideChangeLog, + 'hideMemoriesBackupPromo': instance.hideMemoriesBackupPromo, 'updateFCMToken': instance.updateFCMToken, 'canUseLoginTokenForAuth': instance.canUseLoginTokenForAuth, 'twonlySafeBackup': instance.twonlySafeBackup, diff --git a/lib/src/services/memories/memories_cloud.service.dart b/lib/src/services/memories/memories_cloud.service.dart index fc6c3e21..4cab3a2a 100644 --- a/lib/src/services/memories/memories_cloud.service.dart +++ b/lib/src/services/memories/memories_cloud.service.dart @@ -101,7 +101,7 @@ class MemoriesCloudService { } Future checkUploads() async { - if (_isProcessing || !userService.currentUser.isBackupEnabled) return; + if (_isProcessing || !userService.currentUser.isCloudBackupEnabled) return; try { final memories = await twonlyDB.mediaFilesDao.getMemoriesToBackup(); 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 new file mode 100644 index 00000000..47db82ea --- /dev/null +++ b/lib/src/visual/views/memories/components/cloud_backup_promo.comp.dart @@ -0,0 +1,108 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:twonly/locator.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/elements/my_button.element.dart'; + +class MemoriesCloudBackupPromoComp extends StatelessWidget { + const MemoriesCloudBackupPromoComp({super.key}); + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: userService.onUserUpdated, + builder: (context, snapshot) { + final user = userService.currentUser; + if (user.isCloudBackupEnabled || user.hideMemoriesBackupPromo) { + return const SliverToBoxAdapter( + child: SizedBox.shrink(), + ); + } + + return SliverToBoxAdapter( + child: Container( + margin: const EdgeInsets.symmetric( + horizontal: 8, + 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, + ), + border: Border.all( + color: context.color.primary.withValues(alpha: 0.1), + width: 1.5, + ), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.lang.memoriesBackupTitle, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: context.color.onSurface, + ), + ), + const SizedBox(height: 4), + Text( + context.lang.settingsStorageNoCloudBackupCard, + style: TextStyle( + fontSize: 13, + color: context.color.onSurfaceVariant, + height: 1.3, + ), + ), + const SizedBox(height: 12), + Row( + children: [ + MyButton( + variant: MyButtonVariant.primaryDense, + onPressed: () async { + await UserService.update( + (u) => u.isCloudBackupEnabled = true, + ); + unawaited(memoriesCloudService.checkUploads()); + }, + child: Text(context.lang.enable), + ), + const SizedBox(width: 8), + MyButton( + variant: MyButtonVariant.secondaryDense, + onPressed: () async { + await UserService.update( + (u) => u.hideMemoriesBackupPromo = true, + ); + }, + child: Text( + context.lang.settingsStorageHidePromo, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/src/visual/views/memories/memories.view.dart b/lib/src/visual/views/memories/memories.view.dart index 867e5516..4664cb63 100644 --- a/lib/src/visual/views/memories/memories.view.dart +++ b/lib/src/visual/views/memories/memories.view.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'package:drift/drift.dart' show Value; import 'package:flutter/material.dart'; import 'package:twonly/locator.dart'; @@ -10,6 +11,7 @@ import 'package:twonly/src/visual/components/alert.dialog.dart'; import 'package:twonly/src/visual/components/delete_memories_dialog.comp.dart'; import 'package:twonly/src/visual/components/draggable_scrollbar.comp.dart'; import 'package:twonly/src/visual/components/snackbar.dart'; +import 'package:twonly/src/visual/views/memories/components/cloud_backup_promo.comp.dart'; import 'package:twonly/src/visual/views/memories/components/flashback_banner.comp.dart'; import 'package:twonly/src/visual/views/memories/components/memory_thumbnail.comp.dart'; import 'package:twonly/src/visual/views/memories/components/selection_menu.comp.dart'; @@ -680,6 +682,7 @@ class MemoriesViewState extends State onOpenFlashback: (items, idx) => _openViewer(items, idx, isFlashback: true), ), + const MemoriesCloudBackupPromoComp(), for (final month in months) ...[ SliverPadding( padding: const EdgeInsets.fromLTRB( diff --git a/lib/src/visual/views/settings/data_and_storage/manage_storage.view.dart b/lib/src/visual/views/settings/data_and_storage/manage_storage.view.dart index 6731d122..9eb13ff8 100644 --- a/lib/src/visual/views/settings/data_and_storage/manage_storage.view.dart +++ b/lib/src/visual/views/settings/data_and_storage/manage_storage.view.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; @@ -9,7 +10,9 @@ import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.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'; class ManageStorageView extends StatefulWidget { const ManageStorageView({super.key}); @@ -117,84 +120,123 @@ class _ManageStorageViewState extends State { const SizedBox(height: 24), ] else ...[ Text( - 'Memories Backup', + context.lang.memoriesBackupTitle, style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 8), - Text( - _memoriesUsage != null - ? '${formatBytes(_memoriesUsage!.currentBytes.toInt())} / ${formatBytes(_memoriesUsage!.maxBytes.toInt())}' - : '-', - style: Theme.of(context).textTheme.headlineMedium?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 24), - Container( - height: 24, - width: double.infinity, - decoration: BoxDecoration( - color: Colors.grey.withValues(alpha: 0.2), - borderRadius: BorderRadius.circular(12), - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(12), - child: LayoutBuilder( - builder: (context, constraints) { - if (_memoriesUsage == null || - _memoriesUsage!.maxBytes == 0) { - return const SizedBox.shrink(); - } - - final maxWidth = constraints.maxWidth; - final current = _memoriesUsage!.currentBytes.toDouble(); - final max = _memoriesUsage!.maxBytes.toDouble(); - final usageWidth = - ((current / max).clamp(0.0, 1.0)) * maxWidth; - - return Row( + if (!userService.currentUser.isCloudBackupEnabled) ...[ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (usageWidth > 0) - Container( - width: usageWidth, - color: Colors.blue, - ), + Text( + context.lang.settingsStorageNoCloudBackupCard, + style: Theme.of(context).textTheme.bodyMedium + ?.copyWith( + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + ), ], - ); - }, + ), + ), + const SizedBox(width: 16), + MyButton( + variant: MyButtonVariant.primaryMiddle, + onPressed: () async { + await UserService.update( + (u) => u.isCloudBackupEnabled = true, + ); + setState(() {}); + unawaited(memoriesCloudService.checkUploads()); + }, + child: Text(context.lang.enable), + ), + ], + ), + const SizedBox(height: 24), + const Divider(), + const SizedBox(height: 24), + ] else ...[ + Text( + _memoriesUsage != null + ? '${formatBytes(_memoriesUsage!.currentBytes.toInt())} / ${formatBytes(_memoriesUsage!.maxBytes.toInt())}' + : '-', + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.bold, ), ), - ), - StreamBuilder( - initialData: memoriesCloudService.currentProgress, - stream: memoriesCloudService.progressStream, - builder: (context, snapshot) { - final progress = snapshot.data; - if (progress == null || progress.totalPending == 0) { - return const SizedBox.shrink(); - } - final percent = - (progress.currentUploaded / progress.totalPending) + - (progress.currentUploadProgress / progress.totalPending); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 12), - Text( - 'Syncing: ${progress.currentUploaded} / ${progress.totalPending} files (${(percent * 100).toStringAsFixed(1)}%)', - style: const TextStyle(fontSize: 14), - ), - const SizedBox(height: 6), - LinearProgressIndicator( - value: percent.clamp(0.0, 1.0), - ), - ], - ); - }, - ), - const SizedBox(height: 24), - const Divider(), - const SizedBox(height: 24), + const SizedBox(height: 24), + Container( + height: 24, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.grey.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(12), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: LayoutBuilder( + builder: (context, constraints) { + if (_memoriesUsage == null || + _memoriesUsage!.maxBytes == 0) { + return const SizedBox.shrink(); + } + + final maxWidth = constraints.maxWidth; + final current = _memoriesUsage!.currentBytes.toDouble(); + final max = _memoriesUsage!.maxBytes.toDouble(); + final usageWidth = + ((current / max).clamp(0.0, 1.0)) * maxWidth; + + return Row( + children: [ + if (usageWidth > 0) + Container( + width: usageWidth, + color: Colors.blue, + ), + ], + ); + }, + ), + ), + ), + StreamBuilder( + initialData: memoriesCloudService.currentProgress, + stream: memoriesCloudService.progressStream, + builder: (context, snapshot) { + final progress = snapshot.data; + if (progress == null || progress.totalPending == 0) { + return const SizedBox.shrink(); + } + final percent = + (progress.currentUploaded / progress.totalPending) + + (progress.currentUploadProgress / progress.totalPending); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 12), + Text( + 'Syncing: ${progress.currentUploaded} / ${progress.totalPending} files (${(percent * 100).toStringAsFixed(1)}%)', + style: const TextStyle(fontSize: 14), + ), + const SizedBox(height: 6), + LinearProgressIndicator( + value: percent.clamp(0.0, 1.0), + ), + ], + ); + }, + ), + const SizedBox(height: 24), + const Divider(), + const SizedBox(height: 24), + ], ], Text( isFreePlan