feat: backup and restaure

This commit is contained in:
kodjomoustapha 2023-11-21 13:28:44 +01:00
parent 96aa836e85
commit e530cc655d
12 changed files with 292 additions and 105 deletions

View file

@ -236,5 +236,27 @@
"log_out_from":"Log out from {tracker}?",
"log_out":"Log out",
"update_pending":"Update pending",
"update_all":"Update all"
"update_all":"Update all",
"backup_and_restore":"Backup and restore",
"create_backup":"Create backup",
"create_backup_dialog_title":"What do you want to backup?",
"create_backup_subtitle":"Can be used to restore current library",
"restore_backup":"Restore backup",
"restore_backup_subtitle":"Restore library from backup file",
"automatic_backups":"Automatic backups",
"backup_frequency":"Backup frequency",
"backup_location":"Backup location",
"backup_options":"Backup options",
"backup_options_dialog_title":"What do you want to backup?",
"backup_options_subtile":"What information to include in the backup file",
"backup_and_restore_warning_info":"You should keep copies of backups in other places as well",
"library_entries":"Library entries",
"categories":"Categories",
"chapters_and_episode":"Chapters and episode",
"every_6_hours":"Every 6 hours",
"every_12_hours":"Every 12 hours",
"daily":"Daily",
"every_2_days":"Every 2 days",
"weekly":"Weekly",
"restore_backup_warning_title":"Restoring a backup will overwrite all existing data.\n\nContinue restoring?"
}

View file

@ -236,5 +236,27 @@
"log_out_from":"Se déconnecter de {tracker} ?",
"log_out":"Se déconnecter",
"update_pending":"Mises à jour en attente",
"update_all":"Tout mettre à jour"
"update_all":"Tout mettre à jour",
"backup_and_restore":"Sauvegarder et restaurer",
"create_backup":"Créer une sauvegarde",
"create_backup_dialog_title":"Que voulez-vous sauvegarder ?",
"create_backup_subtitle":"Peut être utilisé pour restaurer la bibliothèque actuelle",
"restore_backup":"Restaurer une sauvegarde",
"restore_backup_subtitle":"Restaurer la bibliothèque à partir d'un fichier de sauvegarde",
"automatic_backups":"Sauvegarders automatiques",
"backup_frequency":"Fréquence de sauvegarde",
"backup_location":"Dossier de sauvegarde",
"backup_options":"Options de sauvegarde",
"backup_options_dialog_title":"Que voulez-vous sauvegarder ?",
"backup_options_subtile":"Quelle information inclure dans le fichier de sauvegarde",
"backup_and_restore_warning_info":"Vous devez égalemement conserver des copies des sauvegardes à d'atures endroits",
"library_entries":"Entrées de la bibliothèque",
"categories":"Catégories",
"chapters_and_episode":"Chapitres et épisodes",
"every_6_hours":"Toutes les 6 heures",
"every_12_hours":"Toutes les 12 heures",
"daily":"Tous les jours",
"every_2_days":"Tous les 2 jours",
"weekly":"Chaque semaine",
"restore_backup_warning_title":"La restauration d'une sauvegarde écrasera toutes les données existantes.\n\nContinuer la restauration ?"
}

View file

@ -306,6 +306,10 @@ class Settings {
: null;
themeIsDark = json['themeIsDark'];
userAgent = json['userAgent'];
backupFrequency = json['backupFrequency'];
backupFrequencyOptions = json['backupFrequencyOptions']?.cast<int>();
autoBackupLocation = json['autoBackupLocation'];
startDatebackup = json['startDatebackup'];
}
Map<String, dynamic> toJson() {
@ -399,6 +403,10 @@ class Settings {
}
data['themeIsDark'] = themeIsDark;
data['userAgent'] = userAgent;
data['backupFrequency'] = backupFrequency;
data['backupFrequencyOptions'] = backupFrequencyOptions;
data['autoBackupLocation'] = autoBackupLocation;
data['startDatebackup'] = startDatebackup;
return data;
}
}

View file

@ -9,6 +9,7 @@ import 'package:mangayomi/models/source.dart';
import 'package:mangayomi/modules/browse/extension/providers/fetch_manga_sources.dart';
import 'package:mangayomi/modules/main_view/providers/migration.dart';
import 'package:mangayomi/modules/more/about/providers/check_for_update.dart';
import 'package:mangayomi/modules/more/backup_and_restore/providers/auto_backup.dart';
import 'package:mangayomi/modules/more/settings/browse/providers/browse_state_provider.dart';
import 'package:mangayomi/modules/widgets/error_text.dart';
import 'package:mangayomi/modules/widgets/progress_center.dart';
@ -29,6 +30,7 @@ class MainScreen extends ConsumerWidget {
final l10n = l10nLocalizations(context)!;
final route = GoRouter.of(context);
ref.watch(checkForUpdateProvider(context: context));
ref.read(checkAndBackupProvider);
return ref.watch(migrationProvider).when(data: (_) {
return Consumer(builder: (context, ref, chuld) {
final location = ref.watch(

View file

@ -23,13 +23,13 @@ class BackupAndRestore extends ConsumerWidget {
final l10n = l10nLocalizations(context)!;
return Scaffold(
appBar: AppBar(
title: const Text("Backup and restore"),
title: Text(l10n.backup_and_restore),
),
body: Column(
children: [
ListTile(
onTap: () {
final list = _getList();
final list = _getList(context);
List<int> indexList = [];
indexList.addAll(backupFrequencyOptions);
showDialog(
@ -38,8 +38,8 @@ class BackupAndRestore extends ConsumerWidget {
return StatefulBuilder(
builder: (context, setState) {
return AlertDialog(
title: const Text(
"What do you want to backup?",
title: Text(
l10n.create_backup_dialog_title,
),
content: SizedBox(
width: mediaWidth(context, 0.8),
@ -101,27 +101,80 @@ class BackupAndRestore extends ConsumerWidget {
);
});
},
title: const Text("Create backup"),
title: Text(l10n.create_backup),
subtitle: Text(
"Can be used to restore current library",
l10n.create_backup_subtitle,
style: TextStyle(fontSize: 11, color: secondaryColor(context)),
),
),
ListTile(
onTap: () async {
FilePickerResult? result = await FilePicker.platform.pickFiles(
allowMultiple: false,
type: FileType.custom,
allowedExtensions: ['backup']);
onTap: () {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text(l10n.restore_backup),
content: SizedBox(
width: mediaWidth(context, 0.8),
child: ListView(
shrinkWrap: true,
children: [
Row(
children: [
Icon(Icons.info_outline_rounded,
color: secondaryColor(context)),
],
),
Padding(
padding:
const EdgeInsets.symmetric(vertical: 5),
child: Text(l10n.restore_backup_warning_title),
),
],
)),
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () async {
Navigator.pop(context);
},
child: Text(
l10n.cancel,
style:
TextStyle(color: primaryColor(context)),
)),
TextButton(
onPressed: () async {
FilePickerResult? result =
await FilePicker.platform.pickFiles(
allowMultiple: false,
type: FileType.custom,
allowedExtensions: ['backup']);
if (result != null && context.mounted) {
ref.watch(doRestoreProvider(
path: result.files.first.path!, context: context));
}
if (result != null && context.mounted) {
ref.watch(doRestoreProvider(
path: result.files.first.path!,
context: context));
}
if (!context.mounted) return;
Navigator.pop(context);
},
child: Text(
l10n.ok,
style:
TextStyle(color: primaryColor(context)),
)),
],
)
],
);
});
},
title: const Text("Restore backup"),
title: Text(l10n.restore_backup),
subtitle: Text(
"Restore library from backup file",
l10n.restore_backup_subtitle,
style: TextStyle(fontSize: 11, color: secondaryColor(context)),
),
),
@ -129,7 +182,7 @@ class BackupAndRestore extends ConsumerWidget {
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 20),
child: Row(
children: [
Text('Automatic backups',
Text(l10n.automatic_backups,
style:
TextStyle(fontSize: 13, color: primaryColor(context))),
],
@ -140,9 +193,9 @@ class BackupAndRestore extends ConsumerWidget {
showDialog(
context: context,
builder: (context) {
final list = _getBackupFrequencyList();
final list = _getBackupFrequencyList(context);
return AlertDialog(
title: const Text("Backup frequency"),
title: Text(l10n.backup_frequency),
content: SizedBox(
width: mediaWidth(context, 0.8),
child: ListView.builder(
@ -186,9 +239,9 @@ class BackupAndRestore extends ConsumerWidget {
);
});
},
title: const Text("Backup frequency"),
title: Text(l10n.backup_frequency),
subtitle: Text(
_getBackupFrequencyList()[backupFrequency],
_getBackupFrequencyList(context)[backupFrequency],
style: TextStyle(fontSize: 11, color: secondaryColor(context)),
),
),
@ -200,7 +253,7 @@ class BackupAndRestore extends ConsumerWidget {
ref.read(autoBackupLocationStateProvider.notifier).set(result);
}
},
title: const Text('Backup location'),
title: Text(l10n.backup_location),
subtitle: Text(
autoBackupLocation.$2.isEmpty
? autoBackupLocation.$1
@ -210,7 +263,7 @@ class BackupAndRestore extends ConsumerWidget {
),
ListTile(
onTap: () {
final list = _getList();
final list = _getList(context);
List<int> indexList = [];
indexList.addAll(backupFrequencyOptions);
showDialog(
@ -219,8 +272,8 @@ class BackupAndRestore extends ConsumerWidget {
return StatefulBuilder(
builder: (context, setState) {
return AlertDialog(
title: const Text(
"What do you want to backup?",
title: Text(
l10n.backup_options_subtile,
),
content: SizedBox(
width: mediaWidth(context, 0.8),
@ -279,9 +332,9 @@ class BackupAndRestore extends ConsumerWidget {
);
});
},
title: const Text("Backup options"),
title: Text(l10n.backup_options),
subtitle: Text(
"What information to include in the backup file",
l10n.backup_options_subtile,
style: TextStyle(fontSize: 11, color: secondaryColor(context)),
),
),
@ -297,8 +350,7 @@ class BackupAndRestore extends ConsumerWidget {
],
),
),
subtitle: Text(
"You should keep copies of backups in other places as well",
subtitle: Text(l10n.backup_and_restore_warning_info,
style: TextStyle(fontSize: 11, color: secondaryColor(context))),
)
],
@ -307,25 +359,27 @@ class BackupAndRestore extends ConsumerWidget {
}
}
List<String> _getList() {
List<String> _getList(BuildContext context) {
final l10n = l10nLocalizations(context)!;
return [
"Library entries",
"Categories",
"Chapters and episode",
"Tracking",
"History",
"Settings",
"Extensions"
l10n.library_entries,
l10n.categories,
l10n.chapters_and_episode,
l10n.tracking,
l10n.history,
l10n.settings,
l10n.extensions
];
}
List<String> _getBackupFrequencyList() {
List<String> _getBackupFrequencyList(BuildContext context) {
final l10n = l10nLocalizations(context)!;
return [
"Off",
"Every 6 hours",
"Every 12 hours",
"Daily",
"Every 2 days",
"Weekly"
l10n.off,
l10n.every_6_hours,
l10n.every_12_hours,
l10n.daily,
l10n.every_2_days,
l10n.weekly
];
}

View file

@ -1,6 +1,7 @@
import 'dart:io';
import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/settings.dart';
import 'package:mangayomi/modules/more/backup_and_restore/providers/backup.dart';
import 'package:mangayomi/providers/storage_provider.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'auto_backup.g.dart';
@ -13,10 +14,8 @@ class BackupFrequencyState extends _$BackupFrequencyState {
}
void set(int value) {
final settings = isar.settings.getSync(227);
state = value;
isar.writeTxnSync(
() => isar.settings.putSync(settings!..backupFrequency = value));
_setBackupFrequency(value);
}
}
@ -24,7 +23,7 @@ class BackupFrequencyState extends _$BackupFrequencyState {
class BackupFrequencyOptionsState extends _$BackupFrequencyOptionsState {
@override
List<int> build() {
return isar.settings.getSync(227)!.backupFrequencyOptions ?? [];
return isar.settings.getSync(227)!.backupFrequencyOptions ?? [0, 1, 2, 3];
}
void set(List<int> values) {
@ -54,13 +53,61 @@ class AutoBackupLocationState extends _$AutoBackupLocationState {
Future refresh() async {
_storageProvider = await StorageProvider().getDefaultDirectory();
final settings = isar.settings.getSync(227);
state =
("${_storageProvider!.path}backup", settings!.autoBackupLocation ?? "");
state = (
"${_storageProvider!.path}backup/",
settings!.autoBackupLocation ?? ""
);
}
}
// this.personalPageModeList,
// this.backupFrequency,
// this.backupFrequencyOptions,
// this.autoBackupLocation,
// this.startDatebackup
@riverpod
Future<void> checkAndBackup(CheckAndBackupRef ref) async {
final settings = isar.settings.getSync(227);
if (settings!.backupFrequency != null) {
final backupFrequency = _duration(settings.backupFrequency);
if (backupFrequency != null) {
if (settings.startDatebackup != null) {
final startDatebackup =
DateTime.fromMillisecondsSinceEpoch(settings.startDatebackup!);
if (DateTime.now().isAfter(startDatebackup)) {
_setBackupFrequency(settings.backupFrequency!);
final storageProvider = StorageProvider();
await storageProvider.requestPermission();
final defaulteDirectory = await storageProvider.getDefaultDirectory();
final backupLocation = ref.watch(autoBackupLocationStateProvider).$2;
final backupDirectory = Directory(backupLocation.isEmpty
? "${defaulteDirectory!.path}backup/"
: backupLocation);
if (!(await backupDirectory.exists())) {
backupDirectory.create();
}
ref.watch(doBackUpProvider(
list: ref.watch(backupFrequencyOptionsStateProvider),
path: backupDirectory.path,
context: null));
}
}
}
}
}
Duration? _duration(int? backupFrequency) {
return switch (backupFrequency) {
1 => const Duration(hours: 6),
2 => const Duration(hours: 12),
3 => const Duration(days: 1),
4 => const Duration(days: 2),
5 => const Duration(days: 7),
_ => null
};
}
void _setBackupFrequency(int value) {
final settings = isar.settings.getSync(227);
final duration = _duration(value);
final now = DateTime.now();
final startDate = duration != null ? now.add(duration) : null;
isar.writeTxnSync(() => isar.settings.putSync(settings!
..backupFrequency = value
..startDatebackup = startDate?.millisecondsSinceEpoch));
}

View file

@ -6,8 +6,23 @@ part of 'auto_backup.dart';
// RiverpodGenerator
// **************************************************************************
String _$checkAndBackupHash() => r'039aea77df04d1925a25ad8839177cf9ee96aa65';
/// See also [checkAndBackup].
@ProviderFor(checkAndBackup)
final checkAndBackupProvider = AutoDisposeProvider<void>.internal(
checkAndBackup,
name: r'checkAndBackupProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$checkAndBackupHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef CheckAndBackupRef = AutoDisposeProviderRef<void>;
String _$backupFrequencyStateHash() =>
r'234368840b43f1f1bb3f4825f39c86bfa189a8ac';
r'2e73e3fe54456978ff92f49cdc67e84f2af6de7c';
/// See also [BackupFrequencyState].
@ProviderFor(BackupFrequencyState)

View file

@ -7,6 +7,7 @@ import 'package:isar/isar.dart';
import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/category.dart';
import 'package:mangayomi/models/chapter.dart';
import 'package:mangayomi/models/download.dart';
import 'package:mangayomi/models/history.dart';
import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/models/settings.dart';
@ -22,7 +23,7 @@ part 'backup.g.dart';
void doBackUp(DoBackUpRef ref,
{required List<int> list,
required String path,
required BuildContext context}) {
required BuildContext? context}) {
Map<String, dynamic> datas = {};
datas.addAll({"version": "1"});
if (list.contains(0)) {
@ -53,6 +54,13 @@ void doBackUp(DoBackUpRef ref,
.map((e) => e.toJson())
.toList();
datas.addAll({"chapters": res});
final res_ = isar.downloads
.filter()
.idIsNotNull()
.findAllSync()
.map((e) => e.toJson())
.toList();
datas.addAll({"downloads": res_});
}
if (list.contains(3)) {
final res = isar.tracks
@ -108,28 +116,30 @@ void doBackUp(DoBackUpRef ref,
encoder.addFile(File(backupFilePath));
encoder.close();
Directory(backupFilePath).deleteSync(recursive: true);
Navigator.pop(context);
BotToast.showNotification(
animationDuration: const Duration(milliseconds: 200),
animationReverseDuration: const Duration(milliseconds: 200),
duration: const Duration(seconds: 5),
backButtonBehavior: BackButtonBehavior.none,
leading: (cancel) =>
Image.asset('assets/app_icons/icon-red.png', height: 40),
title: (_) => const Text(
"Backup created!",
style: TextStyle(fontWeight: FontWeight.bold),
),
trailing: (_) => UnconstrainedBox(
alignment: Alignment.topLeft,
child: ElevatedButton(
onPressed: () {
Share.shareXFiles([XFile('$path/$name.backup')],
text: '$name.backup');
},
child: const Text('Share')),
),
enableSlideOff: true,
onlyOne: true,
crossPage: true);
if (context != null) {
Navigator.pop(context);
BotToast.showNotification(
animationDuration: const Duration(milliseconds: 200),
animationReverseDuration: const Duration(milliseconds: 200),
duration: const Duration(seconds: 5),
backButtonBehavior: BackButtonBehavior.none,
leading: (cancel) =>
Image.asset('assets/app_icons/icon-red.png', height: 40),
title: (_) => const Text(
"Backup created!",
style: TextStyle(fontWeight: FontWeight.bold),
),
trailing: (_) => UnconstrainedBox(
alignment: Alignment.topLeft,
child: ElevatedButton(
onPressed: () {
Share.shareXFiles([XFile('$path/$name.backup')],
text: '$name.backup');
},
child: const Text('Share')),
),
enableSlideOff: true,
onlyOne: true,
crossPage: true);
}
}

View file

@ -6,7 +6,7 @@ part of 'backup.dart';
// RiverpodGenerator
// **************************************************************************
String _$doBackUpHash() => r'e94ed6a96237dd5efc68bf26f26b276ffd777472';
String _$doBackUpHash() => r'4418d6aa9ea87ffa30195af59f9e93f95f7915f6';
/// Copied from Dart SDK
class _SystemHash {
@ -42,7 +42,7 @@ class DoBackUpFamily extends Family<void> {
DoBackUpProvider call({
required List<int> list,
required String path,
required BuildContext context,
required BuildContext? context,
}) {
return DoBackUpProvider(
list: list,
@ -83,7 +83,7 @@ class DoBackUpProvider extends AutoDisposeProvider<void> {
DoBackUpProvider({
required List<int> list,
required String path,
required BuildContext context,
required BuildContext? context,
}) : this._internal(
(ref) => doBackUp(
ref as DoBackUpRef,
@ -118,7 +118,7 @@ class DoBackUpProvider extends AutoDisposeProvider<void> {
final List<int> list;
final String path;
final BuildContext context;
final BuildContext? context;
@override
Override overrideWith(
@ -172,7 +172,7 @@ mixin DoBackUpRef on AutoDisposeProviderRef<void> {
String get path;
/// The parameter `context` of this provider.
BuildContext get context;
BuildContext? get context;
}
class _DoBackUpProviderElement extends AutoDisposeProviderElement<void>
@ -184,7 +184,7 @@ class _DoBackUpProviderElement extends AutoDisposeProviderElement<void>
@override
String get path => (origin as DoBackUpProvider).path;
@override
BuildContext get context => (origin as DoBackUpProvider).context;
BuildContext? get context => (origin as DoBackUpProvider).context;
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View file

@ -6,6 +6,7 @@ import 'package:mangayomi/eval/model/m_bridge.dart';
import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/category.dart';
import 'package:mangayomi/models/chapter.dart';
import 'package:mangayomi/models/download.dart';
import 'package:mangayomi/models/history.dart';
import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/models/settings.dart';
@ -45,6 +46,9 @@ void doRestore(DoRestoreRef ref,
final history = (backup["history"] as List?)
?.map((e) => History.fromJson(e))
.toList();
final downloads = (backup["downloads"] as List?)
?.map((e) => Download.fromJson(e))
.toList();
final settings = (backup["settings"] as List?)
?.map((e) => Settings.fromJson(e))
.toList();
@ -66,6 +70,17 @@ void doRestore(DoRestoreRef ref,
}
}
isar.downloads.clearSync();
if (downloads != null) {
for (var download in downloads) {
final chapter = isar.chapters.getSync(download.chapterId!);
if (chapter != null) {
isar.downloads.putSync(download..chapter.value = chapter);
download.chapter.saveSync();
}
}
}
isar.historys.clearSync();
if (history != null) {
for (var element in history) {

View file

@ -6,7 +6,7 @@ part of 'restore.dart';
// RiverpodGenerator
// **************************************************************************
String _$doRestoreHash() => r'99dffa1f733b1d8ca19be263e70380915bf70590';
String _$doRestoreHash() => r'3faca290a7217c27da2cdf21a559cec077a0201e';
/// Copied from Dart SDK
class _SystemHash {

View file

@ -56,20 +56,12 @@ class MoreScreen extends StatelessWidget {
icon: Icons.label_outline_rounded,
title: l10n.categories,
),
// ListTileWidget(
// onTap: () {
// context.push('/history');
// },
// icon: Icons.history_outlined,
// title: l10n.history,
// ),
ListTileWidget(
onTap: () {
context.push('/backupAndRestore');
},
icon: Icons.settings_backup_restore_sharp,
title: 'Backup and restore',
title: l10n.backup_and_restore,
),
const Divider(),
@ -86,11 +78,11 @@ class MoreScreen extends StatelessWidget {
},
icon: Icons.info_outline,
title: l10n.about),
ListTileWidget(
onTap: () {},
icon: Icons.help_outline,
title: l10n.help,
),
// ListTileWidget(
// onTap: () {},
// icon: Icons.help_outline,
// title: l10n.help,
// ),
],
),
),