mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-09-01 07:24:07 +00:00
improve backup naming and user experience
This commit is contained in:
parent
3c8244b977
commit
753500ebfa
11 changed files with 561 additions and 465 deletions
|
|
@ -1541,7 +1541,7 @@ abstract class AppLocalizations {
|
|||
/// No description provided for @backupArchiveHeader.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Contacts, Settings and Messages'**
|
||||
/// **'Contacts & Messages'**
|
||||
String get backupArchiveHeader;
|
||||
|
||||
/// No description provided for @backupLastBackupDate.
|
||||
|
|
@ -1595,7 +1595,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 twonly Backup.'**
|
||||
/// **'Choose a secure password. This is required if you want to restore your backup.'**
|
||||
String get backupSelectStrongPassword;
|
||||
|
||||
/// No description provided for @password.
|
||||
|
|
@ -1652,6 +1652,36 @@ abstract class AppLocalizations {
|
|||
/// **'You can only change your password after you have authenticated!'**
|
||||
String get backupChangePasswordAuthFailed;
|
||||
|
||||
/// No description provided for @backupFreeSpaceWithCloud.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Free Space with Cloud Backup'**
|
||||
String get backupFreeSpaceWithCloud;
|
||||
|
||||
/// No description provided for @backupMemoriesNotEnabled.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Not enabled'**
|
||||
String get backupMemoriesNotEnabled;
|
||||
|
||||
/// No description provided for @backupMemoriesUpgradeRequired.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Upgrade required'**
|
||||
String get backupMemoriesUpgradeRequired;
|
||||
|
||||
/// No description provided for @todayAt.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Today at {time}'**
|
||||
String todayAt(Object time);
|
||||
|
||||
/// No description provided for @yesterdayAt.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Yesterday at {time}'**
|
||||
String yesterdayAt(Object time);
|
||||
|
||||
/// No description provided for @twonlySafeRecoverTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -3671,7 +3701,7 @@ abstract class AppLocalizations {
|
|||
/// No description provided for @passwordlessRecovery.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Passwordless Recovery'**
|
||||
/// **'Password Recovery'**
|
||||
String get passwordlessRecovery;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryNotConfigured.
|
||||
|
|
@ -4193,7 +4223,7 @@ abstract class AppLocalizations {
|
|||
/// No description provided for @settingsStorageContents.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Storage contents'**
|
||||
/// **'Free up space'**
|
||||
String get settingsStorageContents;
|
||||
|
||||
/// No description provided for @settingsStorageSortStorage.
|
||||
|
|
|
|||
|
|
@ -817,7 +817,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
String get backupIdentityHeader => 'Identität';
|
||||
|
||||
@override
|
||||
String get backupArchiveHeader => 'Kontakte, Einstellungen und Nachrichten';
|
||||
String get backupArchiveHeader => 'Kontakte & Nachrichten';
|
||||
|
||||
@override
|
||||
String get backupLastBackupDate => 'Letztes Backup';
|
||||
|
|
@ -847,7 +847,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get backupSelectStrongPassword =>
|
||||
'Wähle ein sicheres Passwort. Dies ist erforderlich, wenn du dein twonly Backup wiederherstellen möchtest.';
|
||||
'Wähle ein sicheres Passwort. Dies ist erforderlich, wenn du dein Backup wiederherstellen möchtest.';
|
||||
|
||||
@override
|
||||
String get password => 'Passwort';
|
||||
|
|
@ -878,6 +878,26 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
String get backupChangePasswordAuthFailed =>
|
||||
'Du kannst dein Passwort nur ändern, wenn du dich authentifiziert hast!';
|
||||
|
||||
@override
|
||||
String get backupFreeSpaceWithCloud =>
|
||||
'Speicherplatz mit Cloud-Backup freigeben';
|
||||
|
||||
@override
|
||||
String get backupMemoriesNotEnabled => 'Nicht aktiviert';
|
||||
|
||||
@override
|
||||
String get backupMemoriesUpgradeRequired => 'Upgrade erforderlich';
|
||||
|
||||
@override
|
||||
String todayAt(Object time) {
|
||||
return 'Heute um $time';
|
||||
}
|
||||
|
||||
@override
|
||||
String yesterdayAt(Object time) {
|
||||
return 'Gestern um $time';
|
||||
}
|
||||
|
||||
@override
|
||||
String get twonlySafeRecoverTitle => 'Backup wiederherstellen';
|
||||
|
||||
|
|
@ -2118,7 +2138,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
String get avatarCustomizeReset => 'Zurücksetzen';
|
||||
|
||||
@override
|
||||
String get passwordlessRecovery => 'Passwortloses Backup';
|
||||
String get passwordlessRecovery => 'Passwort vergessen';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryNotConfigured => 'Nicht konfiguriert';
|
||||
|
|
@ -2184,7 +2204,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get passwordlessRecoveryEnableSuccess =>
|
||||
'Passwortloses Backup erfolgreich aktiviert!';
|
||||
'\"Passwort vergessen\" erfolgreich aktiviert!';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryEnterPin => 'Bitte gib eine PIN ein.';
|
||||
|
|
@ -2194,13 +2214,16 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
'Bitte gib eine E-Mail-Adresse ein.';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryEnableBtn => 'Passwortloses Backup aktivieren';
|
||||
String get passwordlessRecoveryEnableBtn =>
|
||||
'\"Passwort vergessen\" aktivieren';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryRecoverBtn => 'Passwortlos wiederherstellen';
|
||||
String get passwordlessRecoveryRecoverBtn =>
|
||||
'Mit \"Passwort vergessen\" wiederherstellen';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryModifyBtn => 'Passwortloses Backup bearbeiten';
|
||||
String get passwordlessRecoveryModifyBtn =>
|
||||
'\"Passwort vergessen\" bearbeiten';
|
||||
|
||||
@override
|
||||
String passwordlessRecoveryStatusEnabled(num count) {
|
||||
|
|
@ -2426,7 +2449,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
String get settingsHelpNews => 'Neuigkeiten';
|
||||
|
||||
@override
|
||||
String get settingsStorageContents => 'Speicherinhalte';
|
||||
String get settingsStorageContents => 'Speicherplatz freigeben';
|
||||
|
||||
@override
|
||||
String get settingsStorageSortStorage => 'Belegter Speicher';
|
||||
|
|
|
|||
|
|
@ -812,7 +812,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
String get backupIdentityHeader => 'Identity';
|
||||
|
||||
@override
|
||||
String get backupArchiveHeader => 'Contacts, Settings and Messages';
|
||||
String get backupArchiveHeader => 'Contacts & Messages';
|
||||
|
||||
@override
|
||||
String get backupLastBackupDate => 'Last backup';
|
||||
|
|
@ -842,7 +842,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get backupSelectStrongPassword =>
|
||||
'Choose a secure password. This is required if you want to restore your twonly Backup.';
|
||||
'Choose a secure password. This is required if you want to restore your backup.';
|
||||
|
||||
@override
|
||||
String get password => 'Password';
|
||||
|
|
@ -873,6 +873,25 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
String get backupChangePasswordAuthFailed =>
|
||||
'You can only change your password after you have authenticated!';
|
||||
|
||||
@override
|
||||
String get backupFreeSpaceWithCloud => 'Free Space with Cloud Backup';
|
||||
|
||||
@override
|
||||
String get backupMemoriesNotEnabled => 'Not enabled';
|
||||
|
||||
@override
|
||||
String get backupMemoriesUpgradeRequired => 'Upgrade required';
|
||||
|
||||
@override
|
||||
String todayAt(Object time) {
|
||||
return 'Today at $time';
|
||||
}
|
||||
|
||||
@override
|
||||
String yesterdayAt(Object time) {
|
||||
return 'Yesterday at $time';
|
||||
}
|
||||
|
||||
@override
|
||||
String get twonlySafeRecoverTitle => 'Restore backup';
|
||||
|
||||
|
|
@ -2104,7 +2123,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
String get avatarCustomizeReset => 'Reset';
|
||||
|
||||
@override
|
||||
String get passwordlessRecovery => 'Passwordless Recovery';
|
||||
String get passwordlessRecovery => 'Password Recovery';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryNotConfigured => 'Not configured';
|
||||
|
|
@ -2405,7 +2424,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
String get settingsHelpNews => 'News';
|
||||
|
||||
@override
|
||||
String get settingsStorageContents => 'Storage contents';
|
||||
String get settingsStorageContents => 'Free up space';
|
||||
|
||||
@override
|
||||
String get settingsStorageSortStorage => 'Occupied storage';
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit d08a6d4261135b80fba6a02f1e7e70fefcf17b8d
|
||||
Subproject commit 659bfb084ea26d5471dea6c6d8e1c950c95ecea2
|
||||
|
|
@ -264,6 +264,28 @@ String formatDateTime(BuildContext context, DateTime? dateTime) {
|
|||
}
|
||||
}
|
||||
|
||||
String formatRelativeDateTime(BuildContext context, DateTime? dateTime) {
|
||||
if (dateTime == null) {
|
||||
return '-';
|
||||
}
|
||||
final now = clock.now();
|
||||
final today = DateTime(now.year, now.month, now.day);
|
||||
final yesterday = today.subtract(const Duration(days: 1));
|
||||
final dateDay = DateTime(dateTime.year, dateTime.month, dateTime.day);
|
||||
|
||||
final time = DateFormat.Hm(
|
||||
Localizations.localeOf(context).toLanguageTag(),
|
||||
).format(dateTime);
|
||||
|
||||
if (dateDay == today) {
|
||||
return context.lang.todayAt(time);
|
||||
} else if (dateDay == yesterday) {
|
||||
return context.lang.yesterdayAt(time);
|
||||
} else {
|
||||
return formatDateTime(context, dateTime);
|
||||
}
|
||||
}
|
||||
|
||||
String formatBytes(int bytes) {
|
||||
if (bytes <= 0) return '0 Bytes';
|
||||
const units = <String>['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ class _BackupSetupPageState extends State<BackupSetupPage> {
|
|||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'twonly Backup',
|
||||
context.lang.settingsBackup,
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,13 +1,23 @@
|
|||
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/model/json/backup.model.dart';
|
||||
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'
|
||||
as server;
|
||||
import 'package:twonly/src/providers/purchases.provider.dart';
|
||||
import 'package:twonly/src/services/backup.service.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/better_list_title.element.dart';
|
||||
import 'package:twonly/src/visual/elements/my_button.element.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';
|
||||
|
||||
|
|
@ -21,6 +31,7 @@ class BackupView extends StatefulWidget {
|
|||
class _BackupViewState extends State<BackupView> {
|
||||
bool _isLoading = false;
|
||||
CurrentBackupStatus? _backupStatus;
|
||||
server.Response_MemoriesUsage? _memoriesUsage;
|
||||
StreamSubscription<void>? _backupUpdateSub;
|
||||
|
||||
@override
|
||||
|
|
@ -41,51 +52,29 @@ class _BackupViewState extends State<BackupView> {
|
|||
Future<void> _loadBackupStatus() async {
|
||||
setState(() => _isLoading = true);
|
||||
final status = await BackupService.getData();
|
||||
final memoriesUsage = await apiService.getMemoriesUsage();
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_backupStatus = status;
|
||||
_memoriesUsage = memoriesUsage;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
String _getBackupStatusString(LastBackupUploadState status) {
|
||||
switch (status) {
|
||||
case LastBackupUploadState.none:
|
||||
return context.lang.backupPending;
|
||||
case LastBackupUploadState.pending:
|
||||
return context.lang.backupPending;
|
||||
case LastBackupUploadState.failed:
|
||||
return context.lang.backupFailed;
|
||||
case LastBackupUploadState.success:
|
||||
return context.lang.backupSuccess;
|
||||
String _buildTileSubtitle(DateTime? date, int? size) {
|
||||
if (date == null) return '-';
|
||||
final dateStr = formatRelativeDateTime(context, date);
|
||||
if (size != null && size > 0) {
|
||||
return '$dateStr • ${formatBytes(size)}';
|
||||
}
|
||||
}
|
||||
|
||||
List<TableRow> _buildTableRows(List<(String, String)> rows) {
|
||||
return rows.map((pair) {
|
||||
return TableRow(
|
||||
children: [
|
||||
TableCell(
|
||||
child: Text(pair.$1),
|
||||
),
|
||||
TableCell(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 4,
|
||||
),
|
||||
child: Text(
|
||||
pair.$2,
|
||||
textAlign: TextAlign.right,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}).toList();
|
||||
return dateStr;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final currentPlan = context.watch<PurchasesProvider>().plan;
|
||||
final isFreePlan = currentPlan == SubscriptionPlan.Free;
|
||||
|
||||
return StreamBuilder<void>(
|
||||
stream: userService.onUserUpdated,
|
||||
builder: (context, _) {
|
||||
|
|
@ -94,109 +83,105 @@ class _BackupViewState extends State<BackupView> {
|
|||
title: Text(context.lang.settingsBackup),
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: ListView(
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
context.lang.backupTwonlySafeDesc,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
if (userService.currentUser.passwordLessRecovery != null)
|
||||
const PasswordLessRecoveryStatus(),
|
||||
|
||||
if (userService.currentUser.isBackupEnabled)
|
||||
Column(
|
||||
children: [
|
||||
const SizedBox(height: 32),
|
||||
Center(
|
||||
child: Text(
|
||||
context.lang.backupIdentityHeader,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
child: PasswordLessRecoveryStatus(),
|
||||
)
|
||||
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),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Table(
|
||||
defaultVerticalAlignment:
|
||||
TableCellVerticalAlignment.middle,
|
||||
children: _buildTableRows([
|
||||
(
|
||||
context.lang.backupLastBackupDate,
|
||||
_backupStatus?.identityLastSuccessFull != null
|
||||
? formatDateTime(
|
||||
context,
|
||||
_backupStatus!.identityLastSuccessFull,
|
||||
)
|
||||
: '-',
|
||||
),
|
||||
(
|
||||
context.lang.backupLastBackupSize,
|
||||
_backupStatus?.identitySize != null
|
||||
? formatBytes(_backupStatus!.identitySize!)
|
||||
: '-',
|
||||
),
|
||||
(
|
||||
context.lang.backupLastBackupResult,
|
||||
_getBackupStatusString(
|
||||
_backupStatus?.identityState ??
|
||||
LastBackupUploadState.none,
|
||||
),
|
||||
),
|
||||
]),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Center(
|
||||
child: Text(
|
||||
context.lang.backupArchiveHeader,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Table(
|
||||
defaultVerticalAlignment:
|
||||
TableCellVerticalAlignment.middle,
|
||||
children: _buildTableRows([
|
||||
(
|
||||
context.lang.backupLastBackupDate,
|
||||
_backupStatus?.archiveLastSuccessFull != null
|
||||
? formatDateTime(
|
||||
context,
|
||||
_backupStatus!.archiveLastSuccessFull,
|
||||
)
|
||||
: '-',
|
||||
),
|
||||
(
|
||||
context.lang.backupLastBackupSize,
|
||||
_backupStatus?.archiveSize != null
|
||||
? formatBytes(_backupStatus!.archiveSize!)
|
||||
: '-',
|
||||
),
|
||||
(
|
||||
context.lang.backupLastBackupResult,
|
||||
_getBackupStatusString(
|
||||
_backupStatus?.archiveState ??
|
||||
LastBackupUploadState.none,
|
||||
),
|
||||
),
|
||||
]),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
if (userService.currentUser.passwordLessRecovery == null) ...[
|
||||
const SizedBox(height: 20),
|
||||
Center(
|
||||
child: MyButton(
|
||||
variant: MyButtonVariant.primaryMiddle,
|
||||
onPressed: () =>
|
||||
context.navPush(const PasswordLessRecoverySetup()),
|
||||
child: Text(context.lang.passwordlessRecoveryEnableBtn),
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
context.lang.backupTwonlySafeDesc,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: context.color.onSurfaceVariant,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Divider(),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
if (userService.currentUser.isBackupEnabled) ...[
|
||||
// 1. Identity tile
|
||||
BetterListTile(
|
||||
icon: FontAwesomeIcons.userCheck,
|
||||
text: context.lang.backupIdentityHeader,
|
||||
subtitle: Text(
|
||||
_buildTileSubtitle(
|
||||
_backupStatus?.identityLastSuccessFull,
|
||||
_backupStatus?.identitySize,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 2. Contacts & Messages tile
|
||||
BetterListTile(
|
||||
icon: FontAwesomeIcons.comments,
|
||||
text: context.lang.backupArchiveHeader,
|
||||
subtitle: Text(
|
||||
_buildTileSubtitle(
|
||||
_backupStatus?.archiveLastSuccessFull,
|
||||
_backupStatus?.archiveSize,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 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),
|
||||
],
|
||||
|
||||
Center(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
|
|
@ -212,9 +197,7 @@ class _BackupViewState extends State<BackupView> {
|
|||
_isLoading = true;
|
||||
});
|
||||
await BackupService.makeBackup(force: true);
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
await _loadBackupStatus();
|
||||
},
|
||||
child: Text(context.lang.backupTwonlySaveNow),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ class _SetupBackupViewState extends State<SetupBackupView> {
|
|||
onTap: () => FocusScope.of(context).unfocus(),
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('twonly Backup'),
|
||||
title: Text(context.lang.settingsBackup),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () => showBackupExplanation(context),
|
||||
|
|
@ -127,12 +127,6 @@ class _SetupBackupViewState extends State<SetupBackupView> {
|
|||
_repeatedController.text.isNotEmpty,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
context.lang.backupNoPasswordRecovery,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Center(
|
||||
child: MyButton(
|
||||
variant: MyButtonVariant.primaryMiddle,
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ void showBackupExplanation(BuildContext context) {
|
|||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'twonly Backup',
|
||||
context.lang.settingsBackup,
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,312 @@
|
|||
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/data_and_storage/storage_contents.view.dart';
|
||||
|
||||
class MemoriesBackupDetailView extends StatefulWidget {
|
||||
const MemoriesBackupDetailView({super.key});
|
||||
|
||||
@override
|
||||
State<MemoriesBackupDetailView> createState() =>
|
||||
_MemoriesBackupDetailViewState();
|
||||
}
|
||||
|
||||
class _MemoriesBackupDetailViewState extends State<MemoriesBackupDetailView> {
|
||||
server.Response_MemoriesUsage? _memoriesUsage;
|
||||
int _cloudOnlyCount = 0;
|
||||
bool _isLoading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadStats();
|
||||
}
|
||||
|
||||
Future<void> _loadStats() async {
|
||||
final memoriesUsage = await apiService.getMemoriesUsage();
|
||||
final cloudOnlyCount = await twonlyDB.mediaFilesDao
|
||||
.getCloudOnlyMemoriesCount();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_memoriesUsage = memoriesUsage;
|
||||
_cloudOnlyCount = cloudOnlyCount;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _disableBackup() async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.lang.memoriesBackupTitle),
|
||||
),
|
||||
body: _isLoading
|
||||
? const Center(child: CircularProgressIndicator.adaptive())
|
||||
: ListView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
children: [
|
||||
Text(
|
||||
_memoriesUsage != null
|
||||
? '${formatBytes(_memoriesUsage!.currentBytes.toInt())} / ${formatBytes(_memoriesUsage!.maxBytes.toInt())}'
|
||||
: '-',
|
||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
StreamBuilder<MemoriesBackupProgress?>(
|
||||
initialData: memoriesCloudService.currentProgress,
|
||||
stream: memoriesCloudService.progressStream,
|
||||
builder: (context, snapshot) {
|
||||
final progress = snapshot.data;
|
||||
final isSyncing =
|
||||
progress != null && progress.totalPending > 0;
|
||||
final percent = isSyncing
|
||||
? ((progress.currentUploaded / progress.totalPending) +
|
||||
(progress.currentUploadProgress /
|
||||
progress.totalPending))
|
||||
.clamp(0.0, 1.0)
|
||||
: 0.0;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (isSyncing) ...[
|
||||
Text(
|
||||
'Syncing: ${progress.currentUploaded} / ${progress.totalPending} files (${(percent * 100).toStringAsFixed(1)}%)',
|
||||
style: const TextStyle(fontSize: 14),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
LinearProgressIndicator(
|
||||
value: percent,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
Center(
|
||||
child: MyButton(
|
||||
variant: MyButtonVariant.primaryMiddle,
|
||||
onPressed: () =>
|
||||
context.navPush(const StorageContentsView()),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.folder_open_outlined,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(context.lang.settingsStorageContents),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Center(
|
||||
child: MyButton(
|
||||
variant: MyButtonVariant.secondaryDense,
|
||||
onPressed: isSyncing
|
||||
? null
|
||||
: () async {
|
||||
final memories = await twonlyDB
|
||||
.mediaFilesDao
|
||||
.getMemoriesToBackup();
|
||||
if (memories.isEmpty) {
|
||||
if (context.mounted) {
|
||||
showSnackbar(
|
||||
context,
|
||||
context
|
||||
.lang
|
||||
.settingsStorageSyncUpToDate,
|
||||
level: SnackbarLevel.success,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
unawaited(
|
||||
memoriesCloudService.checkUploads(),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.sync, size: 18),
|
||||
const SizedBox(width: 8),
|
||||
Text(context.lang.settingsStorageSyncNow),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Center(
|
||||
child: MyButton(
|
||||
variant: MyButtonVariant.secondaryDense,
|
||||
onPressed: _disableBackup,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.cloud_off_outlined, size: 18),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
context
|
||||
.lang
|
||||
.settingsStorageDisableBackupAction,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +1,12 @@
|
|||
import 'dart:async';
|
||||
import 'package:drift/drift.dart' hide Column;
|
||||
import 'package:flutter/material.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/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/providers/purchases.provider.dart';
|
||||
import 'package:twonly/src/services/mediafiles/mediafile.service.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/components/snackbar.dart';
|
||||
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||
import 'package:twonly/src/visual/views/settings/data_and_storage/storage_contents.view.dart';
|
||||
|
||||
|
|
@ -28,8 +19,6 @@ class ManageStorageView extends StatefulWidget {
|
|||
|
||||
class _ManageStorageViewState extends State<ManageStorageView> {
|
||||
Map<MediaType, int> _stats = {};
|
||||
server.Response_MemoriesUsage? _memoriesUsage;
|
||||
int _cloudOnlyCount = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -39,110 +28,13 @@ class _ManageStorageViewState extends State<ManageStorageView> {
|
|||
|
||||
Future<void> _loadStats() async {
|
||||
final stats = await twonlyDB.mediaFilesDao.getStorageStats();
|
||||
final memoriesUsage = await apiService.getMemoriesUsage();
|
||||
final cloudOnlyCount = await twonlyDB.mediaFilesDao
|
||||
.getCloudOnlyMemoriesCount();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_stats = stats;
|
||||
_memoriesUsage = memoriesUsage;
|
||||
_cloudOnlyCount = cloudOnlyCount;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _disableBackup() async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
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);
|
||||
await _loadStats();
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
showSnackbar(context, e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final currentPlan = context.watch<PurchasesProvider>().plan;
|
||||
|
|
@ -162,7 +54,7 @@ class _ManageStorageViewState extends State<ManageStorageView> {
|
|||
body: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
if (isFreePlan) ...[
|
||||
if (isFreePlan || !userService.currentUser.isCloudBackupEnabled) ...[
|
||||
Card(
|
||||
elevation: 0,
|
||||
color: Theme.of(
|
||||
|
|
@ -177,223 +69,44 @@ class _ManageStorageViewState extends State<ManageStorageView> {
|
|||
),
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: () => context.push(Routes.settingsSubscription),
|
||||
onTap: () {
|
||||
if (isFreePlan) {
|
||||
context.push(Routes.settingsSubscription);
|
||||
} else {
|
||||
context.push(Routes.settingsBackup);
|
||||
}
|
||||
},
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.cloud_off_outlined,
|
||||
Icons.cloud_queue_rounded,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
context.lang.settingsStorageNoCloudBackupTitle,
|
||||
style: Theme.of(context).textTheme.titleSmall
|
||||
?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
context.lang.settingsStorageNoCloudBackupCard,
|
||||
style: Theme.of(context).textTheme.bodySmall
|
||||
?.copyWith(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
child: Text(
|
||||
context.lang.backupFreeSpaceWithCloud,
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.chevron_right_rounded,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
] else ...[
|
||||
Text(
|
||||
context.lang.memoriesBackupTitle,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (!userService.currentUser.isCloudBackupEnabled) ...[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
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,
|
||||
),
|
||||
),
|
||||
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;
|
||||
final isSyncing =
|
||||
progress != null && progress.totalPending > 0;
|
||||
final percent = isSyncing
|
||||
? ((progress.currentUploaded / progress.totalPending) +
|
||||
(progress.currentUploadProgress /
|
||||
progress.totalPending))
|
||||
.clamp(0.0, 1.0)
|
||||
: 0.0;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (isSyncing) ...[
|
||||
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,
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
Align(
|
||||
child: MyButton(
|
||||
variant: MyButtonVariant.primaryDense,
|
||||
onPressed: isSyncing
|
||||
? null
|
||||
: () async {
|
||||
final memories = await twonlyDB.mediaFilesDao
|
||||
.getMemoriesToBackup();
|
||||
if (memories.isEmpty) {
|
||||
if (context.mounted) {
|
||||
showSnackbar(
|
||||
context,
|
||||
context
|
||||
.lang
|
||||
.settingsStorageSyncUpToDate,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
unawaited(
|
||||
memoriesCloudService.checkUploads(),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.sync, size: 18),
|
||||
const SizedBox(width: 8),
|
||||
Text(context.lang.settingsStorageSyncNow),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Align(
|
||||
child: MyButton(
|
||||
variant: MyButtonVariant.secondaryDense,
|
||||
onPressed: _disableBackup,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.cloud_off_outlined, size: 18),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
context.lang.settingsStorageDisableBackupAction,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const Divider(),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
const Divider(),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
Text(
|
||||
isFreePlan
|
||||
|
|
|
|||
Loading…
Reference in a new issue