mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-09-01 08:04: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.
|
/// No description provided for @backupArchiveHeader.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Contacts, Settings and Messages'**
|
/// **'Contacts & Messages'**
|
||||||
String get backupArchiveHeader;
|
String get backupArchiveHeader;
|
||||||
|
|
||||||
/// No description provided for @backupLastBackupDate.
|
/// No description provided for @backupLastBackupDate.
|
||||||
|
|
@ -1595,7 +1595,7 @@ abstract class AppLocalizations {
|
||||||
/// No description provided for @backupSelectStrongPassword.
|
/// No description provided for @backupSelectStrongPassword.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// 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;
|
String get backupSelectStrongPassword;
|
||||||
|
|
||||||
/// No description provided for @password.
|
/// No description provided for @password.
|
||||||
|
|
@ -1652,6 +1652,36 @@ abstract class AppLocalizations {
|
||||||
/// **'You can only change your password after you have authenticated!'**
|
/// **'You can only change your password after you have authenticated!'**
|
||||||
String get backupChangePasswordAuthFailed;
|
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.
|
/// No description provided for @twonlySafeRecoverTitle.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -3671,7 +3701,7 @@ abstract class AppLocalizations {
|
||||||
/// No description provided for @passwordlessRecovery.
|
/// No description provided for @passwordlessRecovery.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Passwordless Recovery'**
|
/// **'Password Recovery'**
|
||||||
String get passwordlessRecovery;
|
String get passwordlessRecovery;
|
||||||
|
|
||||||
/// No description provided for @passwordlessRecoveryNotConfigured.
|
/// No description provided for @passwordlessRecoveryNotConfigured.
|
||||||
|
|
@ -4193,7 +4223,7 @@ abstract class AppLocalizations {
|
||||||
/// No description provided for @settingsStorageContents.
|
/// No description provided for @settingsStorageContents.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Storage contents'**
|
/// **'Free up space'**
|
||||||
String get settingsStorageContents;
|
String get settingsStorageContents;
|
||||||
|
|
||||||
/// No description provided for @settingsStorageSortStorage.
|
/// No description provided for @settingsStorageSortStorage.
|
||||||
|
|
|
||||||
|
|
@ -817,7 +817,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
String get backupIdentityHeader => 'Identität';
|
String get backupIdentityHeader => 'Identität';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get backupArchiveHeader => 'Kontakte, Einstellungen und Nachrichten';
|
String get backupArchiveHeader => 'Kontakte & Nachrichten';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get backupLastBackupDate => 'Letztes Backup';
|
String get backupLastBackupDate => 'Letztes Backup';
|
||||||
|
|
@ -847,7 +847,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get backupSelectStrongPassword =>
|
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
|
@override
|
||||||
String get password => 'Passwort';
|
String get password => 'Passwort';
|
||||||
|
|
@ -878,6 +878,26 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
String get backupChangePasswordAuthFailed =>
|
String get backupChangePasswordAuthFailed =>
|
||||||
'Du kannst dein Passwort nur ändern, wenn du dich authentifiziert hast!';
|
'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
|
@override
|
||||||
String get twonlySafeRecoverTitle => 'Backup wiederherstellen';
|
String get twonlySafeRecoverTitle => 'Backup wiederherstellen';
|
||||||
|
|
||||||
|
|
@ -2118,7 +2138,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
String get avatarCustomizeReset => 'Zurücksetzen';
|
String get avatarCustomizeReset => 'Zurücksetzen';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get passwordlessRecovery => 'Passwortloses Backup';
|
String get passwordlessRecovery => 'Passwort vergessen';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get passwordlessRecoveryNotConfigured => 'Nicht konfiguriert';
|
String get passwordlessRecoveryNotConfigured => 'Nicht konfiguriert';
|
||||||
|
|
@ -2184,7 +2204,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get passwordlessRecoveryEnableSuccess =>
|
String get passwordlessRecoveryEnableSuccess =>
|
||||||
'Passwortloses Backup erfolgreich aktiviert!';
|
'\"Passwort vergessen\" erfolgreich aktiviert!';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get passwordlessRecoveryEnterPin => 'Bitte gib eine PIN ein.';
|
String get passwordlessRecoveryEnterPin => 'Bitte gib eine PIN ein.';
|
||||||
|
|
@ -2194,13 +2214,16 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
'Bitte gib eine E-Mail-Adresse ein.';
|
'Bitte gib eine E-Mail-Adresse ein.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get passwordlessRecoveryEnableBtn => 'Passwortloses Backup aktivieren';
|
String get passwordlessRecoveryEnableBtn =>
|
||||||
|
'\"Passwort vergessen\" aktivieren';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get passwordlessRecoveryRecoverBtn => 'Passwortlos wiederherstellen';
|
String get passwordlessRecoveryRecoverBtn =>
|
||||||
|
'Mit \"Passwort vergessen\" wiederherstellen';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get passwordlessRecoveryModifyBtn => 'Passwortloses Backup bearbeiten';
|
String get passwordlessRecoveryModifyBtn =>
|
||||||
|
'\"Passwort vergessen\" bearbeiten';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String passwordlessRecoveryStatusEnabled(num count) {
|
String passwordlessRecoveryStatusEnabled(num count) {
|
||||||
|
|
@ -2426,7 +2449,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
String get settingsHelpNews => 'Neuigkeiten';
|
String get settingsHelpNews => 'Neuigkeiten';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settingsStorageContents => 'Speicherinhalte';
|
String get settingsStorageContents => 'Speicherplatz freigeben';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settingsStorageSortStorage => 'Belegter Speicher';
|
String get settingsStorageSortStorage => 'Belegter Speicher';
|
||||||
|
|
|
||||||
|
|
@ -812,7 +812,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
String get backupIdentityHeader => 'Identity';
|
String get backupIdentityHeader => 'Identity';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get backupArchiveHeader => 'Contacts, Settings and Messages';
|
String get backupArchiveHeader => 'Contacts & Messages';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get backupLastBackupDate => 'Last backup';
|
String get backupLastBackupDate => 'Last backup';
|
||||||
|
|
@ -842,7 +842,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get backupSelectStrongPassword =>
|
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
|
@override
|
||||||
String get password => 'Password';
|
String get password => 'Password';
|
||||||
|
|
@ -873,6 +873,25 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
String get backupChangePasswordAuthFailed =>
|
String get backupChangePasswordAuthFailed =>
|
||||||
'You can only change your password after you have authenticated!';
|
'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
|
@override
|
||||||
String get twonlySafeRecoverTitle => 'Restore backup';
|
String get twonlySafeRecoverTitle => 'Restore backup';
|
||||||
|
|
||||||
|
|
@ -2104,7 +2123,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
String get avatarCustomizeReset => 'Reset';
|
String get avatarCustomizeReset => 'Reset';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get passwordlessRecovery => 'Passwordless Recovery';
|
String get passwordlessRecovery => 'Password Recovery';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get passwordlessRecoveryNotConfigured => 'Not configured';
|
String get passwordlessRecoveryNotConfigured => 'Not configured';
|
||||||
|
|
@ -2405,7 +2424,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
String get settingsHelpNews => 'News';
|
String get settingsHelpNews => 'News';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settingsStorageContents => 'Storage contents';
|
String get settingsStorageContents => 'Free up space';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settingsStorageSortStorage => 'Occupied storage';
|
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) {
|
String formatBytes(int bytes) {
|
||||||
if (bytes <= 0) return '0 Bytes';
|
if (bytes <= 0) return '0 Bytes';
|
||||||
const units = <String>['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
const units = <String>['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@ class _BackupSetupPageState extends State<BackupSetupPage> {
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'twonly Backup',
|
context.lang.settingsBackup,
|
||||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,23 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/constants/routes.keys.dart';
|
import 'package:twonly/src/constants/routes.keys.dart';
|
||||||
import 'package:twonly/src/model/json/backup.model.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/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/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/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/components/status.passwordless_recovery.comp.dart';
|
||||||
import 'package:twonly/src/visual/views/settings/backup/passwordless_recovery/setup.passwordless_recovery.view.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> {
|
class _BackupViewState extends State<BackupView> {
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
CurrentBackupStatus? _backupStatus;
|
CurrentBackupStatus? _backupStatus;
|
||||||
|
server.Response_MemoriesUsage? _memoriesUsage;
|
||||||
StreamSubscription<void>? _backupUpdateSub;
|
StreamSubscription<void>? _backupUpdateSub;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -41,51 +52,29 @@ class _BackupViewState extends State<BackupView> {
|
||||||
Future<void> _loadBackupStatus() async {
|
Future<void> _loadBackupStatus() async {
|
||||||
setState(() => _isLoading = true);
|
setState(() => _isLoading = true);
|
||||||
final status = await BackupService.getData();
|
final status = await BackupService.getData();
|
||||||
|
final memoriesUsage = await apiService.getMemoriesUsage();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_backupStatus = status;
|
_backupStatus = status;
|
||||||
|
_memoriesUsage = memoriesUsage;
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
String _getBackupStatusString(LastBackupUploadState status) {
|
String _buildTileSubtitle(DateTime? date, int? size) {
|
||||||
switch (status) {
|
if (date == null) return '-';
|
||||||
case LastBackupUploadState.none:
|
final dateStr = formatRelativeDateTime(context, date);
|
||||||
return context.lang.backupPending;
|
if (size != null && size > 0) {
|
||||||
case LastBackupUploadState.pending:
|
return '$dateStr • ${formatBytes(size)}';
|
||||||
return context.lang.backupPending;
|
|
||||||
case LastBackupUploadState.failed:
|
|
||||||
return context.lang.backupFailed;
|
|
||||||
case LastBackupUploadState.success:
|
|
||||||
return context.lang.backupSuccess;
|
|
||||||
}
|
}
|
||||||
}
|
return dateStr;
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final currentPlan = context.watch<PurchasesProvider>().plan;
|
||||||
|
final isFreePlan = currentPlan == SubscriptionPlan.Free;
|
||||||
|
|
||||||
return StreamBuilder<void>(
|
return StreamBuilder<void>(
|
||||||
stream: userService.onUserUpdated,
|
stream: userService.onUserUpdated,
|
||||||
builder: (context, _) {
|
builder: (context, _) {
|
||||||
|
|
@ -94,100 +83,18 @@ class _BackupViewState extends State<BackupView> {
|
||||||
title: Text(context.lang.settingsBackup),
|
title: Text(context.lang.settingsBackup),
|
||||||
),
|
),
|
||||||
body: Padding(
|
body: Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
child: ListView(
|
child: ListView(
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
context.lang.backupTwonlySafeDesc,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
|
|
||||||
if (userService.currentUser.passwordLessRecovery != null)
|
if (userService.currentUser.passwordLessRecovery != null)
|
||||||
const PasswordLessRecoveryStatus(),
|
const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||||
if (userService.currentUser.isBackupEnabled)
|
child: PasswordLessRecoveryStatus(),
|
||||||
Column(
|
|
||||||
children: [
|
|
||||||
const SizedBox(height: 32),
|
|
||||||
Center(
|
|
||||||
child: Text(
|
|
||||||
context.lang.backupIdentityHeader,
|
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Table(
|
|
||||||
defaultVerticalAlignment:
|
|
||||||
TableCellVerticalAlignment.middle,
|
|
||||||
children: _buildTableRows([
|
|
||||||
(
|
|
||||||
context.lang.backupLastBackupDate,
|
|
||||||
_backupStatus?.identityLastSuccessFull != null
|
|
||||||
? formatDateTime(
|
|
||||||
context,
|
|
||||||
_backupStatus!.identityLastSuccessFull,
|
|
||||||
)
|
)
|
||||||
: '-',
|
else
|
||||||
),
|
Padding(
|
||||||
(
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
context.lang.backupLastBackupSize,
|
child: Center(
|
||||||
_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(
|
child: MyButton(
|
||||||
variant: MyButtonVariant.primaryMiddle,
|
variant: MyButtonVariant.primaryMiddle,
|
||||||
onPressed: () =>
|
onPressed: () =>
|
||||||
|
|
@ -195,8 +102,86 @@ class _BackupViewState extends State<BackupView> {
|
||||||
child: Text(context.lang.passwordlessRecoveryEnableBtn),
|
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 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(
|
Center(
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
|
@ -212,9 +197,7 @@ class _BackupViewState extends State<BackupView> {
|
||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
});
|
});
|
||||||
await BackupService.makeBackup(force: true);
|
await BackupService.makeBackup(force: true);
|
||||||
setState(() {
|
await _loadBackupStatus();
|
||||||
_isLoading = false;
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
child: Text(context.lang.backupTwonlySaveNow),
|
child: Text(context.lang.backupTwonlySaveNow),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ class _SetupBackupViewState extends State<SetupBackupView> {
|
||||||
onTap: () => FocusScope.of(context).unfocus(),
|
onTap: () => FocusScope.of(context).unfocus(),
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('twonly Backup'),
|
title: Text(context.lang.settingsBackup),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () => showBackupExplanation(context),
|
onPressed: () => showBackupExplanation(context),
|
||||||
|
|
@ -127,12 +127,6 @@ class _SetupBackupViewState extends State<SetupBackupView> {
|
||||||
_repeatedController.text.isNotEmpty,
|
_repeatedController.text.isNotEmpty,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
Text(
|
|
||||||
context.lang.backupNoPasswordRecovery,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: const TextStyle(fontSize: 12),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
Center(
|
Center(
|
||||||
child: MyButton(
|
child: MyButton(
|
||||||
variant: MyButtonVariant.primaryMiddle,
|
variant: MyButtonVariant.primaryMiddle,
|
||||||
|
|
|
||||||
|
|
@ -128,7 +128,7 @@ void showBackupExplanation(BuildContext context) {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
Text(
|
Text(
|
||||||
'twonly Backup',
|
context.lang.settingsBackup,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 22,
|
fontSize: 22,
|
||||||
fontWeight: FontWeight.bold,
|
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:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/constants/routes.keys.dart';
|
import 'package:twonly/src/constants/routes.keys.dart';
|
||||||
import 'package:twonly/src/database/tables/mediafiles.table.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/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/subscription.service.dart';
|
||||||
import 'package:twonly/src/services/user.service.dart';
|
|
||||||
import 'package:twonly/src/utils/misc.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/elements/my_button.element.dart';
|
||||||
import 'package:twonly/src/visual/views/settings/data_and_storage/storage_contents.view.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> {
|
class _ManageStorageViewState extends State<ManageStorageView> {
|
||||||
Map<MediaType, int> _stats = {};
|
Map<MediaType, int> _stats = {};
|
||||||
server.Response_MemoriesUsage? _memoriesUsage;
|
|
||||||
int _cloudOnlyCount = 0;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
|
@ -39,110 +28,13 @@ class _ManageStorageViewState extends State<ManageStorageView> {
|
||||||
|
|
||||||
Future<void> _loadStats() async {
|
Future<void> _loadStats() async {
|
||||||
final stats = await twonlyDB.mediaFilesDao.getStorageStats();
|
final stats = await twonlyDB.mediaFilesDao.getStorageStats();
|
||||||
final memoriesUsage = await apiService.getMemoriesUsage();
|
|
||||||
final cloudOnlyCount = await twonlyDB.mediaFilesDao
|
|
||||||
.getCloudOnlyMemoriesCount();
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_stats = stats;
|
_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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final currentPlan = context.watch<PurchasesProvider>().plan;
|
final currentPlan = context.watch<PurchasesProvider>().plan;
|
||||||
|
|
@ -162,7 +54,7 @@ class _ManageStorageViewState extends State<ManageStorageView> {
|
||||||
body: ListView(
|
body: ListView(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
children: [
|
children: [
|
||||||
if (isFreePlan) ...[
|
if (isFreePlan || !userService.currentUser.isCloudBackupEnabled) ...[
|
||||||
Card(
|
Card(
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
color: Theme.of(
|
color: Theme.of(
|
||||||
|
|
@ -177,223 +69,44 @@ class _ManageStorageViewState extends State<ManageStorageView> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () => context.push(Routes.settingsSubscription),
|
onTap: () {
|
||||||
|
if (isFreePlan) {
|
||||||
|
context.push(Routes.settingsSubscription);
|
||||||
|
} else {
|
||||||
|
context.push(Routes.settingsBackup);
|
||||||
|
}
|
||||||
|
},
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
Icons.cloud_off_outlined,
|
Icons.cloud_queue_rounded,
|
||||||
color: Theme.of(context).colorScheme.primary,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
size: 24,
|
size: 24,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Text(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
context.lang.backupFreeSpaceWithCloud,
|
||||||
children: [
|
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||||
Text(
|
|
||||||
context.lang.settingsStorageNoCloudBackupTitle,
|
|
||||||
style: Theme.of(context).textTheme.titleSmall
|
|
||||||
?.copyWith(
|
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
context.lang.settingsStorageNoCloudBackupCard,
|
|
||||||
style: Theme.of(context).textTheme.bodySmall
|
|
||||||
?.copyWith(
|
|
||||||
color: Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.onSurfaceVariant,
|
|
||||||
),
|
),
|
||||||
|
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 SizedBox(height: 24),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
const SizedBox(height: 24),
|
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),
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
Text(
|
Text(
|
||||||
isFreePlan
|
isFreePlan
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue