From b89c7bc60866f9aa51189778909125a0976ac094 Mon Sep 17 00:00:00 2001 From: Levi Date: Wed, 24 Jan 2024 15:49:51 +0800 Subject: [PATCH] feat: migrate shared_preferences to hive (#343) --- ui/flutter/.gitignore | 6 +- ui/flutter/lib/app/data/api_provider.dart | 8 -- .../app/controllers/app_controller.dart | 34 +++--- .../app/modules/create/views/create_view.dart | 22 ++-- .../controller/history_controller.dart | 26 ----- .../modules/history/views/history_view.dart | 5 +- ui/flutter/lib/database/database.dart | 103 ++++++++++++++++++ ui/flutter/lib/database/entity.dart | 37 +++++++ ui/flutter/lib/database/entity.g.dart | 43 ++++++++ ui/flutter/lib/main.dart | 11 +- ui/flutter/lib/util/mac_secure_util.dart | 16 +-- ui/flutter/lib/util/util.dart | 28 ++--- ui/flutter/lib/util/window_util.dart | 39 ------- ui/flutter/pubspec.lock | 10 +- ui/flutter/pubspec.yaml | 2 +- 15 files changed, 247 insertions(+), 143 deletions(-) delete mode 100644 ui/flutter/lib/app/data/api_provider.dart delete mode 100644 ui/flutter/lib/app/modules/history/controller/history_controller.dart create mode 100644 ui/flutter/lib/database/database.dart create mode 100644 ui/flutter/lib/database/entity.dart create mode 100644 ui/flutter/lib/database/entity.g.dart delete mode 100644 ui/flutter/lib/util/window_util.dart diff --git a/ui/flutter/.gitignore b/ui/flutter/.gitignore index b1f0661d..33b0d670 100644 --- a/ui/flutter/.gitignore +++ b/ui/flutter/.gitignore @@ -63,4 +63,8 @@ windows/flutter/generated_plugin_registrant.cc windows/flutter/generated_plugin_registrant.h windows/flutter/generated_plugins.cmake -/extensions \ No newline at end of file +/extensions + +# Hive database files +database.hive +database.lock \ No newline at end of file diff --git a/ui/flutter/lib/app/data/api_provider.dart b/ui/flutter/lib/app/data/api_provider.dart deleted file mode 100644 index d3683762..00000000 --- a/ui/flutter/lib/app/data/api_provider.dart +++ /dev/null @@ -1,8 +0,0 @@ -import 'package:get/get.dart'; - -class ApiProvider extends GetConnect { - @override - void onInit() { - httpClient.baseUrl = 'YOUR-API-URL'; - } -} diff --git a/ui/flutter/lib/app/modules/app/controllers/app_controller.dart b/ui/flutter/lib/app/modules/app/controllers/app_controller.dart index 25e183c5..89bb0da7 100644 --- a/ui/flutter/lib/app/modules/app/controllers/app_controller.dart +++ b/ui/flutter/lib/app/modules/app/controllers/app_controller.dart @@ -6,9 +6,9 @@ import 'package:app_links/app_links.dart'; import 'package:flutter/material.dart'; import 'package:flutter_foreground_task/flutter_foreground_task.dart'; import 'package:get/get.dart'; +import 'package:gopeed/database/entity.dart'; import 'package:lecle_downloads_path_provider/lecle_downloads_path_provider.dart'; import 'package:path_provider/path_provider.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:tray_manager/tray_manager.dart'; import 'package:uri_to_file/uri_to_file.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -17,18 +17,14 @@ import 'package:window_manager/window_manager.dart'; import '../../../../api/api.dart'; import '../../../../api/model/downloader_config.dart'; import '../../../../core/common/start_config.dart'; +import '../../../../database/database.dart'; import '../../../../i18n/message.dart'; import '../../../../util/locale_manager.dart'; import '../../../../util/log_util.dart'; import '../../../../util/package_info.dart'; import '../../../../util/util.dart'; -import '../../../../util/window_util.dart' as windows_util; import '../../../routes/app_pages.dart'; -const _startConfigNetwork = "start.network"; -const _startConfigAddress = "start.address"; -const _startConfigApiToken = "start.apiToken"; - const unixSocketPath = 'gopeed.sock'; const allTrackerSubscribeUrls = [ @@ -127,17 +123,18 @@ class AppController extends GetxController with WindowListener, TrayListener { @override void onWindowMaximize() { - windows_util.saveState(isMaximized: true); + Database.instance.saveWindowState(WindowStateEntity(isMaximized: true)); } @override void onWindowUnmaximize() { - windows_util.saveState(isMaximized: false); + Database.instance.saveWindowState(WindowStateEntity(isMaximized: false)); } final _windowsResizeSave = Util.debounce(() async { final size = await windowManager.getSize(); - windows_util.saveState(width: size.width, height: size.height); + Database.instance.saveWindowState( + WindowStateEntity(width: size.width, height: size.height)); }, 500); @override @@ -307,13 +304,10 @@ class AppController extends GetxController with WindowListener, TrayListener { Future loadStartConfig() async { final defaultCfg = await _initDefaultStartConfig(); - final prefs = await SharedPreferences.getInstance(); - startConfig.value.network = - prefs.getString(_startConfigNetwork) ?? defaultCfg.network; - startConfig.value.address = - prefs.getString(_startConfigAddress) ?? defaultCfg.address; - startConfig.value.apiToken = - prefs.getString(_startConfigApiToken) ?? defaultCfg.apiToken; + final saveCfg = Database.instance.getStartConfig(); + startConfig.value.network = saveCfg?.network ?? defaultCfg.network; + startConfig.value.address = saveCfg?.address ?? defaultCfg.address; + startConfig.value.apiToken = saveCfg?.apiToken ?? defaultCfg.apiToken; } Future loadDownloaderConfig() async { @@ -433,10 +427,10 @@ class AppController extends GetxController with WindowListener, TrayListener { } Future saveConfig() async { - final prefs = await SharedPreferences.getInstance(); - await prefs.setString(_startConfigNetwork, startConfig.value.network); - await prefs.setString(_startConfigAddress, startConfig.value.address); - await prefs.setString(_startConfigApiToken, startConfig.value.apiToken); + Database.instance.saveStartConfig(StartConfigEntity( + network: startConfig.value.network, + address: startConfig.value.address, + apiToken: startConfig.value.apiToken)); await putConfig(downloaderConfig.value); } } diff --git a/ui/flutter/lib/app/modules/create/views/create_view.dart b/ui/flutter/lib/app/modules/create/views/create_view.dart index 9808997e..1c895677 100644 --- a/ui/flutter/lib/app/modules/create/views/create_view.dart +++ b/ui/flutter/lib/app/modules/create/views/create_view.dart @@ -12,6 +12,7 @@ import '../../../../api/model/create_task.dart'; import '../../../../api/model/options.dart'; import '../../../../api/model/request.dart'; import '../../../../api/model/resolve_result.dart'; +import '../../../../database/database.dart'; import '../../../../util/input_formatter.dart'; import '../../../../util/message.dart'; import '../../../../util/util.dart'; @@ -19,7 +20,6 @@ import '../../../routes/app_pages.dart'; import '../../../views/directory_selector.dart'; import '../../../views/file_list_view.dart'; import '../../app/controllers/app_controller.dart'; -import '../../history/controller/history_controller.dart'; import '../../history/views/history_view.dart'; import '../controllers/create_controller.dart'; @@ -35,7 +35,6 @@ class CreateView extends GetView { final _httpCookieController = TextEditingController(); final _httpRefererController = TextEditingController(); final _btTrackerController = TextEditingController(); - final _historyController = HistoryController(); final _availableSchemes = ["http:", "https:", "magnet:"]; @@ -168,10 +167,7 @@ class CreateView extends GetView { icon: const Icon(Icons.history_rounded), onPressed: () async { List resultOfHistories = - await _historyController.getAllHistory(); - // reversing: display last entered history first - List reverseResultOfHistories = - resultOfHistories.reversed.toList(); + Database.instance.getCreateHistory() ?? []; // show dialog box to list history if (context.mounted) { showGeneralDialog( @@ -183,16 +179,14 @@ class CreateView extends GetView { opacity: a1.value, child: HistoryView( isHistoryListEmpty: - reverseResultOfHistories.isEmpty, + resultOfHistories.isEmpty, historyList: ListView.builder( - itemCount: - reverseResultOfHistories.length, + itemCount: resultOfHistories.length, itemBuilder: (context, index) { return GestureDetector( onTap: () { _urlController.text = - reverseResultOfHistories[ - index]; + resultOfHistories[index]; Navigator.pop(context); }, child: MouseRegion( @@ -217,9 +211,7 @@ class CreateView extends GetView { 10.0), ), child: Text( - reverseResultOfHistories[ - index] - .toString(), + resultOfHistories[index], ), ), ), @@ -419,7 +411,7 @@ class CreateView extends GetView { // Add url to the history if (!isWebFileChosen) { for (final url in urls) { - _historyController.addHistory(url); + Database.instance.saveCreateHistory(url); } } diff --git a/ui/flutter/lib/app/modules/history/controller/history_controller.dart b/ui/flutter/lib/app/modules/history/controller/history_controller.dart deleted file mode 100644 index d7642006..00000000 --- a/ui/flutter/lib/app/modules/history/controller/history_controller.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:shared_preferences/shared_preferences.dart'; - -class HistoryController { - // Get History in List - Future> getAllHistory() async { - SharedPreferences preferences = await SharedPreferences.getInstance(); - List? historyList = preferences.getStringList('history'); - if(historyList == null) { - return []; - } - return historyList; - } - - // Get Existing History & Add New History - void addHistory(String historyText) async { - SharedPreferences preferences = await SharedPreferences.getInstance(); - List existingHistoryData = await getAllHistory(); - existingHistoryData.add(historyText); - preferences.setStringList('history', existingHistoryData); - } - - void clearHistory() async { - SharedPreferences preferences = await SharedPreferences.getInstance(); - preferences.remove('history'); - } -} diff --git a/ui/flutter/lib/app/modules/history/views/history_view.dart b/ui/flutter/lib/app/modules/history/views/history_view.dart index 08053658..e5e23bcd 100644 --- a/ui/flutter/lib/app/modules/history/views/history_view.dart +++ b/ui/flutter/lib/app/modules/history/views/history_view.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:gopeed/app/modules/history/controller/history_controller.dart'; +import 'package:gopeed/database/database.dart'; class HistoryView extends StatefulWidget { const HistoryView({ @@ -62,8 +62,7 @@ class _HistoryViewState extends State { children: [ IconButton( onPressed: () { - HistoryController _controller = HistoryController(); - _controller.clearHistory(); + Database.instance.clearCreateHistory(); Navigator.pop(context); }, tooltip: "clearHistory".tr, diff --git a/ui/flutter/lib/database/database.dart b/ui/flutter/lib/database/database.dart new file mode 100644 index 00000000..0125ec33 --- /dev/null +++ b/ui/flutter/lib/database/database.dart @@ -0,0 +1,103 @@ +import 'dart:convert'; + +import 'package:gopeed/util/util.dart'; +import 'package:hive/hive.dart'; + +import 'entity.dart'; + +const String _startConfig = 'startConfig'; +const String _windowState = 'windowState'; +const String _bookmark = 'bookmark'; +const String _createHistory = 'createHistory'; + +class Database { + static final Database _instance = Database._internal(); + + static Database get instance => _instance; + + factory Database() { + return _instance; + } + + late Box box; + + Database._internal(); + + Future init() async { + Hive.init(Util.getStorageDir()); + box = await Hive.openBox('database'); + } + + void save(String key, T entity) { + box.put(key, jsonEncode(entity)); + } + + T? get(String key, T Function(dynamic json) fromJsonT) { + final json = box.get(key); + if (json == null) { + return null; + } + return fromJsonT(jsonDecode(json)); + } + + void clear(String key) { + box.delete(key); + } + + void saveStartConfig(StartConfigEntity entity) { + save(_startConfig, entity); + } + + StartConfigEntity? getStartConfig() { + return get( + _startConfig, (json) => StartConfigEntity.fromJson(json)); + } + + /// Patch non-null fields with the original value + void saveWindowState(WindowStateEntity entity) { + final state = getWindowState(); + entity.isMaximized ??= state?.isMaximized; + entity.width ??= state?.width; + entity.height ??= state?.height; + save(_windowState, entity); + } + + WindowStateEntity? getWindowState() { + return get( + _windowState, (json) => WindowStateEntity.fromJson(json)); + } + + /// Use map to ensure that the same directory only saves the latest bookmark + void saveBookmark(MapEntry entry) { + final map = getBookmark() ?? {}; + map[entry.key] = entry.value; + save>(_bookmark, map); + } + + Map? getBookmark() { + return get>(_bookmark, (json) { + return (json as Map) + .map((key, value) => MapEntry(key, value.toString())); + }); + } + + void saveCreateHistory(String url) { + var list = getCreateHistory() ?? []; + list.remove(url); + list.insert(0, url); + if (list.length > 64) { + list.removeLast(); + } + save>(_createHistory, list); + } + + List? getCreateHistory() { + return get>(_createHistory, (json) { + return (json as List).map((e) => e.toString()).toList(); + }); + } + + void clearCreateHistory() { + clear(_createHistory); + } +} diff --git a/ui/flutter/lib/database/entity.dart b/ui/flutter/lib/database/entity.dart new file mode 100644 index 00000000..6cb5273e --- /dev/null +++ b/ui/flutter/lib/database/entity.dart @@ -0,0 +1,37 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'entity.g.dart'; + +@JsonSerializable() +class StartConfigEntity { + String network; + String address; + String apiToken; + + StartConfigEntity({ + required this.network, + required this.address, + required this.apiToken, + }); + + factory StartConfigEntity.fromJson(Map json) => + _$StartConfigEntityFromJson(json); + Map toJson() => _$StartConfigEntityToJson(this); +} + +@JsonSerializable() +class WindowStateEntity { + bool? isMaximized; + double? width; + double? height; + + WindowStateEntity({ + this.isMaximized, + this.width, + this.height, + }); + + factory WindowStateEntity.fromJson(Map json) => + _$WindowStateEntityFromJson(json); + Map toJson() => _$WindowStateEntityToJson(this); +} diff --git a/ui/flutter/lib/database/entity.g.dart b/ui/flutter/lib/database/entity.g.dart new file mode 100644 index 00000000..e13f12ab --- /dev/null +++ b/ui/flutter/lib/database/entity.g.dart @@ -0,0 +1,43 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'entity.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +StartConfigEntity _$StartConfigEntityFromJson(Map json) => + StartConfigEntity( + network: json['network'] as String, + address: json['address'] as String, + apiToken: json['apiToken'] as String, + ); + +Map _$StartConfigEntityToJson(StartConfigEntity instance) => + { + 'network': instance.network, + 'address': instance.address, + 'apiToken': instance.apiToken, + }; + +WindowStateEntity _$WindowStateEntityFromJson(Map json) => + WindowStateEntity( + isMaximized: json['isMaximized'] as bool?, + width: (json['width'] as num?)?.toDouble(), + height: (json['height'] as num?)?.toDouble(), + ); + +Map _$WindowStateEntityToJson(WindowStateEntity instance) { + final val = {}; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('isMaximized', instance.isMaximized); + writeNotNull('width', instance.width); + writeNotNull('height', instance.height); + return val; +} diff --git a/ui/flutter/lib/main.dart b/ui/flutter/lib/main.dart index f3ce15b5..95aff7f5 100644 --- a/ui/flutter/lib/main.dart +++ b/ui/flutter/lib/main.dart @@ -7,13 +7,13 @@ import 'api/api.dart' as api; import 'app/modules/app/controllers/app_controller.dart'; import 'app/modules/app/views/app_view.dart'; import 'core/libgopeed_boot.dart'; +import 'database/database.dart'; import 'i18n/message.dart'; import 'util/locale_manager.dart'; import 'util/log_util.dart'; import 'util/mac_secure_util.dart'; import 'util/package_info.dart'; import 'util/util.dart'; -import 'util/window_util.dart' as windows_util; void main() async { await init(); @@ -24,11 +24,13 @@ void main() async { Future init() async { WidgetsFlutterBinding.ensureInitialized(); + await Util.initStorageDir(); + await Database.instance.init(); if (Util.isDesktop()) { await windowManager.ensureInitialized(); - final windowState = await windows_util.loadState(); + final windowState = Database.instance.getWindowState(); final windowOptions = WindowOptions( - size: Size(windowState.width, windowState.height), + size: Size(windowState?.width ?? 800, windowState?.height ?? 600), center: true, skipTaskbar: false, ); @@ -44,9 +46,6 @@ Future init() async { }); } - if (!Util.isWeb()) { - await Util.initStorageDir(); - } initLogger(); final controller = Get.put(AppController()); diff --git a/ui/flutter/lib/util/mac_secure_util.dart b/ui/flutter/lib/util/mac_secure_util.dart index 69f6bdd4..19905833 100644 --- a/ui/flutter/lib/util/mac_secure_util.dart +++ b/ui/flutter/lib/util/mac_secure_util.dart @@ -1,33 +1,29 @@ import 'dart:io'; import 'package:macos_secure_bookmarks/macos_secure_bookmarks.dart'; -import 'package:shared_preferences/shared_preferences.dart'; +import '../database/database.dart'; import 'util.dart'; class MacSecureUtil { - static const _bookmarkKey = "bookmark:"; - static Future saveBookmark(String dir) async { if (!Util.isMacos()) { return; } final secureBookmarks = SecureBookmarks(); final bookmark = await secureBookmarks.bookmark(Directory(dir)); - final prefs = await SharedPreferences.getInstance(); - await prefs.setString(_bookmarkKey + dir, bookmark); + Database.instance.saveBookmark(MapEntry(dir, bookmark)); } static Future loadBookmark() async { if (!Util.isMacos()) { return; } - final prefs = await SharedPreferences.getInstance(); - final keys = prefs.getKeys(); final secureBookmarks = SecureBookmarks(); - keys.where((k) => k.startsWith(_bookmarkKey)).forEach((k) async { - final resolvedFile = await secureBookmarks - .resolveBookmark(prefs.getString(k)!, isDirectory: true); + final bookmark = Database.instance.getBookmark() ?? {}; + bookmark.forEach((_, v) async { + final resolvedFile = + await secureBookmarks.resolveBookmark(v, isDirectory: true); await secureBookmarks.startAccessingSecurityScopedResource(resolvedFile); }); } diff --git a/ui/flutter/lib/util/util.dart b/ui/flutter/lib/util/util.dart index 821f4fa4..36b5bacb 100644 --- a/ui/flutter/lib/util/util.dart +++ b/ui/flutter/lib/util/util.dart @@ -50,19 +50,21 @@ class Util { } static Future initStorageDir() async { - var storageDir = "./"; - if (Platform.isAndroid) { - storageDir = (await getExternalStorageDirectory())?.path ?? storageDir; - } else if (Platform.isIOS) { - storageDir = (await getLibraryDirectory()).path; - } else if (Util.isLinux()) { - storageDir = File(Platform.resolvedExecutable).parent.path; - // check has write permission, if not, fallback to application support dir - try { - final testFile = File(path.join(storageDir, ".test")); - await testFile.writeAsString("test"); - await testFile.delete(); - } catch (e) { + var storageDir = ""; + if (Util.isWindows()) { + storageDir = "./"; + } else if (!Util.isWeb()) { + if (Util.isLinux()) { + storageDir = File(Platform.resolvedExecutable).parent.path; + // check has write permission, if not, fallback to application support dir + try { + final testFile = File(path.join(storageDir, ".test")); + await testFile.writeAsString("test"); + await testFile.delete(); + } catch (e) { + storageDir = (await getApplicationSupportDirectory()).path; + } + } else { storageDir = (await getApplicationSupportDirectory()).path; } } diff --git a/ui/flutter/lib/util/window_util.dart b/ui/flutter/lib/util/window_util.dart deleted file mode 100644 index a37c8e7c..00000000 --- a/ui/flutter/lib/util/window_util.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'dart:async'; - -import 'package:shared_preferences/shared_preferences.dart'; - -class WindowState { - late bool isMaximized; - late double width; - late double height; - - WindowState(this.isMaximized, this.width, this.height); -} - -const String _windowStateIsMaximized = "window.isMaximized"; -const String _windowStateWidth = "window.width"; -const String _windowStateHeight = "window.height"; - -/// Save window state to storage. -void saveState({bool? isMaximized, double? width, double? height}) async { - final prefs = await SharedPreferences.getInstance(); - if (isMaximized != null) { - await prefs.setBool(_windowStateIsMaximized, isMaximized); - } - if (width != null) { - await prefs.setDouble(_windowStateWidth, width); - } - if (height != null) { - await prefs.setDouble(_windowStateHeight, height); - } -} - -/// Load window state from storage. -Future loadState() async { - final prefs = await SharedPreferences.getInstance(); - return WindowState( - prefs.getBool(_windowStateIsMaximized) ?? false, - prefs.getDouble(_windowStateWidth) ?? 800, - prefs.getDouble(_windowStateHeight) ?? 600, - ); -} diff --git a/ui/flutter/pubspec.lock b/ui/flutter/pubspec.lock index 4ed9c003..18445458 100644 --- a/ui/flutter/pubspec.lock +++ b/ui/flutter/pubspec.lock @@ -405,6 +405,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.3.1" + hive: + dependency: "direct main" + description: + name: hive + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.3" http: dependency: transitive description: @@ -798,7 +806,7 @@ packages: source: hosted version: "3.3.1" shared_preferences: - dependency: "direct main" + dependency: transitive description: name: shared_preferences sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02" diff --git a/ui/flutter/pubspec.yaml b/ui/flutter/pubspec.yaml index b3072a28..11141be6 100644 --- a/ui/flutter/pubspec.yaml +++ b/ui/flutter/pubspec.yaml @@ -46,7 +46,6 @@ dependencies: macos_secure_bookmarks: ^0.2.1 rounded_loading_button: ^2.1.0 url_launcher: ^6.1.6 - shared_preferences: ^2.2.0 logger: ^1.3.0 desktop_drop: ^0.4.3 package_info_plus: ^4.0.0 @@ -64,6 +63,7 @@ dependencies: open_filex: ^4.3.4 tray_manager: ^0.2.1 lecle_downloads_path_provider: ^0.0.2+8 + hive: ^2.2.3 dev_dependencies: flutter_test: sdk: flutter