-
-
Notifications
You must be signed in to change notification settings - Fork 58
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #335 from mateusz-bak/175-feature_request-add-impo…
…rt-and-export-csv-feature Import/export books as CSV file, import Goodreads CSV
- Loading branch information
Showing
17 changed files
with
1,974 additions
and
1,190 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,228 @@ | ||
import 'dart:convert'; | ||
import 'dart:io'; | ||
|
||
import 'package:flutter/material.dart'; | ||
|
||
import 'package:archive/archive.dart'; | ||
import 'package:easy_localization/easy_localization.dart'; | ||
import 'package:flutter_bloc/flutter_bloc.dart'; | ||
import 'package:package_info_plus/package_info_plus.dart'; | ||
import 'package:shared_storage/shared_storage.dart'; | ||
|
||
import 'package:openreads/core/helpers/backup/backup_helpers.dart'; | ||
import 'package:openreads/generated/locale_keys.g.dart'; | ||
import 'package:openreads/logic/bloc/challenge_bloc/challenge_bloc.dart'; | ||
import 'package:openreads/main.dart'; | ||
|
||
class BackupExport { | ||
static createLocalBackupLegacyStorage(BuildContext context) async { | ||
final tmpBackupPath = await prepareTemporaryBackup(context); | ||
if (tmpBackupPath == null) return; | ||
|
||
try { | ||
// ignore: use_build_context_synchronously | ||
final backupPath = await BackupGeneral.openFolderPicker(context); | ||
final fileName = await _prepareBackupFileName(); | ||
|
||
final filePath = '$backupPath/$fileName'; | ||
|
||
File(filePath).writeAsBytesSync(File(tmpBackupPath).readAsBytesSync()); | ||
|
||
BackupGeneral.showInfoSnackbar(LocaleKeys.backup_successfull.tr()); | ||
} catch (e) { | ||
BackupGeneral.showInfoSnackbar(e.toString()); | ||
} | ||
} | ||
|
||
static Future createLocalBackup(BuildContext context) async { | ||
final tmpBackupPath = await prepareTemporaryBackup(context); | ||
if (tmpBackupPath == null) return; | ||
|
||
final selectedUriDir = await openDocumentTree(); | ||
|
||
if (selectedUriDir == null) { | ||
return; | ||
} | ||
|
||
final fileName = await _prepareBackupFileName(); | ||
|
||
try { | ||
createFileAsBytes( | ||
selectedUriDir, | ||
mimeType: '', | ||
displayName: fileName, | ||
bytes: File(tmpBackupPath).readAsBytesSync(), | ||
); | ||
|
||
BackupGeneral.showInfoSnackbar(LocaleKeys.backup_successfull.tr()); | ||
} catch (e) { | ||
BackupGeneral.showInfoSnackbar(e.toString()); | ||
} | ||
} | ||
|
||
static Future<String?> prepareTemporaryBackup(BuildContext context) async { | ||
try { | ||
await bookCubit.getAllBooks(tags: true); | ||
|
||
final books = await bookCubit.allBooks.first; | ||
final listOfBookJSONs = List<String>.empty(growable: true); | ||
final coverFiles = List<File>.empty(growable: true); | ||
|
||
for (var book in books) { | ||
// Making sure no covers are stored as JSON | ||
final bookWithCoverNull = book.copyWithNullCover(); | ||
|
||
listOfBookJSONs.add(jsonEncode(bookWithCoverNull.toJSON())); | ||
|
||
// Creating a list of current cover files | ||
if (book.hasCover) { | ||
// Check if cover file exists, if not then skip | ||
if (!File('${appDocumentsDirectory.path}/${book.id}.jpg') | ||
.existsSync()) { | ||
continue; | ||
} | ||
|
||
final coverFile = File( | ||
'${appDocumentsDirectory.path}/${book.id}.jpg', | ||
); | ||
coverFiles.add(coverFile); | ||
} | ||
|
||
await Future.delayed(const Duration(milliseconds: 50)); | ||
} | ||
|
||
// ignore: use_build_context_synchronously | ||
final challengeTargets = await _getChallengeTargets(context); | ||
|
||
// ignore: use_build_context_synchronously | ||
return await _writeTempBackupFile( | ||
listOfBookJSONs, | ||
challengeTargets, | ||
coverFiles, | ||
); | ||
} catch (e) { | ||
BackupGeneral.showInfoSnackbar(e.toString()); | ||
|
||
return null; | ||
} | ||
} | ||
|
||
static Future<String?> _getChallengeTargets(BuildContext context) async { | ||
if (!context.mounted) return null; | ||
|
||
final state = BlocProvider.of<ChallengeBloc>(context).state; | ||
|
||
if (state is SetChallengeState) { | ||
return state.yearlyChallenges; | ||
} | ||
|
||
return null; | ||
} | ||
|
||
// Current backup version: 5 | ||
static Future<String?> _writeTempBackupFile( | ||
List<String> listOfBookJSONs, | ||
String? challengeTargets, | ||
List<File>? coverFiles, | ||
) async { | ||
final data = listOfBookJSONs.join('@@@@@'); | ||
final fileName = await _prepareBackupFileName(); | ||
final tmpFilePath = '${appTempDirectory.path}/$fileName'; | ||
|
||
try { | ||
// Saving books to temp file | ||
File('${appTempDirectory.path}/books.backup').writeAsStringSync(data); | ||
|
||
// Reading books temp file to memory | ||
final booksBytes = | ||
File('${appTempDirectory.path}/books.backup').readAsBytesSync(); | ||
|
||
final archivedBooks = ArchiveFile( | ||
'books.backup', | ||
booksBytes.length, | ||
booksBytes, | ||
); | ||
|
||
// Prepare main archive | ||
final archive = Archive(); | ||
archive.addFile(archivedBooks); | ||
|
||
if (challengeTargets != null) { | ||
// Saving challenges to temp file | ||
File('${appTempDirectory.path}/challenges.backup') | ||
.writeAsStringSync(challengeTargets); | ||
|
||
// Reading challenges temp file to memory | ||
final challengeTargetsBytes = | ||
File('${appTempDirectory.path}/challenges.backup') | ||
.readAsBytesSync(); | ||
|
||
final archivedChallengeTargets = ArchiveFile( | ||
'challenges.backup', | ||
challengeTargetsBytes.length, | ||
challengeTargetsBytes, | ||
); | ||
|
||
archive.addFile(archivedChallengeTargets); | ||
} | ||
|
||
// Adding covers to the backup file | ||
if (coverFiles != null && coverFiles.isNotEmpty) { | ||
for (var coverFile in coverFiles) { | ||
final coverBytes = coverFile.readAsBytesSync(); | ||
|
||
final archivedCover = ArchiveFile( | ||
coverFile.path.split('/').last, | ||
coverBytes.length, | ||
coverBytes, | ||
); | ||
|
||
archive.addFile(archivedCover); | ||
} | ||
} | ||
|
||
// Add info file | ||
final info = await _prepareBackupInfo(); | ||
final infoBytes = utf8.encode(info); | ||
|
||
final archivedInfo = ArchiveFile( | ||
'info.txt', | ||
infoBytes.length, | ||
infoBytes, | ||
); | ||
archive.addFile(archivedInfo); | ||
|
||
final encodedArchive = ZipEncoder().encode(archive); | ||
|
||
if (encodedArchive == null) return null; | ||
|
||
File(tmpFilePath).writeAsBytesSync(encodedArchive); | ||
|
||
return tmpFilePath; | ||
} catch (e) { | ||
BackupGeneral.showInfoSnackbar(e.toString()); | ||
|
||
return null; | ||
} | ||
} | ||
|
||
static Future<String> _getAppVersion() async { | ||
PackageInfo packageInfo = await PackageInfo.fromPlatform(); | ||
|
||
return packageInfo.version; | ||
} | ||
|
||
static Future<String> _prepareBackupFileName() async { | ||
final date = DateTime.now(); | ||
final backupDate = | ||
'${date.year}_${date.month}_${date.day}-${date.hour}_${date.minute}_${date.second}'; | ||
|
||
return 'Openreads-$backupDate.backup'; | ||
} | ||
|
||
static Future<String> _prepareBackupInfo() async { | ||
final appVersion = await _getAppVersion(); | ||
|
||
return 'App version: $appVersion\nBackup version: 5'; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
import 'dart:io'; | ||
|
||
import 'package:flutter/material.dart'; | ||
|
||
import 'package:easy_localization/easy_localization.dart'; | ||
import 'package:filesystem_picker/filesystem_picker.dart'; | ||
import 'package:openreads/main.dart'; | ||
import 'package:permission_handler/permission_handler.dart'; | ||
|
||
import 'package:openreads/generated/locale_keys.g.dart'; | ||
|
||
class BackupGeneral { | ||
static showInfoSnackbar(String message) { | ||
final snackBar = SnackBar(content: Text(message)); | ||
snackbarKey.currentState?.showSnackBar(snackBar); | ||
} | ||
|
||
static Future<bool> requestStoragePermission(BuildContext context) async { | ||
if (await Permission.storage.isPermanentlyDenied) { | ||
// ignore: use_build_context_synchronously | ||
_openSystemSettings(context); | ||
return false; | ||
} else if (await Permission.storage.status.isDenied) { | ||
if (await Permission.storage.request().isGranted) { | ||
return true; | ||
} else { | ||
// ignore: use_build_context_synchronously | ||
_openSystemSettings(context); | ||
return false; | ||
} | ||
} else if (await Permission.storage.status.isGranted) { | ||
return true; | ||
} | ||
return false; | ||
} | ||
|
||
static Future<String?> openFolderPicker(BuildContext context) async { | ||
if (!context.mounted) return null; | ||
|
||
return await FilesystemPicker.open( | ||
context: context, | ||
title: LocaleKeys.choose_backup_folder.tr(), | ||
pickText: LocaleKeys.save_file_to_this_folder.tr(), | ||
fsType: FilesystemType.folder, | ||
rootDirectory: Directory('/storage/emulated/0/'), | ||
contextActions: [ | ||
FilesystemPickerNewFolderContextAction( | ||
icon: Icon( | ||
Icons.create_new_folder, | ||
color: Theme.of(context).colorScheme.primary, | ||
size: 24, | ||
), | ||
), | ||
], | ||
theme: FilesystemPickerTheme( | ||
backgroundColor: Theme.of(context).colorScheme.surface, | ||
pickerAction: FilesystemPickerActionThemeData( | ||
foregroundColor: Theme.of(context).colorScheme.primary, | ||
backgroundColor: Theme.of(context).colorScheme.surface, | ||
), | ||
fileList: FilesystemPickerFileListThemeData( | ||
iconSize: 24, | ||
upIconSize: 24, | ||
checkIconSize: 24, | ||
folderTextStyle: const TextStyle(fontSize: 16), | ||
), | ||
topBar: FilesystemPickerTopBarThemeData( | ||
backgroundColor: Theme.of(context).colorScheme.surface, | ||
titleTextStyle: const TextStyle(fontSize: 18), | ||
elevation: 0, | ||
shadowColor: Colors.transparent, | ||
), | ||
), | ||
); | ||
} | ||
|
||
static Future<String?> openFilePicker( | ||
BuildContext context, { | ||
List<String> allowedExtensions = const ['.backup', '.zip', '.png'], | ||
}) async { | ||
if (!context.mounted) return null; | ||
|
||
return await FilesystemPicker.open( | ||
context: context, | ||
title: LocaleKeys.choose_backup_file.tr(), | ||
pickText: LocaleKeys.use_this_file.tr(), | ||
fsType: FilesystemType.file, | ||
rootDirectory: Directory('/storage/emulated/0/'), | ||
fileTileSelectMode: FileTileSelectMode.wholeTile, | ||
allowedExtensions: allowedExtensions, | ||
theme: FilesystemPickerTheme( | ||
backgroundColor: Theme.of(context).colorScheme.surface, | ||
fileList: FilesystemPickerFileListThemeData( | ||
iconSize: 24, | ||
upIconSize: 24, | ||
checkIconSize: 24, | ||
folderTextStyle: const TextStyle(fontSize: 16), | ||
), | ||
topBar: FilesystemPickerTopBarThemeData( | ||
titleTextStyle: const TextStyle(fontSize: 18), | ||
shadowColor: Colors.transparent, | ||
foregroundColor: Theme.of(context).colorScheme.onSurface, | ||
), | ||
), | ||
); | ||
} | ||
|
||
static _openSystemSettings(BuildContext context) { | ||
if (!context.mounted) return; | ||
|
||
ScaffoldMessenger.of(context).showSnackBar( | ||
SnackBar( | ||
content: Text( | ||
LocaleKeys.need_storage_permission.tr(), | ||
), | ||
action: SnackBarAction( | ||
label: LocaleKeys.open_settings.tr(), | ||
onPressed: () { | ||
if (context.mounted) { | ||
openAppSettings(); | ||
} | ||
}, | ||
), | ||
), | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export 'backup_export.dart'; | ||
export 'backup_general.dart'; | ||
export 'backup_import.dart'; | ||
export 'csv_export.dart'; | ||
export 'csv_goodreads_import.dart'; |
Oops, something went wrong.