feat: Implement backup settings page for backups wizard

This commit is contained in:
NaiJi 2023-10-04 03:55:02 -03:00
parent e8ebc8be72
commit 13fae7c19e
10 changed files with 374 additions and 204 deletions

View File

@ -292,6 +292,9 @@
"hosting": "Storage Provider",
"period": "Automatic backups",
"rotation": "Rotation settings"
},
"settings": {
"initialize_settings_title": "Backup Settings"
}
},
"storage": {

View File

@ -27,7 +27,7 @@ class BackupsWizardCubit extends Cubit<BackupsWizardState> {
backupsCredential: backupsCredential,
currentStep: state.currentStep == BackupsWizardStep.hostingRecovery
? BackupsWizardStep.finished
: BackupsWizardStep.period,
: BackupsWizardStep.settingsInitialization,
),
);
}
@ -36,7 +36,6 @@ class BackupsWizardCubit extends Cubit<BackupsWizardState> {
emit(
state.copyWith(
autobackupPeriod: autobackupPeriod,
currentStep: BackupsWizardStep.quotas,
),
);
}
@ -45,7 +44,14 @@ class BackupsWizardCubit extends Cubit<BackupsWizardState> {
emit(
state.copyWith(
autobackupQuotas: autobackupQuotas,
currentStep: BackupsWizardStep.confirmation,
),
);
}
void confirmSettings() {
emit(
state.copyWith(
currentStep: BackupsWizardStep.confirmInitialization,
),
);
}

View File

@ -41,8 +41,8 @@ class BackupsWizardState {
enum BackupsWizardStep {
hostingRecovery,
hostingInitialization,
period,
quotas,
confirmation,
settingsInitialization,
confirmInitialization,
confirmRecovery,
finished,
}

View File

@ -140,6 +140,12 @@ class BackupDetailsPage extends StatelessWidget {
builder: (final context, final scrollController) =>
ChangeAutobackupsPeriodModal(
scrollController: scrollController,
initialAutobackupPeriod:
context.read<BackupsCubit>().state.autobackupPeriod,
onSetPeriodCallback: (final Duration? selectedPeriod) =>
context
.read<BackupsCubit>()
.setAutobackupPeriod(selectedPeriod),
),
),
);
@ -183,6 +189,14 @@ class BackupDetailsPage extends StatelessWidget {
initialChildSize: 0.6,
builder: (final context, final scrollController) =>
ChangeRotationQuotasModal(
onSetAutobackupQuotasCallback: (
final AutobackupQuotas selectedAutobackupQuotas,
) =>
context
.read<BackupsCubit>()
.setAutobackupQuotas(selectedAutobackupQuotas),
initialAutobackupQuotas:
context.read<BackupsCubit>().state.autobackupQuotas,
scrollController: scrollController,
),
),

View File

@ -1,17 +1,18 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/logic/cubit/backups/backups_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_jobs/server_jobs_cubit.dart';
import 'package:selfprivacy/utils/extensions/duration.dart';
class ChangeAutobackupsPeriodModal extends StatefulWidget {
const ChangeAutobackupsPeriodModal({
required this.scrollController,
required this.initialAutobackupPeriod,
required this.onSetPeriodCallback,
this.scrollController,
super.key,
});
final ScrollController scrollController;
final ScrollController? scrollController;
final Duration? initialAutobackupPeriod;
final Function(Duration? selectedPeriod) onSetPeriodCallback;
@override
State<ChangeAutobackupsPeriodModal> createState() =>
@ -34,72 +35,66 @@ class _ChangeAutobackupsPeriodModalState
@override
void initState() {
super.initState();
selectedPeriod = context.read<BackupsCubit>().state.autobackupPeriod;
selectedPeriod = widget.initialAutobackupPeriod;
}
@override
Widget build(final BuildContext context) {
final Duration? initialAutobackupPeriod =
context.watch<BackupsCubit>().state.autobackupPeriod;
return ListView(
controller: widget.scrollController,
padding: const EdgeInsets.all(16),
children: [
const SizedBox(height: 16),
Text(
'backup.autobackup_period_title'.tr(),
style: Theme.of(context).textTheme.headlineSmall,
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
// Select all services tile
RadioListTile<Duration?>(
onChanged: (final Duration? value) {
setState(() {
selectedPeriod = value;
});
},
title: Text(
'backup.autobackup_period_disable'.tr(),
Widget build(final BuildContext context) => ListView(
controller: widget.scrollController,
padding: const EdgeInsets.all(16),
children: [
const SizedBox(height: 16),
Text(
'backup.autobackup_period_title'.tr(),
style: Theme.of(context).textTheme.headlineSmall,
textAlign: TextAlign.center,
),
value: null,
groupValue: selectedPeriod,
),
const Divider(
height: 1.0,
),
...autobackupPeriods.map(
(final Duration period) => RadioListTile<Duration?>(
const SizedBox(height: 16),
// Select all services tile
RadioListTile<Duration?>(
onChanged: (final Duration? value) {
setState(() {
selectedPeriod = value;
});
},
title: Text(
'backup.autobackup_period_every'.tr(
namedArgs: {'period': period.toPrettyString(context.locale)},
),
'backup.autobackup_period_disable'.tr(),
),
value: period,
value: null,
groupValue: selectedPeriod,
),
),
const SizedBox(height: 16),
// Create backup button
FilledButton(
onPressed: selectedPeriod == initialAutobackupPeriod
? null
: () {
context
.read<BackupsCubit>()
.setAutobackupPeriod(selectedPeriod);
Navigator.of(context).pop();
},
child: Text(
'backup.autobackup_set_period'.tr(),
const Divider(
height: 1.0,
),
),
],
);
}
...autobackupPeriods.map(
(final Duration period) => RadioListTile<Duration?>(
onChanged: (final Duration? value) {
setState(() {
selectedPeriod = value;
});
},
title: Text(
'backup.autobackup_period_every'.tr(
namedArgs: {'period': period.toPrettyString(context.locale)},
),
),
value: period,
groupValue: selectedPeriod,
),
),
const SizedBox(height: 16),
// Create backup button
FilledButton(
onPressed: selectedPeriod == widget.initialAutobackupPeriod
? null
: () {
widget.onSetPeriodCallback(selectedPeriod);
Navigator.of(context).pop();
},
child: Text(
'backup.autobackup_set_period'.tr(),
),
),
],
);
}

View File

@ -1,17 +1,19 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.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/models/backup.dart';
class ChangeRotationQuotasModal extends StatefulWidget {
const ChangeRotationQuotasModal({
required this.scrollController,
required this.initialAutobackupQuotas,
required this.onSetAutobackupQuotasCallback,
this.scrollController,
super.key,
});
final ScrollController scrollController;
final ScrollController? scrollController;
final AutobackupQuotas? initialAutobackupQuotas;
final Function(AutobackupQuotas selectedAutobackupQuotas)
onSetAutobackupQuotasCallback;
@override
State<ChangeRotationQuotasModal> createState() =>
@ -39,8 +41,7 @@ class _ChangeRotationQuotasModalState extends State<ChangeRotationQuotasModal> {
@override
void initState() {
super.initState();
selectedQuotas =
context.read<BackupsCubit>().state.autobackupQuotas ?? selectedQuotas;
selectedQuotas = widget.initialAutobackupQuotas ?? selectedQuotas;
}
String generateSubtitle(final int value, final QuotaUnits unit) {
@ -81,126 +82,123 @@ class _ChangeRotationQuotasModalState extends State<ChangeRotationQuotasModal> {
}
@override
Widget build(final BuildContext context) {
final AutobackupQuotas? initialAutobackupQuotas =
context.watch<BackupsCubit>().state.autobackupQuotas;
return ListView(
controller: widget.scrollController,
padding: const EdgeInsets.all(16),
children: [
const SizedBox(height: 16),
Text(
'backup.rotation_quotas_title'.tr(),
style: Theme.of(context).textTheme.headlineSmall,
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
'backup.quotas_only_applied_to_autobackups'.tr(),
style: Theme.of(context).textTheme.bodyMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
// Accordions for each quota type. When tapped allows to enter a new int value
// for the quota.
QuotaSelectionTile(
title: 'backup.quota_titles.last'.tr(),
subtitle: generateSubtitle(selectedQuotas.last, QuotaUnits.last),
value: selectedQuotas.last,
min: 1,
max: 30,
callback: (final double value) {
setState(() {
if (value == 31) {
selectedQuotas = selectedQuotas.copyWith(last: -1);
return;
}
selectedQuotas = selectedQuotas.copyWith(last: value.toInt());
});
},
),
QuotaSelectionTile(
title: 'backup.quota_titles.daily'.tr(),
subtitle: generateSubtitle(selectedQuotas.daily, QuotaUnits.daily),
value: selectedQuotas.daily,
min: 0,
max: 30,
callback: (final double value) {
setState(() {
if (value == 31) {
selectedQuotas = selectedQuotas.copyWith(daily: -1);
return;
}
selectedQuotas = selectedQuotas.copyWith(daily: value.toInt());
});
},
),
QuotaSelectionTile(
title: 'backup.quota_titles.weekly'.tr(),
subtitle: generateSubtitle(selectedQuotas.weekly, QuotaUnits.weekly),
value: selectedQuotas.weekly,
min: 0,
max: 15,
callback: (final double value) {
setState(() {
if (value == 16) {
selectedQuotas = selectedQuotas.copyWith(weekly: -1);
return;
}
selectedQuotas = selectedQuotas.copyWith(weekly: value.toInt());
});
},
),
QuotaSelectionTile(
title: 'backup.quota_titles.monthly'.tr(),
subtitle:
generateSubtitle(selectedQuotas.monthly, QuotaUnits.monthly),
value: selectedQuotas.monthly,
min: 0,
max: 24,
callback: (final double value) {
setState(() {
if (value == 25) {
selectedQuotas = selectedQuotas.copyWith(monthly: -1);
return;
}
selectedQuotas = selectedQuotas.copyWith(monthly: value.toInt());
});
},
),
QuotaSelectionTile(
title: 'backup.quota_titles.yearly'.tr(),
subtitle: generateSubtitle(selectedQuotas.yearly, QuotaUnits.yearly),
value: selectedQuotas.yearly,
min: 0,
max: 5,
callback: (final double value) {
setState(() {
if (value == 6) {
selectedQuotas = selectedQuotas.copyWith(yearly: -1);
return;
}
selectedQuotas = selectedQuotas.copyWith(yearly: value.toInt());
});
},
),
const SizedBox(height: 16),
FilledButton(
onPressed: selectedQuotas == initialAutobackupQuotas
? null
: () {
context
.read<BackupsCubit>()
.setAutobackupQuotas(selectedQuotas);
Navigator.of(context).pop();
},
child: Text(
'backup.set_rotation_quotas'.tr(),
Widget build(final BuildContext context) => ListView(
controller: widget.scrollController,
padding: const EdgeInsets.all(16),
children: [
const SizedBox(height: 16),
Text(
'backup.rotation_quotas_title'.tr(),
style: Theme.of(context).textTheme.headlineSmall,
textAlign: TextAlign.center,
),
),
],
);
}
const SizedBox(height: 8),
Text(
'backup.quotas_only_applied_to_autobackups'.tr(),
style: Theme.of(context).textTheme.bodyMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
// Accordions for each quota type. When tapped allows to enter a new int value
// for the quota.
QuotaSelectionTile(
title: 'backup.quota_titles.last'.tr(),
subtitle: generateSubtitle(selectedQuotas.last, QuotaUnits.last),
value: selectedQuotas.last,
min: 1,
max: 30,
callback: (final double value) {
setState(() {
if (value == 31) {
selectedQuotas = selectedQuotas.copyWith(last: -1);
return;
}
selectedQuotas = selectedQuotas.copyWith(last: value.toInt());
});
},
),
QuotaSelectionTile(
title: 'backup.quota_titles.daily'.tr(),
subtitle: generateSubtitle(selectedQuotas.daily, QuotaUnits.daily),
value: selectedQuotas.daily,
min: 0,
max: 30,
callback: (final double value) {
setState(() {
if (value == 31) {
selectedQuotas = selectedQuotas.copyWith(daily: -1);
return;
}
selectedQuotas = selectedQuotas.copyWith(daily: value.toInt());
});
},
),
QuotaSelectionTile(
title: 'backup.quota_titles.weekly'.tr(),
subtitle:
generateSubtitle(selectedQuotas.weekly, QuotaUnits.weekly),
value: selectedQuotas.weekly,
min: 0,
max: 15,
callback: (final double value) {
setState(() {
if (value == 16) {
selectedQuotas = selectedQuotas.copyWith(weekly: -1);
return;
}
selectedQuotas = selectedQuotas.copyWith(weekly: value.toInt());
});
},
),
QuotaSelectionTile(
title: 'backup.quota_titles.monthly'.tr(),
subtitle:
generateSubtitle(selectedQuotas.monthly, QuotaUnits.monthly),
value: selectedQuotas.monthly,
min: 0,
max: 24,
callback: (final double value) {
setState(() {
if (value == 25) {
selectedQuotas = selectedQuotas.copyWith(monthly: -1);
return;
}
selectedQuotas =
selectedQuotas.copyWith(monthly: value.toInt());
});
},
),
QuotaSelectionTile(
title: 'backup.quota_titles.yearly'.tr(),
subtitle:
generateSubtitle(selectedQuotas.yearly, QuotaUnits.yearly),
value: selectedQuotas.yearly,
min: 0,
max: 5,
callback: (final double value) {
setState(() {
if (value == 6) {
selectedQuotas = selectedQuotas.copyWith(yearly: -1);
return;
}
selectedQuotas = selectedQuotas.copyWith(yearly: value.toInt());
});
},
),
const SizedBox(height: 16),
FilledButton(
onPressed: selectedQuotas == widget.initialAutobackupQuotas
? null
: () {
widget.onSetAutobackupQuotasCallback(selectedQuotas);
Navigator.of(context).pop();
},
child: Text(
'backup.set_rotation_quotas'.tr(),
),
),
],
);
}
class QuotaSelectionTile extends StatelessWidget {

View File

@ -0,0 +1,36 @@
import 'package:cubit_form/cubit_form.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart';
import 'package:selfprivacy/ui/components/buttons/brand_button.dart';
import 'package:selfprivacy/ui/layouts/responsive_layout_with_infobox.dart';
class BackupProviderPicker extends StatelessWidget {
const BackupProviderPicker({
super.key,
});
@override
Widget build(final BuildContext context) => ResponsiveLayoutWithInfobox(
topChild: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${'initializing.connect_to_server_provider'.tr()}Backblaze',
style: Theme.of(context).textTheme.headlineSmall,
),
],
),
primaryColumn: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 32),
BrandButton.rised(
onPressed: () => context.read<BackblazeFormCubit>().trySubmit(),
text: 'basis.connect'.tr(),
),
const SizedBox(height: 10),
],
),
);
}

View File

@ -2,7 +2,7 @@ import 'package:auto_route/auto_route.dart';
import 'package:cubit_form/cubit_form.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/cubit/backups/backups_cubit.dart';
import 'package:selfprivacy/logic/cubit/backups_wizard/backups_wizard_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/ui/components/buttons/brand_button.dart';
import 'package:selfprivacy/ui/components/buttons/outlined_button.dart';
@ -10,6 +10,7 @@ import 'package:selfprivacy/ui/components/drawers/progress_drawer.dart';
import 'package:selfprivacy/ui/components/progress_bar/progress_bar.dart';
import 'package:selfprivacy/ui/components/drawers/support_drawer.dart';
import 'package:selfprivacy/ui/pages/backups/setup/backup_provider_picker.dart';
import 'package:selfprivacy/ui/pages/backups/setup/backup_settings_page.dart';
import 'package:selfprivacy/ui/router/router.dart';
import 'package:selfprivacy/utils/breakpoints.dart';
@ -20,16 +21,14 @@ class BackupsInitializingPage extends StatelessWidget {
@override
Widget build(final BuildContext context) {
final Widget actualInitializingPage;
final cubit = context.watch<BackupsCubit>();
final currentStep = ((cubit.state) as BackupsNotFinishedState).step;
final cubit = context.watch<BackupsWizardCubit>();
final currentStep = cubit.state.currentStep;
switch (currentStep) {
case BackupsInitializingStep.period:
actualInitializingPage = const BackupProviderPicker();
case BackupsWizardStep.settingsInitialization:
actualInitializingPage = const BackupSettingsPage();
break;
case BackupsInitializingStep.rotation:
actualInitializingPage = const BackupProviderPicker();
break;
case BackupsInitializingStep.hosting:
case BackupsWizardStep.hostingRecovery:
case BackupsWizardStep.hostingInitialization:
default:
actualInitializingPage = const BackupProviderPicker();
break;
@ -41,9 +40,9 @@ class BackupsInitializingPage extends StatelessWidget {
'backup.steps.rotation',
];
return BlocListener<BackupsCubit, BackupsState>(
return BlocListener<BackupsWizardCubit, BackupsWizardState>(
listener: (final context, final state) {
if (cubit.state is! BackupsNotFinishedState) {
if (cubit.state.currentStep == BackupsWizardStep.finished) {
context.router.pop();
}
},
@ -54,7 +53,7 @@ class BackupsInitializingPage extends StatelessWidget {
? null
: AppBar(
actions: [
if (cubit.state is! BackupsNotFinishedState)
if (cubit.state.currentStep == BackupsWizardStep.finished)
IconButton(
icon: const Icon(Icons.check),
onPressed: () {

View File

@ -0,0 +1,119 @@
import 'package:cubit_form/cubit_form.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/cubit/backups_wizard/backups_wizard_cubit.dart';
import 'package:selfprivacy/logic/models/backup.dart';
import 'package:selfprivacy/ui/layouts/responsive_layout_with_infobox.dart';
import 'package:selfprivacy/ui/pages/backups/change_period_modal.dart';
import 'package:selfprivacy/ui/pages/backups/change_rotation_quotas_modal.dart';
import 'package:selfprivacy/utils/extensions/duration.dart';
class BackupSettingsPage extends StatelessWidget {
const BackupSettingsPage({
super.key,
});
@override
Widget build(final BuildContext context) {
final cubit = context.watch<BackupsWizardCubit>();
final autobackupPeriod = cubit.state.autobackupPeriod;
return ResponsiveLayoutWithInfobox(
topChild: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'backup.settings.initialize_settings_title'.tr(),
style: Theme.of(context).textTheme.headlineSmall,
),
],
),
primaryColumn: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 32),
ListTile(
onTap: () {
showModalBottomSheet(
useRootNavigator: true,
context: context,
isScrollControlled: true,
builder: (final BuildContext context) =>
DraggableScrollableSheet(
expand: false,
maxChildSize: 0.9,
minChildSize: 0.4,
initialChildSize: 0.6,
builder: (final context, final scrollController) =>
ChangeAutobackupsPeriodModal(
initialAutobackupPeriod: null,
onSetPeriodCallback: (final Duration? selectedPeriod) =>
context
.read<BackupsWizardCubit>()
.setAutobackupPeriod(selectedPeriod),
scrollController: scrollController,
),
),
);
},
leading: const Icon(
Icons.manage_history_outlined,
),
title: Text(
'backup.autobackup_period_title'.tr(),
),
subtitle: Text(
autobackupPeriod != null
? 'backup.autobackup_period_subtitle'.tr(
namedArgs: {
'period':
autobackupPeriod.toPrettyString(context.locale)
},
)
: 'backup.autobackup_period_never'.tr(),
),
),
ListTile(
onTap: () {
showModalBottomSheet(
useRootNavigator: true,
context: context,
isScrollControlled: true,
builder: (final BuildContext context) =>
DraggableScrollableSheet(
expand: false,
maxChildSize: 0.9,
minChildSize: 0.4,
initialChildSize: 0.6,
builder: (final context, final scrollController) =>
ChangeRotationQuotasModal(
initialAutobackupQuotas: null,
onSetAutobackupQuotasCallback:
(final AutobackupQuotas selectedAutobackupQuotas) =>
context
.read<BackupsWizardCubit>()
.setAutobackupQuotas(selectedAutobackupQuotas),
scrollController: scrollController,
),
),
);
},
leading: const Icon(
Icons.auto_delete_outlined,
),
title: Text(
'backup.rotation_quotas_title'.tr(),
),
),
const SizedBox(height: 16),
FilledButton(
onPressed: () =>
context.read<BackupsWizardCubit>().confirmSettings(),
child: Text(
'backup.set_rotation_quotas'.tr(),
),
),
],
),
);
}
}

View File

@ -5,7 +5,7 @@ import 'package:selfprivacy/logic/models/disk_status.dart';
import 'package:selfprivacy/logic/models/service.dart';
import 'package:selfprivacy/ui/pages/backups/backup_details.dart';
import 'package:selfprivacy/ui/pages/backups/backups_list.dart';
import 'package:selfprivacy/ui/pages/backups/setup/backups_initializing.dart';
import 'package:selfprivacy/ui/pages/backups/setup/backup_initializing.dart';
import 'package:selfprivacy/ui/pages/devices/devices.dart';
import 'package:selfprivacy/ui/pages/dns_details/dns_details.dart';
import 'package:selfprivacy/ui/pages/more/about_application.dart';