diff --git a/lib/main.dart b/lib/main.dart index 77124df..25da600 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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 diff --git a/lib/services/database.dart b/lib/services/database.dart index a8ac3db..8b0a7ef 100644 --- a/lib/services/database.dart +++ b/lib/services/database.dart @@ -50,12 +50,79 @@ class MyBook { } class MyLibraryDb { - Database dbInstance; + static final MyLibraryDb instance = MyLibraryDb._internal(); + static Database? _database; + MyLibraryDb._internal(); + + Future get database async { + if (_database != null) { + return _database!; + } + _database = await _initDatabase(); + return _database!; + } + + Future _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 isTableExist = await db.query('sqlite_master', + where: 'name = ?', whereArgs: ['bookposition']); + List isPreferenceTableExist = await db.query('sqlite_master', + where: 'name = ?', whereArgs: ['preferences']); + List 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 insert(MyBook book) async { + final dbInstance = await instance.database; await dbInstance.insert( tableName, book.toMap(), @@ -64,6 +131,7 @@ class MyLibraryDb { } Future delete(String id) async { + final dbInstance = await instance.database; await dbInstance.delete( tableName, where: 'id = ?', @@ -72,6 +140,7 @@ class MyLibraryDb { } Future getId(String id) async { + final dbInstance = await instance.database; List> data = await dbInstance.query(tableName, where: 'id = ?', whereArgs: [id]); List book = listMapToMyBook(data); @@ -82,6 +151,7 @@ class MyLibraryDb { } Future checkIdExists(String id) async { + final dbInstance = await instance.database; List> data = await dbInstance.query(tableName, where: 'id = ?', whereArgs: [id]); List book = listMapToMyBook(data); @@ -92,6 +162,7 @@ class MyLibraryDb { } Future> getAll() async { + final dbInstance = await instance.database; final List> maps = await dbInstance.query(tableName); return listMapToMyBook(maps); } @@ -113,6 +184,7 @@ class MyLibraryDb { } Future 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 deleteBookState(String fileName) async { + final dbInstance = await instance.database; await dbInstance.delete( 'bookposition', where: 'fileName = ?', @@ -129,6 +202,7 @@ class MyLibraryDb { } Future getBookState(String fileName) async { + final dbInstance = await instance.database; List> data = await dbInstance .query('bookposition', where: 'fileName = ?', whereArgs: [fileName]); List dataList = List.generate(data.length, (i) { @@ -141,29 +215,45 @@ class MyLibraryDb { } } - Future savePreference(String name, bool value) async { - int boolInt = value ? 1 : 0; + Future 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 getPreference(String name) async { + Future getPreference(String name) async { + Database dbInstance = await instance.database; List> data = await dbInstance .query('preferences', where: 'name = ?', whereArgs: [name]); List 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 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 getBrowserOptions(String name) async { + final dbInstance = await instance.database; List> data = await dbInstance .query('browserOptions', where: 'name = ?', whereArgs: [name]); List dataList = List.generate(data.length, (i) { diff --git a/lib/services/download_file.dart b/lib/services/download_file.dart index 7664196..7c8fe34 100644 --- a/lib/services/download_file.dart +++ b/lib/services/download_file.dart @@ -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 _getFilePath(String fileName) async { - final path = await getAppDirectoryPath; - return '$path/$fileName'; + String bookStorageDirectory = + await dataBase.getPreference('bookStorageDirectory'); + return '$bookStorageDirectory/$fileName'; } List _reorderMirrors(List mirrors) { @@ -114,8 +116,9 @@ Future downloadFile( Future 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; diff --git a/lib/services/files.dart b/lib/services/files.dart index 5a642ae..3bcd593 100644 --- a/lib/services/files.dart +++ b/lib/services/files.dart @@ -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 get getAppDirectoryPath async { +MyLibraryDb dataBase = MyLibraryDb.instance; + +Future get getBookStorageDefaultDirectory async { if (Platform.isAndroid) { final directory = await getExternalStorageDirectory(); return directory!.path; @@ -47,8 +51,9 @@ Future deleteFile(String filePath) async { } Future 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 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) { diff --git a/lib/state/state.dart b/lib/state/state.dart index 8540268..9b8573a 100644 --- a/lib/state/state.dart +++ b/lib/state/state.dart @@ -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 typeValues = { @@ -151,15 +153,13 @@ final downloadState = final checkSumState = StateProvider.autoDispose( (ref) => CheckSumProcessState.waiting); -final dbProvider = Provider((ref) => throw UnimplementedError()); - final myLibraryProvider = FutureProvider((ref) async { - return await ref.read(dbProvider).getAll(); + return dataBase.getAll(); }); final checkIdExists = FutureProvider.family.autoDispose((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((ref) => 0); Future 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 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((ref, fileName) async { - return await ref.read(dbProvider).getBookState(fileName); + return await dataBase.getBookState(fileName); }); final openPdfWithExternalAppProvider = StateProvider((ref) => false); diff --git a/lib/ui/book_info_page.dart b/lib/ui/book_info_page.dart index f2110f6..3ace9d8 100644 --- a/lib/ui/book_info_page.dart +++ b/lib/ui/book_info_page.dart @@ -34,7 +34,6 @@ import 'package:openlib/state/state.dart' CheckSumProcessState, downloadState, checkSumState, - dbProvider, checkIdExists, myLibraryProvider; @@ -246,7 +245,9 @@ Future 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, diff --git a/lib/ui/epub_viewer.dart b/lib/ui/epub_viewer.dart index 28f11e6..7e2b92d 100644 --- a/lib/ui/epub_viewer.dart +++ b/lib/ui/epub_viewer.dart @@ -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 launchEpubViewer( {required String fileName, @@ -24,7 +24,9 @@ Future 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) { diff --git a/lib/ui/mybook_page.dart b/lib/ui/mybook_page.dart index 4d6525c..8ff236a 100644 --- a/lib/ui/mybook_page.dart +++ b/lib/ui/mybook_page.dart @@ -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, diff --git a/lib/ui/settings_page.dart b/lib/ui/settings_page.dart index 6d7b6d2..6e758bb 100644 --- a/lib/ui/settings_page.dart +++ b/lib/ui/settings_page.dart @@ -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 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, diff --git a/lib/ui/webview_page.dart b/lib/ui/webview_page.dart index 3c793d7..757f4d8 100644 --- a/lib/ui/webview_page.dart +++ b/lib/ui/webview_page.dart @@ -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 { 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 { 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 { ref.read(cookieProvider.notifier).state = cfClearance; - await ref - .read(dbProvider) - .setBrowserOptions('cookie', cfClearance); + await dataBase.setBrowserOptions('cookie', cfClearance); ref.invalidate(bookInfoProvider); diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index ac03459..6343396 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -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")) diff --git a/pubspec.lock b/pubspec.lock index 2cf1b5e..6844c32 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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: diff --git a/pubspec.yaml b/pubspec.yaml index 48c4448..dd60bfe 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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