mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-09-01 08:04:07 +00:00
add promo for enabling e2ee backup
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
This commit is contained in:
parent
54538a6c31
commit
fb9acc690a
10 changed files with 264 additions and 76 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -2415,4 +2415,10 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get galleryActionShare => 'Teilen';
|
||||
|
||||
@override
|
||||
String get settingsStorageHidePromo => 'Ausblenden';
|
||||
|
||||
@override
|
||||
String get memoriesBackupTitle => 'Memories Backup';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2394,4 +2394,10 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get galleryActionShare => 'Share';
|
||||
|
||||
@override
|
||||
String get settingsStorageHidePromo => 'Hide';
|
||||
|
||||
@override
|
||||
String get memoriesBackupTitle => 'Memories Backup';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 9d05713a1f69e7e4de742c414df7df6c4c7925a7
|
||||
Subproject commit f72c61787096a44e2252058c08e4c14a0a8e5cff
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ UserData _$UserDataFromJson(Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> json) =>
|
|||
: TwonlySafeBackup.fromJson(
|
||||
json['twonlySafeBackup'] as Map<String, dynamic>,
|
||||
)
|
||||
..isBackupEnabled = json['isBackupEnabled'] as bool? ?? true
|
||||
..isBackupEnabled = json['isBackupEnabled'] as bool? ?? false
|
||||
..passwordLessRecovery = json['passwordLessRecovery'] == null
|
||||
? null
|
||||
: PasswordLessRecovery.fromJson(
|
||||
|
|
@ -153,6 +156,7 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
|
|||
?.toIso8601String(),
|
||||
'allowErrorTrackingViaSentry': instance.allowErrorTrackingViaSentry,
|
||||
'screenLockEnabled': instance.screenLockEnabled,
|
||||
'isCloudBackupEnabled': instance.isCloudBackupEnabled,
|
||||
'isUserDiscoveryEnabled': instance.isUserDiscoveryEnabled,
|
||||
'requiredSendImages': instance.requiredSendImages,
|
||||
'userDiscoveryThreshold': instance.userDiscoveryThreshold,
|
||||
|
|
@ -165,6 +169,7 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
|
|||
'currentSignedPreKeyIndexStart': instance.currentSignedPreKeyIndexStart,
|
||||
'lastChangeLogHash': instance.lastChangeLogHash,
|
||||
'hideChangeLog': instance.hideChangeLog,
|
||||
'hideMemoriesBackupPromo': instance.hideMemoriesBackupPromo,
|
||||
'updateFCMToken': instance.updateFCMToken,
|
||||
'canUseLoginTokenForAuth': instance.canUseLoginTokenForAuth,
|
||||
'twonlySafeBackup': instance.twonlySafeBackup,
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ class MemoriesCloudService {
|
|||
}
|
||||
|
||||
Future<void> checkUploads() async {
|
||||
if (_isProcessing || !userService.currentUser.isBackupEnabled) return;
|
||||
if (_isProcessing || !userService.currentUser.isCloudBackupEnabled) return;
|
||||
|
||||
try {
|
||||
final memories = await twonlyDB.mediaFilesDao.getMemoriesToBackup();
|
||||
|
|
|
|||
|
|
@ -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<void>(
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<MemoriesView>
|
|||
onOpenFlashback: (items, idx) =>
|
||||
_openViewer(items, idx, isFlashback: true),
|
||||
),
|
||||
const MemoriesCloudBackupPromoComp(),
|
||||
for (final month in months) ...[
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
|
|
|
|||
|
|
@ -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<ManageStorageView> {
|
|||
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<MemoriesBackupProgress>(
|
||||
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<MemoriesBackupProgress>(
|
||||
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
|
||||
|
|
|
|||
Loading…
Reference in a new issue