feat: add a setting for the destination folder of downloaded books

This commit is contained in:
Clovis DUGUÉ
2024-08-24 00:23:12 +02:00
parent 1025552f2f
commit 7f36ca98b7
13 changed files with 235 additions and 58 deletions

View File

@ -26,8 +26,7 @@ import 'package:openlib/state/state.dart'
themeModeProvider,
openPdfWithExternalAppProvider,
userAgentProvider,
cookieProvider,
dbProvider;
cookieProvider;
void main() async {
WidgetsFlutterBinding.ensureInitialized();
@ -37,13 +36,15 @@ void main() async {
databaseFactory = databaseFactoryFfi;
}
Database initDb = await Sqlite.initDb();
MyLibraryDb dataBase = MyLibraryDb(dbInstance: initDb);
bool isDarkMode = await dataBase.getPreference('darkMode');
bool openPdfwithExternalapp =
await dataBase.getPreference('openPdfwithExternalApp');
bool openEpubwithExternalapp =
await dataBase.getPreference('openEpubwithExternalApp');
MyLibraryDb dataBase = MyLibraryDb.instance;
bool isDarkMode =
await dataBase.getPreference('darkMode') == 0 ? false : true;
bool openPdfwithExternalapp = await dataBase
.getPreference('openPdfwithExternalApp')
.catchError((e) => print(e)) ==
0
? false
: true;
String browserUserAgent = await dataBase.getBrowserOptions('userAgent');
String browserCookie = await dataBase.getBrowserOptions('cookie');
@ -59,7 +60,6 @@ void main() async {
runApp(
ProviderScope(
overrides: [
dbProvider.overrideWithValue(dataBase),
themeModeProvider.overrideWith(
(ref) => isDarkMode ? ThemeMode.dark : ThemeMode.light),
openPdfWithExternalAppProvider

View File

@ -50,12 +50,79 @@ class MyBook {
}
class MyLibraryDb {
Database dbInstance;
static final MyLibraryDb instance = MyLibraryDb._internal();
static Database? _database;
MyLibraryDb._internal();
Future<Database> get database async {
if (_database != null) {
return _database!;
}
_database = await _initDatabase();
return _database!;
}
Future<Database> _initDatabase() async {
final databasePath = await getDatabasesPath();
final path = '$databasePath/mylibrary.db';
final bool isMobile = Platform.isAndroid || Platform.isIOS;
return await openDatabase(
path,
version: 5,
onCreate: (Database db, int version) async {
await db.execute(
'CREATE TABLE mybooks (id TEXT PRIMARY KEY, title TEXT,author TEXT,thumbnail TEXT,link TEXT,publisher TEXT,info TEXT,format TEXT,description TEXT)');
await db.execute(
'CREATE TABLE preferences (name TEXT PRIMARY KEY,value TEXT)');
if (isMobile || true) {
// TODO: Breaks getBrowserOptions() on Mac
await db.execute(
'CREATE TABLE bookposition (fileName TEXT PRIMARY KEY,position TEXT)');
await db.execute(
'CREATE TABLE browserOptions (name TEXT PRIMARY KEY,value TEXT)');
}
},
onUpgrade: (db, oldVersion, newVersion) async {
List<dynamic> isTableExist = await db.query('sqlite_master',
where: 'name = ?', whereArgs: ['bookposition']);
List<dynamic> isPreferenceTableExist = await db.query('sqlite_master',
where: 'name = ?', whereArgs: ['preferences']);
List<dynamic> isbrowserOptionsExist = await db.query('sqlite_master',
where: 'name = ?', whereArgs: ['browserOptions']);
if (isPreferenceTableExist.isEmpty) {
await db.execute(
'CREATE TABLE preferences (name TEXT PRIMARY KEY,value TEXT)');
}
if (isMobile && isTableExist.isEmpty) {
await db.execute(
'CREATE TABLE bookposition (fileName TEXT PRIMARY KEY,position TEXT)');
}
if (isMobile && isbrowserOptionsExist.isEmpty) {
await db.execute(
'CREATE TABLE browserOptions (name TEXT PRIMARY KEY,value TEXT)');
}
},
onOpen: (db) async {
final bookStorageDefaultDirectory =
await getBookStorageDefaultDirectory;
await db.execute(
"INSERT OR IGNORE INTO preferences (name, value) VALUES ('darkMode', 0)");
await db.execute(
"INSERT OR IGNORE INTO preferences (name, value) VALUES ('openPdfwithExternalApp', 0)");
await db.execute(
"INSERT OR IGNORE INTO preferences (name, value) VALUES ('openEpubwithExternalApp', 0)");
await db.execute(
"INSERT OR IGNORE INTO preferences (name, value) VALUES ('bookStorageDirectory', '$bookStorageDefaultDirectory')");
},
);
}
// Database dbInstance;
String tableName = 'mybooks';
MyLibraryDb({required this.dbInstance});
Future<void> insert(MyBook book) async {
final dbInstance = await instance.database;
await dbInstance.insert(
tableName,
book.toMap(),
@ -64,6 +131,7 @@ class MyLibraryDb {
}
Future<void> delete(String id) async {
final dbInstance = await instance.database;
await dbInstance.delete(
tableName,
where: 'id = ?',
@ -72,6 +140,7 @@ class MyLibraryDb {
}
Future<MyBook?> getId(String id) async {
final dbInstance = await instance.database;
List<Map<String, dynamic>> data =
await dbInstance.query(tableName, where: 'id = ?', whereArgs: [id]);
List<MyBook> book = listMapToMyBook(data);
@ -82,6 +151,7 @@ class MyLibraryDb {
}
Future<bool> checkIdExists(String id) async {
final dbInstance = await instance.database;
List<Map<String, dynamic>> data =
await dbInstance.query(tableName, where: 'id = ?', whereArgs: [id]);
List<MyBook> book = listMapToMyBook(data);
@ -92,6 +162,7 @@ class MyLibraryDb {
}
Future<List<MyBook>> getAll() async {
final dbInstance = await instance.database;
final List<Map<String, dynamic>> maps = await dbInstance.query(tableName);
return listMapToMyBook(maps);
}
@ -113,6 +184,7 @@ class MyLibraryDb {
}
Future<void> saveBookState(String fileName, String position) async {
final dbInstance = await instance.database;
await dbInstance.insert(
'bookposition',
{'fileName': fileName, 'position': position},
@ -121,6 +193,7 @@ class MyLibraryDb {
}
Future<void> deleteBookState(String fileName) async {
final dbInstance = await instance.database;
await dbInstance.delete(
'bookposition',
where: 'fileName = ?',
@ -129,6 +202,7 @@ class MyLibraryDb {
}
Future<String?> getBookState(String fileName) async {
final dbInstance = await instance.database;
List<Map<String, dynamic>> data = await dbInstance
.query('bookposition', where: 'fileName = ?', whereArgs: [fileName]);
List<dynamic> dataList = List.generate(data.length, (i) {
@ -141,29 +215,45 @@ class MyLibraryDb {
}
}
Future<void> savePreference(String name, bool value) async {
int boolInt = value ? 1 : 0;
Future<void> savePreference(String name, dynamic value) async {
switch (value.runtimeType) {
case bool:
value = value ? 1 : 0;
break;
case int || String:
break;
default:
throw 'Invalid type';
}
Database dbInstance = await instance.database;
await dbInstance.insert(
'preferences',
{'name': name, 'value': boolInt},
{'name': name, 'value': value},
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
Future<bool> getPreference(String name) async {
Future<dynamic> getPreference(String name) async {
Database dbInstance = await instance.database;
List<Map<String, dynamic>> data = await dbInstance
.query('preferences', where: 'name = ?', whereArgs: [name]);
List<dynamic> dataList = List.generate(data.length, (i) {
return {'name': data[i]['name'], 'value': data[i]['value']};
});
if (dataList.isNotEmpty) {
return dataList[0]['value'] == 0 ? false : true;
} else {
return false;
// Convert to int if possible
int? preference = int.tryParse(dataList[0]['value']);
if (preference != null) {
return preference;
}
// Return string value if not int
return dataList[0]['value'];
}
throw "Preference $name not found";
}
Future<void> setBrowserOptions(String name, String value) async {
final dbInstance = await instance.database;
await dbInstance.insert(
'browserOptions',
{'name': name, 'value': value},
@ -172,6 +262,7 @@ class MyLibraryDb {
}
Future<String> getBrowserOptions(String name) async {
final dbInstance = await instance.database;
List<Map<String, dynamic>> data = await dbInstance
.query('browserOptions', where: 'name = ?', whereArgs: [name]);
List<dynamic> dataList = List.generate(data.length, (i) {

View File

@ -7,11 +7,13 @@ import 'package:dio/dio.dart';
// Project imports:
import 'package:openlib/services/database.dart' show MyLibraryDb;
import 'files.dart';
MyLibraryDb dataBase = MyLibraryDb.instance;
Future<String> _getFilePath(String fileName) async {
final path = await getAppDirectoryPath;
return '$path/$fileName';
String bookStorageDirectory =
await dataBase.getPreference('bookStorageDirectory');
return '$bookStorageDirectory/$fileName';
}
List<String> _reorderMirrors(List<String> mirrors) {
@ -114,8 +116,9 @@ Future<void> downloadFile(
Future<bool> verifyFileCheckSum(
{required String md5Hash, required String format}) async {
try {
final path = await getAppDirectoryPath;
final filePath = '$path/$md5Hash.$format';
final bookStorageDirectory =
await dataBase.getPreference('bookStorageDirectory');
final filePath = '$bookStorageDirectory/$md5Hash.$format';
final file = File(filePath);
final stream = file.openRead();
final hash = await md5.bind(stream).first;

View File

@ -6,8 +6,12 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:path_provider/path_provider.dart';
// Project imports:
import 'package:openlib/services/database.dart';
import 'package:openlib/state/state.dart' show myLibraryProvider;
Future<String> get getAppDirectoryPath async {
MyLibraryDb dataBase = MyLibraryDb.instance;
Future<String> get getBookStorageDefaultDirectory async {
if (Platform.isAndroid) {
final directory = await getExternalStorageDirectory();
return directory!.path;
@ -47,8 +51,9 @@ Future<void> deleteFile(String filePath) async {
}
Future<String> getFilePath(String fileName) async {
String appDirPath = await getAppDirectoryPath;
String filePath = '$appDirPath/$fileName';
final bookStorageDirectory =
await dataBase.getPreference('bookStorageDirectory');
String filePath = '$bookStorageDirectory/$fileName';
bool isExists = await isFileExists(filePath);
if (isExists == true) {
return filePath;
@ -60,10 +65,11 @@ Future<void> deleteFileWithDbData(
FutureProviderRef ref, String md5, String format) async {
try {
String fileName = '$md5.$format';
String appDirPath = await getAppDirectoryPath;
await deleteFile('$appDirPath/$fileName');
await ref.read(dbProvider).delete(md5);
await ref.read(dbProvider).deleteBookState(fileName);
final bookStorageDirectory =
await dataBase.getPreference('bookStorageDirectory');
await deleteFile('$bookStorageDirectory/$fileName');
await dataBase.delete(md5);
await dataBase.deleteBookState(fileName);
// ignore: unused_result
ref.refresh(myLibraryProvider);
} catch (e) {

View File

@ -14,6 +14,8 @@ import 'package:openlib/services/database.dart';
import 'package:openlib/services/files.dart';
import 'package:openlib/services/open_library.dart';
MyLibraryDb dataBase = MyLibraryDb.instance;
//Provider for dropdownbutton in search page
Map<String, String> typeValues = {
@ -151,15 +153,13 @@ final downloadState =
final checkSumState = StateProvider.autoDispose<CheckSumProcessState>(
(ref) => CheckSumProcessState.waiting);
final dbProvider = Provider<MyLibraryDb>((ref) => throw UnimplementedError());
final myLibraryProvider = FutureProvider((ref) async {
return await ref.read(dbProvider).getAll();
return dataBase.getAll();
});
final checkIdExists =
FutureProvider.family.autoDispose<bool, String>((ref, id) async {
return await ref.read(dbProvider).checkIdExists(id);
return await dataBase.checkIdExists(id);
});
class FileName {
@ -179,18 +179,18 @@ final totalPdfPage = StateProvider.autoDispose<int>((ref) => 0);
Future<void> savePdfState(String fileName, WidgetRef ref) async {
String position = ref.watch(pdfCurrentPage).toString();
await ref.watch(dbProvider).saveBookState(fileName, position);
await dataBase.saveBookState(fileName, position);
}
Future<void> saveEpubState(
String fileName, String? position, WidgetRef ref) async {
String pos = position ?? '';
await ref.watch(dbProvider).saveBookState(fileName, pos);
await dataBase.saveBookState(fileName, pos);
}
final getBookPosition =
FutureProvider.family.autoDispose<String?, String>((ref, fileName) async {
return await ref.read(dbProvider).getBookState(fileName);
return await dataBase.getBookState(fileName);
});
final openPdfWithExternalAppProvider = StateProvider<bool>((ref) => false);

View File

@ -34,7 +34,6 @@ import 'package:openlib/state/state.dart'
CheckSumProcessState,
downloadState,
checkSumState,
dbProvider,
checkIdExists,
myLibraryProvider;
@ -246,7 +245,9 @@ Future<void> downloadFileWidget(
ref.read(downloadProgressProvider.notifier).state = rcv / total;
if (rcv / total == 1.0) {
await ref.read(dbProvider).insert(MyBook(
MyLibraryDb dataBase = MyLibraryDb.instance;
await dataBase.insert(MyBook(
id: data.md5,
title: data.title,
author: data.author,

View File

@ -16,7 +16,7 @@ import 'package:openlib/services/files.dart' show getFilePath;
import 'package:openlib/ui/components/snack_bar_widget.dart';
import 'package:openlib/state/state.dart'
show filePathProvider, saveEpubState, dbProvider, getBookPosition;
show filePathProvider, saveEpubState, getBookPosition;
Future<void> launchEpubViewer(
{required String fileName,
@ -24,7 +24,9 @@ Future<void> launchEpubViewer(
required WidgetRef ref}) async {
if (Platform.isAndroid || Platform.isIOS) {
String path = await getFilePath(fileName);
String? epubConfig = await ref.read(dbProvider).getBookState(fileName);
MyLibraryDb dataBase = MyLibraryDb.instance;
String? epubConfig = await dataBase.getBookState(fileName);
await OpenFile.open(path);
} else {
Navigator.push(context, MaterialPageRoute(builder: (BuildContext context) {

View File

@ -6,8 +6,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
// Project imports:
import 'package:openlib/services/database.dart';
import 'package:openlib/state/state.dart' show dbProvider;
import 'package:openlib/ui/components/book_info_widget.dart';
import 'package:openlib/ui/components/file_buttons_widget.dart';
@ -26,7 +24,9 @@ class BookPage extends StatelessWidget {
),
body: Consumer(
builder: (BuildContext context, WidgetRef ref, _) {
final bookInfo = ref.read(dbProvider).getId(id);
MyLibraryDb dataBase = MyLibraryDb.instance;
final bookInfo = dataBase.getId(id);
return FutureBuilder(
future: bookInfo,

View File

@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
// Package imports:
import 'package:device_info_plus/device_info_plus.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:permission_handler/permission_handler.dart';
@ -16,13 +17,42 @@ import 'package:openlib/ui/about_page.dart';
import 'package:openlib/ui/components/page_title_widget.dart';
import 'package:openlib/state/state.dart'
show themeModeProvider, openPdfWithExternalAppProvider, dbProvider;
show themeModeProvider, openPdfWithExternalAppProvider;
Future<void> requestStoragePermission() async {
bool permissionGranted = false;
// Check whether the device is running Android 11 or higher
DeviceInfoPlugin plugin = DeviceInfoPlugin();
AndroidDeviceInfo android = await plugin.androidInfo;
// Android < 11
if (android.version.sdkInt < 33) {
if (await Permission.storage.request().isGranted) {
permissionGranted = true;
} else if (await Permission.storage.request().isPermanentlyDenied) {
await openAppSettings();
}
}
// Android > 11
else {
if (await Permission.manageExternalStorage.request().isGranted) {
permissionGranted = true;
} else if (await Permission.manageExternalStorage
.request()
.isPermanentlyDenied) {
await openAppSettings();
} else if (await Permission.manageExternalStorage.request().isDenied) {
permissionGranted = false;
}
}
print("Storage permission status: $permissionGranted");
}
class SettingsPage extends ConsumerWidget {
const SettingsPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
MyLibraryDb dataBase = MyLibraryDb.instance;
return Padding(
padding: const EdgeInsets.only(left: 5, right: 5, top: 10),
child: SingleChildScrollView(
@ -49,8 +79,7 @@ class SettingsPage extends ConsumerWidget {
onChanged: (bool value) {
ref.read(themeModeProvider.notifier).state =
value == true ? ThemeMode.dark : ThemeMode.light;
ref.read(dbProvider).savePreference('darkMode', value);
dataBase.savePreference('darkMode', value);
if (Platform.isAndroid) {
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
systemNavigationBarColor:
@ -77,13 +106,31 @@ class SettingsPage extends ConsumerWidget {
onChanged: (bool value) {
ref.read(openPdfWithExternalAppProvider.notifier).state =
value;
ref
.read(dbProvider)
.savePreference('openPdfwithExternalApp', value);
dataBase.savePreference('openPdfwithExternalApp', value);
},
)
],
),
_PaddedContainer(
onClick: () async {
String? pickedDirectory =
await FilePicker.platform.getDirectoryPath();
// TODO: Attempt moving existing books to the new directory
await requestStoragePermission();
dataBase.savePreference(
'bookStorageDirectory', pickedDirectory);
},
children: [
Text(
"Change storage path",
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.tertiary,
),
),
Icon(Icons.folder),
]),
_PaddedContainer(
onClick: () {
Navigator.push(context,

View File

@ -12,7 +12,7 @@ import 'package:webview_cookie_manager/webview_cookie_manager.dart'
as cookiejar;
import 'package:openlib/state/state.dart'
show cookieProvider, userAgentProvider, dbProvider, bookInfoProvider;
show cookieProvider, userAgentProvider, bookInfoProvider;
class Webview extends ConsumerStatefulWidget {
const Webview({super.key, required this.url});
@ -28,6 +28,7 @@ class _WebviewState extends ConsumerState<Webview> {
final cookieManager = cookiejar.WebviewCookieManager();
@override
Widget build(BuildContext context) {
MyLibraryDb dataBase = MyLibraryDb.instance;
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
@ -37,11 +38,12 @@ class _WebviewState extends ConsumerState<Webview> {
child: WebViewWidget(
controller: controller
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setBackgroundColor(const Color(0x00000000))
// ..setBackgroundColor(const Color(
// 0x00000000)) // TODO: Crashes macOS app. https://github.com/flutter/flutter/issues/153773
..loadRequest(Uri.parse(widget.url))
..getUserAgent().then((value) {
ref.read(userAgentProvider.notifier).state = value!;
ref.read(dbProvider).setBrowserOptions('userAgent', value);
dataBase.setBrowserOptions('userAgent', value);
})
..setNavigationDelegate(NavigationDelegate(
onPageStarted: (url) async {
@ -64,9 +66,7 @@ class _WebviewState extends ConsumerState<Webview> {
ref.read(cookieProvider.notifier).state = cfClearance;
await ref
.read(dbProvider)
.setBrowserOptions('cookie', cfClearance);
await dataBase.setBrowserOptions('cookie', cfClearance);
ref.invalidate(bookInfoProvider);

View File

@ -5,12 +5,14 @@
import FlutterMacOS
import Foundation
import device_info_plus
import path_provider_foundation
import sqflite
import url_launcher_macos
import webview_flutter_wkwebview
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))

View File

@ -137,6 +137,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.0"
device_info_plus:
dependency: "direct main"
description:
name: device_info_plus
sha256: a7fd703482b391a87d60b6061d04dfdeab07826b96f9abd8f5ed98068acc0074
url: "https://pub.dev"
source: hosted
version: "10.1.2"
device_info_plus_platform_interface:
dependency: transitive
description:
name: device_info_plus_platform_interface
sha256: "282d3cf731045a2feb66abfe61bbc40870ae50a3ed10a4d3d217556c35c8c2ba"
url: "https://pub.dev"
source: hosted
version: "7.0.1"
dio:
dependency: "direct main"
description:
@ -917,6 +933,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.5.4"
win32_registry:
dependency: transitive
description:
name: win32_registry
sha256: "723b7f851e5724c55409bb3d5a32b203b3afe8587eaf5dafb93a5fed8ecda0d6"
url: "https://pub.dev"
source: hosted
version: "1.1.4"
xdg_directories:
dependency: transitive
description:

View File

@ -62,6 +62,7 @@ dependencies:
dev: ^1.0.0
crypto: ^3.0.3
file_picker: ^8.1.2
device_info_plus: ^10.1.2
dev_dependencies:
import_sorter: ^4.6.0