mirror of
https://git.selfprivacy.org/kherel/selfprivacy.org.app.git
synced 2025-01-07 00:24:18 +00:00
feat(backups): Add snapshot restore modal
This commit is contained in:
parent
03f7e7d819
commit
b01c61a47b
|
@ -41,7 +41,6 @@ class BackupsCubit extends ServerInstallationDependendCubit<BackupsState> {
|
|||
refreshing: false,
|
||||
),
|
||||
);
|
||||
print(state);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -113,9 +112,7 @@ class BackupsCubit extends ServerInstallationDependendCubit<BackupsState> {
|
|||
final BackblazeBucket? bucket = getIt<ApiConfigModel>().backblazeBucket;
|
||||
if (bucket == null) {
|
||||
emit(state.copyWith(isInitialized: false));
|
||||
print('bucket is null');
|
||||
} else {
|
||||
print('bucket is not null');
|
||||
final GenericResult result = await api.initializeRepository(
|
||||
InitializeRepositoryInput(
|
||||
provider: BackupsProviderType.backblaze,
|
||||
|
@ -125,7 +122,6 @@ class BackupsCubit extends ServerInstallationDependendCubit<BackupsState> {
|
|||
password: bucket.applicationKey,
|
||||
),
|
||||
);
|
||||
print('result is $result');
|
||||
if (result.success == false) {
|
||||
getIt<NavigationService>()
|
||||
.showSnackBar(result.message ?? 'Unknown error');
|
||||
|
@ -214,7 +210,6 @@ class BackupsCubit extends ServerInstallationDependendCubit<BackupsState> {
|
|||
await updateBackups();
|
||||
}
|
||||
|
||||
// TODO: inex
|
||||
Future<void> forgetSnapshot(final String snapshotId) async {
|
||||
final result = await api.forgetSnapshot(snapshotId);
|
||||
if (!result.success) {
|
||||
|
@ -226,6 +221,17 @@ class BackupsCubit extends ServerInstallationDependendCubit<BackupsState> {
|
|||
getIt<NavigationService>()
|
||||
.showSnackBar('backup.forget_snapshot_error'.tr());
|
||||
}
|
||||
|
||||
// Optimistic update
|
||||
final backups = state.backups;
|
||||
final index =
|
||||
backups.indexWhere((final snapshot) => snapshot.id == snapshotId);
|
||||
if (index != -1) {
|
||||
backups.removeAt(index);
|
||||
emit(state.copyWith(backups: backups));
|
||||
}
|
||||
|
||||
await updateBackups();
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
@ -3,17 +3,22 @@ import 'package:flutter/material.dart';
|
|||
class OutlinedCard extends StatelessWidget {
|
||||
const OutlinedCard({
|
||||
required this.child,
|
||||
this.borderColor,
|
||||
this.borderWidth,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final Color? borderColor;
|
||||
final double? borderWidth;
|
||||
@override
|
||||
Widget build(final BuildContext context) => Card(
|
||||
elevation: 0.0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
side: BorderSide(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
color: borderColor ?? Theme.of(context).colorScheme.outline,
|
||||
width: borderWidth ?? 1.0,
|
||||
),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
|
|
|
@ -18,6 +18,7 @@ import 'package:selfprivacy/ui/helpers/modals.dart';
|
|||
import 'package:selfprivacy/ui/pages/backups/change_period_modal.dart';
|
||||
import 'package:selfprivacy/ui/pages/backups/copy_encryption_key_modal.dart';
|
||||
import 'package:selfprivacy/ui/pages/backups/create_backups_modal.dart';
|
||||
import 'package:selfprivacy/ui/pages/backups/snapshot_modal.dart';
|
||||
import 'package:selfprivacy/ui/router/router.dart';
|
||||
import 'package:selfprivacy/utils/extensions/duration.dart';
|
||||
|
||||
|
@ -69,14 +70,15 @@ class BackupDetailsPage extends StatelessWidget {
|
|||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
if (!preventActions) BrandButton.rised(
|
||||
onPressed: preventActions
|
||||
? null
|
||||
: () async {
|
||||
await context.read<BackupsCubit>().initializeBackups();
|
||||
},
|
||||
text: 'backup.initialize'.tr(),
|
||||
),
|
||||
if (!preventActions)
|
||||
BrandButton.rised(
|
||||
onPressed: preventActions
|
||||
? null
|
||||
: () async {
|
||||
await context.read<BackupsCubit>().initializeBackups();
|
||||
},
|
||||
text: 'backup.initialize'.tr(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@ -183,7 +185,9 @@ class BackupDetailsPage extends StatelessWidget {
|
|||
'backup.backups_encryption_key_subtitle'.tr(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 8),
|
||||
const Divider(),
|
||||
const SizedBox(height: 8),
|
||||
if (backupJobs.isNotEmpty)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
@ -209,7 +213,6 @@ class BackupDetailsPage extends StatelessWidget {
|
|||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Divider(),
|
||||
ListTile(
|
||||
title: Text(
|
||||
'backup.latest_snapshots'.tr(),
|
||||
|
@ -241,16 +244,39 @@ class BackupDetailsPage extends StatelessWidget {
|
|||
onTap: preventActions
|
||||
? null
|
||||
: () {
|
||||
showPopUpAlert(
|
||||
alertTitle: 'backup.restoring'.tr(),
|
||||
description: 'backup.restore_alert'.tr(
|
||||
args: [backup.time.toString()],
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (final BuildContext context) =>
|
||||
DraggableScrollableSheet(
|
||||
expand: false,
|
||||
maxChildSize: 0.9,
|
||||
minChildSize: 0.5,
|
||||
initialChildSize: 0.7,
|
||||
builder: (
|
||||
final context,
|
||||
final scrollController,
|
||||
) =>
|
||||
SnapshotModal(
|
||||
snapshot: backup,
|
||||
scrollController: scrollController,
|
||||
),
|
||||
),
|
||||
actionButtonTitle: 'modals.yes'.tr(),
|
||||
);
|
||||
},
|
||||
onLongPress: preventActions
|
||||
? null
|
||||
: () {
|
||||
showPopUpAlert(
|
||||
alertTitle: 'backup.forget_snapshot'.tr(),
|
||||
description:
|
||||
'backup.forget_snapshot_alert'.tr(),
|
||||
actionButtonTitle:
|
||||
'backup.forget_snapshot'.tr(),
|
||||
actionButtonOnPressed: () => {
|
||||
context.read<BackupsCubit>().restoreBackup(
|
||||
backup.id, // TODO: inex
|
||||
BackupRestoreStrategy.unknown,
|
||||
context.read<BackupsCubit>().forgetSnapshot(
|
||||
backup.id,
|
||||
)
|
||||
},
|
||||
);
|
||||
|
|
|
@ -9,6 +9,7 @@ import 'package:selfprivacy/logic/models/backup.dart';
|
|||
import 'package:selfprivacy/logic/models/service.dart';
|
||||
import 'package:selfprivacy/ui/helpers/modals.dart';
|
||||
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
|
||||
import 'package:selfprivacy/ui/pages/backups/snapshot_modal.dart';
|
||||
|
||||
@RoutePage()
|
||||
class BackupsListPage extends StatelessWidget {
|
||||
|
@ -47,16 +48,34 @@ class BackupsListPage extends StatelessWidget {
|
|||
onTap: preventActions
|
||||
? null
|
||||
: () {
|
||||
showPopUpAlert(
|
||||
alertTitle: 'backup.restoring'.tr(),
|
||||
description: 'backup.restore_alert'.tr(
|
||||
args: [backup.time.toString()],
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (final BuildContext context) =>
|
||||
DraggableScrollableSheet(
|
||||
expand: false,
|
||||
maxChildSize: 0.9,
|
||||
minChildSize: 0.5,
|
||||
initialChildSize: 0.7,
|
||||
builder: (final context, final scrollController) =>
|
||||
SnapshotModal(
|
||||
snapshot: backup,
|
||||
scrollController: scrollController,
|
||||
),
|
||||
),
|
||||
actionButtonTitle: 'modals.yes'.tr(),
|
||||
);
|
||||
},
|
||||
onLongPress: preventActions
|
||||
? null
|
||||
: () {
|
||||
showPopUpAlert(
|
||||
alertTitle: 'backup.forget_snapshot'.tr(),
|
||||
description: 'backup.forget_snapshot_alert'.tr(),
|
||||
actionButtonTitle: 'backup.forget_snapshot'.tr(),
|
||||
actionButtonOnPressed: () => {
|
||||
context.read<BackupsCubit>().restoreBackup(
|
||||
backup.id, // TODO: inex
|
||||
BackupRestoreStrategy.unknown,
|
||||
context.read<BackupsCubit>().forgetSnapshot(
|
||||
backup.id,
|
||||
)
|
||||
},
|
||||
);
|
||||
|
|
232
lib/ui/pages/backups/snapshot_modal.dart
Normal file
232
lib/ui/pages/backups/snapshot_modal.dart
Normal file
|
@ -0,0 +1,232 @@
|
|||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:selfprivacy/config/get_it_config.dart';
|
||||
import 'package:selfprivacy/logic/cubit/backups/backups_cubit.dart';
|
||||
import 'package:selfprivacy/logic/cubit/server_jobs/server_jobs_cubit.dart';
|
||||
import 'package:selfprivacy/logic/cubit/services/services_cubit.dart';
|
||||
import 'package:selfprivacy/logic/models/backup.dart';
|
||||
import 'package:selfprivacy/logic/models/json/server_job.dart';
|
||||
import 'package:selfprivacy/logic/models/service.dart';
|
||||
import 'package:selfprivacy/ui/components/buttons/brand_button.dart';
|
||||
import 'package:selfprivacy/ui/components/cards/outlined_card.dart';
|
||||
import 'package:selfprivacy/ui/components/info_box/info_box.dart';
|
||||
|
||||
class SnapshotModal extends StatefulWidget {
|
||||
const SnapshotModal({
|
||||
required this.snapshot,
|
||||
required this.scrollController,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Backup snapshot;
|
||||
final ScrollController scrollController;
|
||||
|
||||
@override
|
||||
State<SnapshotModal> createState() => _SnapshotModalState();
|
||||
}
|
||||
|
||||
class _SnapshotModalState extends State<SnapshotModal> {
|
||||
BackupRestoreStrategy selectedStrategy =
|
||||
BackupRestoreStrategy.downloadVerifyOverwrite;
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) {
|
||||
final List<String> busyServices = context
|
||||
.watch<ServerJobsCubit>()
|
||||
.state
|
||||
.backupJobList
|
||||
.where(
|
||||
(final ServerJob job) =>
|
||||
job.status == JobStatusEnum.running ||
|
||||
job.status == JobStatusEnum.created,
|
||||
)
|
||||
.map((final ServerJob job) => job.typeId.split('.')[1])
|
||||
.toList();
|
||||
|
||||
final bool isServiceBusy = busyServices.contains(widget.snapshot.serviceId);
|
||||
|
||||
final Service? service = context
|
||||
.read<ServicesCubit>()
|
||||
.state
|
||||
.getServiceById(widget.snapshot.serviceId);
|
||||
|
||||
return ListView(
|
||||
controller: widget.scrollController,
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'backup.snapshot_modal_heading'.tr(),
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ListTile(
|
||||
leading: service != null
|
||||
? SvgPicture.string(
|
||||
service.svgIcon,
|
||||
height: 24,
|
||||
width: 24,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.onSurface,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
)
|
||||
: const Icon(
|
||||
Icons.question_mark_outlined,
|
||||
),
|
||||
title: Text(
|
||||
'backup.snapshot_service_title'.tr(),
|
||||
),
|
||||
subtitle: Text(
|
||||
service?.displayName ?? widget.snapshot.fallbackServiceName,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: Icon(
|
||||
Icons.access_time_outlined,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
title: Text(
|
||||
'backup.snapshot_creation_time_title'.tr(),
|
||||
),
|
||||
subtitle: Text(
|
||||
'${MaterialLocalizations.of(context).formatShortDate(widget.snapshot.time)} ${TimeOfDay.fromDateTime(widget.snapshot.time).format(context)}',
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: Icon(
|
||||
Icons.numbers_outlined,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
title: Text(
|
||||
'backup.snapshot_id_title'.tr(),
|
||||
),
|
||||
subtitle: Text(
|
||||
widget.snapshot.id,
|
||||
),
|
||||
),
|
||||
if (service != null)
|
||||
Column(
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'backup.snapshot_modal_select_strategy'.tr(),
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_BackupStrategySelectionCard(
|
||||
isSelected: selectedStrategy ==
|
||||
BackupRestoreStrategy.downloadVerifyOverwrite,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
selectedStrategy =
|
||||
BackupRestoreStrategy.downloadVerifyOverwrite;
|
||||
});
|
||||
},
|
||||
title:
|
||||
'backup.snapshot_modal_download_verify_option_title'.tr(),
|
||||
subtitle:
|
||||
'backup.snapshot_modal_download_verify_option_description'
|
||||
.tr(),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_BackupStrategySelectionCard(
|
||||
isSelected: selectedStrategy == BackupRestoreStrategy.inplace,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
selectedStrategy = BackupRestoreStrategy.inplace;
|
||||
});
|
||||
},
|
||||
title: 'backup.snapshot_modal_inplace_option_title'.tr(),
|
||||
subtitle:
|
||||
'backup.snapshot_modal_inplace_option_description'.tr(),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// Restore backup button
|
||||
BrandButton.filled(
|
||||
onPressed: isServiceBusy
|
||||
? null
|
||||
: () {
|
||||
context.read<BackupsCubit>().restoreBackup(
|
||||
widget.snapshot.id,
|
||||
selectedStrategy,
|
||||
);
|
||||
Navigator.of(context).pop();
|
||||
getIt<NavigationService>()
|
||||
.showSnackBar('backup.restore_started'.tr());
|
||||
},
|
||||
text: 'backup.restore'.tr(),
|
||||
),
|
||||
],
|
||||
)
|
||||
else
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: InfoBox(
|
||||
isWarning: true,
|
||||
text: 'backup.snapshot_modal_service_not_found'.tr(),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BackupStrategySelectionCard extends StatelessWidget {
|
||||
const _BackupStrategySelectionCard({
|
||||
required this.isSelected,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final bool isSelected;
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final void Function() onTap;
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) => OutlinedCard(
|
||||
borderColor: isSelected ? Theme.of(context).colorScheme.primary : null,
|
||||
borderWidth: isSelected ? 3 : 1,
|
||||
child: InkResponse(
|
||||
highlightShape: BoxShape.rectangle,
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
if (isSelected)
|
||||
Icon(
|
||||
Icons.radio_button_on_outlined,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
)
|
||||
else
|
||||
Icon(
|
||||
Icons.radio_button_off_outlined,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
Text(
|
||||
subtitle,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
Loading…
Reference in a new issue