feat(backups): Add snapshot restore modal

pull/274/head
Inex Code 2023-08-14 07:10:15 +03:00
parent 03f7e7d819
commit b01c61a47b
5 changed files with 320 additions and 32 deletions

View File

@ -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

View File

@ -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,

View File

@ -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,
)
},
);

View File

@ -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,
)
},
);

View 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,
),
],
),
),
],
),
),
),
);
}