Skip to content

Commit

Permalink
[WIP] [Presentation] Added initial backup page ui
Browse files Browse the repository at this point in the history
  • Loading branch information
Wolfteam committed Jan 26, 2023
1 parent 6969308 commit 75b77a4
Show file tree
Hide file tree
Showing 7 changed files with 341 additions and 25 deletions.
170 changes: 170 additions & 0 deletions lib/presentation/backups/backups_page.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:shiori/application/bloc.dart';
import 'package:shiori/domain/extensions/string_extensions.dart';
import 'package:shiori/domain/models/models.dart';
import 'package:shiori/generated/l10n.dart';
import 'package:shiori/injection.dart';
import 'package:shiori/presentation/backups/widgets/backup_list_item.dart';
import 'package:shiori/presentation/shared/app_fab.dart';
import 'package:shiori/presentation/shared/dialogs/confirm_dialog.dart';
import 'package:shiori/presentation/shared/loading.dart';
import 'package:shiori/presentation/shared/mixins/app_fab_mixin.dart';
import 'package:shiori/presentation/shared/nothing_found_column.dart';
import 'package:shiori/presentation/shared/styles.dart';
import 'package:shiori/presentation/shared/utils/toast_utils.dart';

class BackupsPage extends StatefulWidget {
const BackupsPage({super.key});

@override
State<BackupsPage> createState() => _BackupsPageState();
}

class _BackupsPageState extends State<BackupsPage> with SingleTickerProviderStateMixin, AppFabMixin {
@override
bool get isInitiallyVisible => true;

@override
bool get hideOnTop => false;

@override
Widget build(BuildContext context) {
final s = S.of(context);
return BlocProvider<BackupRestoreBloc>(
create: (context) => Injection.backupRestoreBloc..add(const BackupRestoreEvent.init()),
child: Scaffold(
appBar: AppBar(
title: Text('Backups'),
actions: [
BlocBuilder<BackupRestoreBloc, BackupRestoreState>(
builder: (context, state) => state.maybeMap(loaded: (_) => false, orElse: () => true)
? const SizedBox.shrink()
: Tooltip(
message: 'Import',
child: IconButton(
splashRadius: Styles.mediumButtonSplashRadius,
icon: const Icon(Icons.upload),
onPressed: () => FilePicker.platform
.pickFiles(dialogTitle: 'Choose a file', lockParentWindow: true)
.then((result) => _handlePickerResult(context, result)),
),
),
),
],
),
body: SafeArea(
child: BlocConsumer<BackupRestoreBloc, BackupRestoreState>(
listener: (context, state) {
state.maybeMap(
loaded: (state) {
if (state.createResult != null) {
_handleCreateResult(context, state.createResult);
} else if (state.restoreResult != null) {
_handleRestoreResult(context, state.restoreResult);
} else if (state.readResult != null) {
_handleReadResult(context, state.readResult);
}
},
orElse: () {},
);
},
builder: (context, state) => state.maybeMap(
loaded: (state) => state.backups.isEmpty
? const NothingFoundColumn()
: ListView.builder(
itemCount: state.backups.length,
itemBuilder: (context, index) => BackupListItem(backup: state.backups[index]),
),
orElse: () => const Loading(useScaffold: false),
),
),
),
floatingActionButton: Builder(
builder: (context) => AppFab(
icon: const Icon(Icons.add),
hideFabAnimController: hideFabAnimController,
scrollController: scrollController,
mini: false,
onPressed: () => showDialog<bool?>(
context: context,
builder: (context) => ConfirmDialog(
title: s.confirm,
content: 'Would you like to create a backup of all your data and configuration ?',
),
).then((confirmed) {
if (confirmed == true) {
context.read<BackupRestoreBloc>().add(const BackupRestoreEvent.create());
}
}),
),
),
),
);
}

void _showToastMsg(String msg, bool succeed, BuildContext context) {
final toast = ToastUtils.of(context);
if (succeed) {
ToastUtils.showSucceedToast(toast, msg);
} else {
ToastUtils.showErrorToast(toast, msg, durationType: ToastDurationType.long);
}
}

void _handleCreateResult(BuildContext context, BackupOperationResultModel? result) {
if (result == null) {
return;
}

final s = S.of(context);
final msg = result.succeed ? 'Backup = ${result.name} was successfully created' : 'Could not create backup';
_showToastMsg(msg, result.succeed, context);
}

void _handleRestoreResult(BuildContext context, BackupOperationResultModel? result) {
if (result == null) {
return;
}

final s = S.of(context);
final msg = result.succeed ? 'Backup = ${result.name} was successfully restored.\nRestarting...' : 'Could not restore file = ${result.name}';
_showToastMsg(msg, result.succeed, context);
if (result.succeed) {
Future.delayed(const Duration(seconds: 1)).then((value) => context.read<MainBloc>().add(const MainEvent.restart()));
}
}

void _handleReadResult(BuildContext context, BackupOperationResultModel? result) {
if (result == null) {
return;
}

final s = S.of(context);
if (result.succeed) {
showDialog<bool>(
context: context,
builder: (_) => ConfirmDialog(title: s.confirm, content: 'Would you like to restore backup = ${result.name} ?'),
).then((confirmed) {
if (confirmed == true) {
context.read<BackupRestoreBloc>().add(BackupRestoreEvent.restore(result.path));
}
});
} else {
_showToastMsg('File = ${result.name} could not be read.\nMake sure you have selected the appropriate one.', false, context);
}
}

void _handlePickerResult(BuildContext context, FilePickerResult? result) {
if (result == null) {
return;
}
final path = result.files.single.path;
if (path.isNullEmptyOrWhitespace) {
return;
}

context.read<BackupRestoreBloc>().add(BackupRestoreEvent.read(path!));
}
}
55 changes: 55 additions & 0 deletions lib/presentation/backups/widgets/backup_details_dialog.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl/intl.dart';
import 'package:shiori/application/backup_restore/backup_restore_bloc.dart';
import 'package:shiori/domain/models/models.dart';
import 'package:shiori/generated/l10n.dart';

class BackupDetailsDialog extends StatelessWidget {
final BackupFileItemModel backup;

const BackupDetailsDialog({
super.key,
required this.backup,
});

@override
Widget build(BuildContext context) {
final s = S.of(context);
final theme = Theme.of(context);
return AlertDialog(
title: Text(s.details),
scrollable: true,
content: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
Text(backup.filename, style: theme.textTheme.subtitle1),
Text(s.appVersion(backup.appVersion)),
Text('Date: ${DateFormat.yMd().add_Hm().format(backup.createdAt)}'),
Container(
margin: const EdgeInsets.only(top: 10),
child: Text(
'Keep in mind that restoring a backup will replace all your existing data and configuration',
style: theme.textTheme.subtitle2!.copyWith(fontStyle: FontStyle.italic, color: theme.primaryColor),
),
),
],
),
actions: [
OutlinedButton(
onPressed: () => Navigator.pop(context, false),
child: Text(s.cancel, style: TextStyle(color: theme.primaryColor)),
),
ElevatedButton(
onPressed: () => context.read<BackupRestoreBloc>().add(BackupRestoreEvent.delete(backup.filePath)),
child: Text(s.delete),
),
ElevatedButton(
onPressed: () => context.read<BackupRestoreBloc>().add(BackupRestoreEvent.restore(backup.filePath)),
child: Text(s.restore),
),
],
);
}
}
77 changes: 77 additions & 0 deletions lib/presentation/backups/widgets/backup_list_item.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl/intl.dart';
import 'package:shiori/application/bloc.dart';
import 'package:shiori/domain/models/models.dart';
import 'package:shiori/generated/l10n.dart';
import 'package:shiori/presentation/backups/widgets/backup_details_dialog.dart';
import 'package:shiori/presentation/shared/dialogs/confirm_dialog.dart';
import 'package:shiori/presentation/shared/styles.dart';

class BackupListItem extends StatelessWidget {
final BackupFileItemModel backup;

const BackupListItem({
required this.backup,
});

@override
Widget build(BuildContext context) {
final s = S.of(context);
final theme = Theme.of(context);
return Card(
child: ListTile(
title: Tooltip(
message: backup.filename,
child: Text(
backup.filename,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.subtitle2,
),
),
subtitle: Text(
DateFormat.yMd().add_Hm().format(backup.createdAt),
style: theme.textTheme.caption,
),
trailing: Row(
mainAxisAlignment: MainAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
splashRadius: Styles.smallButtonSplashRadius,
icon: const Icon(Icons.settings_backup_restore, color: Colors.green),
visualDensity: VisualDensity.compact,
tooltip: s.restore,
onPressed: () => showDialog<bool?>(
context: context,
builder: (_) => ConfirmDialog(title: s.confirm, content: 'Restore backup ${backup.filename} ?'),
).then((confirmed) {
if (confirmed == true) {
context.read<BackupRestoreBloc>().add(BackupRestoreEvent.restore(backup.filePath));
}
}),
),
IconButton(
splashRadius: Styles.smallButtonSplashRadius,
icon: const Icon(Icons.delete, color: Colors.red),
visualDensity: VisualDensity.compact,
tooltip: s.delete,
onPressed: () => showDialog<bool?>(
context: context,
builder: (_) => ConfirmDialog(title: s.confirm, content: 'Delete backup ${backup.filename} ?'),
).then((confirmed) {
if (confirmed == true) {
context.read<BackupRestoreBloc>().add(BackupRestoreEvent.delete(backup.filePath));
}
}),
),
],
),
onTap: () => showDialog(
context: context,
builder: (_) => BackupDetailsDialog(backup: backup),
),
),
);
}
}
4 changes: 4 additions & 0 deletions lib/presentation/banner_history/banner_history_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import 'package:shiori/presentation/shared/extensions/i18n_extensions.dart';
import 'package:shiori/presentation/shared/item_popupmenu_filter.dart';
import 'package:shiori/presentation/shared/mixins/app_fab_mixin.dart';
import 'package:shiori/presentation/shared/nothing_found_column.dart';
import 'package:shiori/presentation/shared/styles.dart';

const double _tabletFirstCellWidth = 150;
const double _mobileFirstCellWidth = 120;
Expand Down Expand Up @@ -147,6 +148,7 @@ class _AppBar extends StatelessWidget implements PreferredSizeWidget {
actions: [
IconButton(
icon: const Icon(Icons.search),
splashRadius: Styles.mediumButtonSplashRadius,
onPressed: () => showSearch<List<String>>(
context: context,
delegate: _AppBarSearchDelegate(
Expand All @@ -167,6 +169,7 @@ class _AppBar extends StatelessWidget implements PreferredSizeWidget {
onSelected: (val) => context.read<BannerHistoryBloc>().add(BannerHistoryEvent.typeChanged(type: val)),
icon: const Icon(Icons.swap_horiz),
itemText: (val, _) => s.translateBannerHistoryItemType(val),
splashRadius: Styles.mediumButtonSplashRadius,
),
ItemPopupMenuFilter<BannerHistorySortType>(
tooltipText: s.sortType,
Expand All @@ -175,6 +178,7 @@ class _AppBar extends StatelessWidget implements PreferredSizeWidget {
onSelected: (val) => context.read<BannerHistoryBloc>().add(BannerHistoryEvent.sortTypeChanged(type: val)),
icon: const Icon(Icons.sort),
itemText: (val, _) => s.translateBannerHistorySortType(val),
splashRadius: Styles.mediumButtonSplashRadius,
),
],
),
Expand Down
Loading

0 comments on commit 75b77a4

Please sign in to comment.