1+ import 'dart:io' ;
2+
13import 'package:flutter/material.dart' ;
24import 'package:flutter_riverpod/flutter_riverpod.dart' ;
35import 'package:intl/intl.dart' ;
46import 'package:open_authenticator/i18n/translations.g.dart' ;
57import 'package:open_authenticator/model/backup.dart' ;
6- import 'package:open_authenticator/utils/platform.dart' ;
78import 'package:open_authenticator/utils/result.dart' ;
89import 'package:open_authenticator/widgets/centered_circular_progress_indicator.dart' ;
910import 'package:open_authenticator/widgets/dialog/confirmation_dialog.dart' ;
1011import 'package:open_authenticator/widgets/dialog/text_input_dialog.dart' ;
12+ import 'package:open_authenticator/widgets/list/expand_list_tile.dart' ;
1113import 'package:open_authenticator/widgets/waiting_overlay.dart' ;
14+ import 'package:share_plus/share_plus.dart' ;
1215
1316/// Allows the user to restore a backup.
1417class ManageBackupSettingsEntryWidget extends ConsumerWidget {
@@ -47,35 +50,31 @@ class _RestoreBackupDialog extends ConsumerStatefulWidget {
4750
4851/// The restore backup dialog state.
4952class _RestoreBackupDialogState extends ConsumerState <_RestoreBackupDialog > {
53+ /// The list global key.
54+ late GlobalKey listKey = GlobalKey ();
55+
5056 @override
5157 Widget build (BuildContext context) {
5258 DateFormat formatter = DateFormat (_RestoreBackupDialog .kDateFormat);
5359 AsyncValue <List <Backup >> backups = ref.watch (backupStoreProvider);
5460 Widget content;
5561 switch (backups) {
5662 case AsyncData (: final value):
57- content = ListView (
58- shrinkWrap: true ,
59- children: [
60- for (Backup backup in value)
61- ListTile (
62- title: Text (formatter.format (backup.dateTime)),
63- onLongPress: currentPlatform.isDesktop ? null : () => deleteBackup (backup),
64- contentPadding: EdgeInsets .zero,
65- trailing: currentPlatform.isDesktop
66- ? Row (
67- mainAxisSize: MainAxisSize .min,
68- children: [
69- createRestoreButton (backup),
70- IconButton (
71- onPressed: () => deleteBackup (backup),
72- icon: const Icon (Icons .delete),
73- ),
74- ],
75- )
76- : createRestoreButton (backup),
77- ),
78- ],
63+ content = SizedBox (
64+ width: MediaQuery .of (context).size.width,
65+ child: ListView (
66+ key: listKey,
67+ shrinkWrap: true ,
68+ children: [
69+ for (Backup backup in value)
70+ ExpandListTile (
71+ title: Text (
72+ formatter.format (backup.dateTime),
73+ ),
74+ children: createBackupActions (backup),
75+ ),
76+ ],
77+ ),
7978 );
8079 break ;
8180 case AsyncError (: final error):
@@ -102,32 +101,68 @@ class _RestoreBackupDialogState extends ConsumerState<_RestoreBackupDialog> {
102101 );
103102 }
104103
105- /// Creates the button that allows to restore the given [backup] .
106- Widget createRestoreButton (Backup backup) => IconButton (
107- onPressed: () async {
108- String ? password = await TextInputDialog .prompt (
109- context,
110- title: translations.settings.backups.manageBackups.restoreBackupPasswordDialog.title,
111- message: translations.settings.backups.manageBackups.restoreBackupPasswordDialog.message,
112- password: true ,
113- );
114- if (password == null || ! mounted) {
115- return ;
116- }
117- Result result = await showWaitingOverlay (
118- context,
119- future: backup.restore (password),
120- );
121- if (mounted) {
122- context.showSnackBarForResult (result);
123- Navigator .pop (context);
124- }
125- },
126- icon: const Icon (Icons .upload),
127- );
104+ /// Creates the buttons to interact with a given [backup] .
105+ List <Widget > createBackupActions (Backup backup) => [
106+ ListTile (
107+ dense: true ,
108+ onTap: () => restoreBackup (backup),
109+ title: Text (translations.settings.backups.manageBackups.button.restore),
110+ leading: const Icon (Icons .upload),
111+ ),
112+ ListTile (
113+ dense: true ,
114+ onTap: () => exportBackup (backup),
115+ title: Text (translations.settings.backups.manageBackups.button.export),
116+ leading: const Icon (Icons .share),
117+ ),
118+ ListTile (
119+ dense: true ,
120+ onTap: () => deleteBackup (backup),
121+ title: Text (translations.settings.backups.manageBackups.button.delete),
122+ leading: const Icon (Icons .delete),
123+ ),
124+ ];
125+
126+ /// Asks the user for the given [backup] restoring.
127+ Future <void > restoreBackup (Backup backup) async {
128+ String ? password = await TextInputDialog .prompt (
129+ context,
130+ title: translations.settings.backups.manageBackups.restoreBackupPasswordDialog.title,
131+ message: translations.settings.backups.manageBackups.restoreBackupPasswordDialog.message,
132+ password: true ,
133+ );
134+ if (password == null || ! mounted) {
135+ return ;
136+ }
137+ Result result = await showWaitingOverlay (
138+ context,
139+ future: backup.restore (password),
140+ );
141+ if (mounted) {
142+ context.showSnackBarForResult (result);
143+ Navigator .pop (context);
144+ }
145+ }
146+
147+ /// Asks the user for the given [backup] export.
148+ Future <void > exportBackup (Backup backup) async {
149+ RenderBox ? box = listKey.currentContext? .findRenderObject () as RenderBox ? ;
150+ File file = await backup.getBackupPath ();
151+ await Share .shareXFiles (
152+ [
153+ XFile (
154+ file.path,
155+ mimeType: 'application/json' ,
156+ ),
157+ ],
158+ subject: translations.settings.backups.manageBackups.exportBackupDialog.subject,
159+ text: translations.settings.backups.manageBackups.exportBackupDialog.text,
160+ sharePositionOrigin: box == null ? Rect .zero : (box.localToGlobal (Offset .zero) & box.size),
161+ );
162+ }
128163
129164 /// Asks the user for the given [backup] deletion.
130- void deleteBackup (Backup backup) async {
165+ Future < void > deleteBackup (Backup backup) async {
131166 bool result = await ConfirmationDialog .ask (
132167 context,
133168 title: translations.settings.backups.manageBackups.deleteBackupConfirmationDialog.title,
0 commit comments