Skip to content

Commit

Permalink
Added Backup & Restore functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
encrystudio committed Apr 14, 2024
1 parent 2d50694 commit 6d04261
Show file tree
Hide file tree
Showing 7 changed files with 406 additions and 16 deletions.
28 changes: 28 additions & 0 deletions lib/ui/screens/Settings/settings_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import 'package:url_launcher/url_launcher.dart';
import '../../widgets/common_dialog_widget.dart';
import '../../widgets/cust_switch.dart';
import '../../widgets/export_file_dialog.dart';
import '../../widgets/backup_dialog.dart';
import '../../widgets/restore_dialog.dart';
import '../Library/library_controller.dart';
import '../../widgets/snackbar.dart';
import '/ui/widgets/link_piped.dart';
Expand Down Expand Up @@ -446,6 +448,32 @@ class SettingsScreen extends StatelessWidget {
onChanged:
settingsController.toggleStopPlyabackOnSwipeAway),
)),
ListTile(
contentPadding: const EdgeInsets.only(left: 5, right: 10),
title: Text("backupSettingsAndPlaylists".tr),
subtitle: Text(
"backupSettingsAndPlaylistsDes".tr,
style: Theme.of(context).textTheme.bodyMedium,
),
isThreeLine: true,
onTap: () => showDialog(
context: context,
builder: (context) => const BackupDialog(),
).whenComplete(() => Get.delete<BackupDialogController>()),
),
ListTile(
contentPadding: const EdgeInsets.only(left: 5, right: 10),
title: Text("restoreSettingsAndPlaylists".tr),
subtitle: Text(
"restoreSettingsAndPlaylistsDes".tr,
style: Theme.of(context).textTheme.bodyMedium,
),
isThreeLine: true,
onTap: () => showDialog(
context: context,
builder: (context) => const RestoreDialog(),
).whenComplete(() => Get.delete<RestoreDialogController>()),
),
GetPlatform.isAndroid
? Obx(
() => ListTile(
Expand Down
6 changes: 5 additions & 1 deletion lib/ui/screens/Settings/settings_screen_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ class SettingsScreenController extends GetxController {
await box.clear();
await box.close();
});
}else{
} else {
await Hive.openBox("homeScreenData");
Get.find<HomeScreenController>().cachedHomeScreenData(updateAll: true);
}
Expand Down Expand Up @@ -265,4 +265,8 @@ class SettingsScreenController extends GetxController {
setBox.put('stopPlyabackOnSwipeAway', val);
stopPlyabackOnSwipeAway.value = val;
}

Future<void> closeAllDatabases() async {
await Hive.close();
}
}
169 changes: 169 additions & 0 deletions lib/ui/widgets/backup_dialog.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import 'dart:io';

import 'package:archive/archive_io.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:harmonymusic/ui/screens/Settings/settings_screen_controller.dart';
import 'package:harmonymusic/ui/widgets/loader.dart';

import '../../services/permission_service.dart';
import 'common_dialog_widget.dart';

class BackupDialog extends StatelessWidget {
const BackupDialog({super.key});

@override
Widget build(BuildContext context) {
final backupDialogController = Get.put(BackupDialogController());
return CommonDialog(
child: Container(
height: 300,
padding:
const EdgeInsets.only(top: 20, bottom: 30, left: 20, right: 20),
child: Stack(
children: [
Column(crossAxisAlignment: CrossAxisAlignment.center, children: [
Container(
padding: const EdgeInsets.only(bottom: 10.0, top: 10),
child: Text(
"backupSettingsAndPlaylists".tr,
style: Theme.of(context).textTheme.titleMedium,
),
),
SizedBox(
height: 150,
child: Center(
child: Obx(() => backupDialogController.exportProgress
.toInt() ==
backupDialogController.filesToExport.length
? Text("backupMsg".tr)
: backupDialogController.exportRunning.isTrue
? Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"${backupDialogController.exportProgress.toInt()}/${backupDialogController.filesToExport.length}",
style:
Theme.of(context).textTheme.titleLarge),
const SizedBox(
height: 10,
),
Text("exporting".tr)
],
)
: backupDialogController.ready.isTrue
? Text(
"${backupDialogController.filesToExport.length} ${"backFilesFound".tr}")
: backupDialogController.scanning.isTrue
? Column(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
const LoadingIndicator(),
const SizedBox(
height: 10,
),
Text("scanning".tr)
],
)
: const SizedBox()),
),
),
SizedBox(
width: double.maxFinite,
child: Align(
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).textTheme.titleLarge!.color,
borderRadius: BorderRadius.circular(10)),
child: InkWell(
onTap: () {
if (backupDialogController.exportProgress.toInt() ==
backupDialogController.filesToExport.length) {
Navigator.of(context).pop();
} else {
backupDialogController.backup();
}
},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 15.0, vertical: 10),
child: Obx(
() => Text(
backupDialogController.exportProgress.toInt() ==
backupDialogController.filesToExport.length
? "close".tr
: "export".tr,
style:
TextStyle(color: Theme.of(context).canvasColor),
),
),
),
),
),
),
),
]),
],
),
),
);
}
}

class BackupDialogController extends GetxController {
final scanning = true.obs;
final ready = false.obs;
final exportRunning = false.obs;
final exportProgress = (-1).obs;
List<String> filesToExport = [];

@override
void onInit() {
scanFilesToBackup();
super.onInit();
}

Future<void> scanFilesToBackup() async {
final supportDirPath = Get.find<SettingsScreenController>().supportDirPath;
final filesEntityList =
Directory("$supportDirPath/db").listSync(recursive: false);
final filesPath = filesEntityList.map((entity) => entity.path).toList();
filesToExport.addAll(filesPath);
scanning.value = false;
ready.value = true;
}

Future<void> backup() async {
if (!await PermissionService.getExtStoragePermission()) {
return;
}

if (!await PermissionService.getExtStoragePermission()) {
return;
}

final String? pickedFolderPath = await FilePicker.platform
.getDirectoryPath(dialogTitle: "Select backup file folder");
if (pickedFolderPath == '/' || pickedFolderPath == null) {
return;
}

exportProgress.value = 0;
exportRunning.value = true;
final exportDirPath = pickedFolderPath.toString();

var encoder = ZipFileEncoder();
encoder.create(
'$exportDirPath/${DateTime.now().millisecondsSinceEpoch.toString()}.hmb');
final length_ = filesToExport.length;
for (int i = 0; i < length_; i++) {
final filePath = filesToExport[i];
await encoder.addFile(File(filePath));
exportProgress.value = i + 1;
}
encoder.close();
exportRunning.value = false;
}
}
154 changes: 154 additions & 0 deletions lib/ui/widgets/restore_dialog.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import 'dart:io';

import 'package:archive/archive_io.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:harmonymusic/ui/screens/Settings/settings_screen_controller.dart';

import '../../services/permission_service.dart';
import 'common_dialog_widget.dart';

import 'package:path/path.dart' as p;

class RestoreDialog extends StatelessWidget {
const RestoreDialog({super.key});

@override
Widget build(BuildContext context) {
final restoreDialogController = Get.put(RestoreDialogController());
return CommonDialog(
child: Container(
height: 300,
padding:
const EdgeInsets.only(top: 20, bottom: 30, left: 20, right: 20),
child: Stack(
children: [
Column(crossAxisAlignment: CrossAxisAlignment.center, children: [
Container(
padding: const EdgeInsets.only(bottom: 10.0, top: 10),
child: Text(
"restoreSettingsAndPlaylists".tr,
style: Theme.of(context).textTheme.titleMedium,
),
),
SizedBox(
height: 150,
child: Center(
child: Obx(() => restoreDialogController.restoreProgress
.toInt() ==
restoreDialogController.filesToRestore.toInt()
? Text("restoreMsg".tr)
: restoreDialogController.restoreRunning.isTrue
? Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"${restoreDialogController.restoreProgress.toInt()}/${restoreDialogController.filesToRestore.toInt()}",
style:
Theme.of(context).textTheme.titleLarge),
const SizedBox(
height: 10,
),
Text("restoring".tr)
],
)
: const SizedBox()),
),
),
SizedBox(
width: double.maxFinite,
child: Align(
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).textTheme.titleLarge!.color,
borderRadius: BorderRadius.circular(10)),
child: InkWell(
onTap: () {
if (restoreDialogController.restoreProgress.toInt() ==
restoreDialogController.filesToRestore.toInt()) {
exit(0);
} else {
restoreDialogController.backup();
}
},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 15.0, vertical: 10),
child: Obx(
() => Text(
restoreDialogController.restoreProgress.toInt() ==
restoreDialogController.filesToRestore
.toInt()
? "closeApp".tr
: "restore".tr,
style:
TextStyle(color: Theme.of(context).canvasColor),
),
),
),
),
),
),
),
]),
],
),
),
);
}
}

class RestoreDialogController extends GetxController {
final restoreRunning = false.obs;
final restoreProgress = (-1).obs;
final filesToRestore = (0).obs;

Future<void> backup() async {
if (!await PermissionService.getExtStoragePermission()) {
return;
}

if (!await PermissionService.getExtStoragePermission()) {
return;
}

final FilePickerResult? pickedFileResult = await FilePicker.platform
.pickFiles(
dialogTitle: "Select backup file",
type: FileType.custom,
allowedExtensions: ['hmb'],
allowMultiple: false);

final String? pickedFile = pickedFileResult?.files.first.path;

// is this check necessary?
if (pickedFile == '/' || pickedFile == null) {
return;
}

restoreProgress.value = 0;
restoreRunning.value = true;
final restoreFilePath = pickedFile.toString();
final dbDirPath =
p.join(Get.find<SettingsScreenController>().supportDirPath, "db");
final Directory dbDir = Directory(dbDirPath);
printInfo(info: dbDir.path);
await Get.find<SettingsScreenController>().closeAllDatabases();
await dbDir.delete(recursive: true);
final bytes = await File(restoreFilePath).readAsBytes();
final archive = ZipDecoder().decodeBytes(bytes);
filesToRestore.value = archive.length;
for (final file in archive) {
final filename = file.name;
if (file.isFile) {
final data = file.content as List<int>;
final outputFile = File('$dbDirPath/$filename');
await outputFile.create(recursive: true);
await outputFile.writeAsBytes(data);
restoreProgress.value++;
}
}
restoreRunning.value = false;
}
}
Loading

0 comments on commit 6d04261

Please sign in to comment.