From 56afc6a0d65173642fe40f5f793e3b7225aa51f8 Mon Sep 17 00:00:00 2001 From: iphreetom Date: Sun, 10 May 2026 16:21:07 +0800 Subject: [PATCH 1/3] Add local search tab --- lib/src/config/theme_config.dart | 2 +- lib/src/l18n/en_US.dart | 5 + lib/src/l18n/ko_KR.dart | 3 + lib/src/l18n/pt_BR.dart | 3 + lib/src/l18n/ru_RU.dart | 3 + lib/src/l18n/zh_CN.dart | 5 + lib/src/l18n/zh_TW.dart | 3 + lib/src/model/tab_bar_icon.dart | 1 + .../desktop/desktop_layout_page_state.dart | 9 + .../mobile_layout_page_v2_state.dart | 9 + .../local_search/local_search_logic.dart | 189 ++++++++++++++++++ .../pages/local_search/local_search_page.dart | 61 ++++++ .../local_search/local_search_state.dart | 9 + lib/src/routes/routes.dart | 8 + macos/Podfile.lock | 42 ++-- macos/Runner.xcodeproj/project.pbxproj | 6 +- .../xcshareddata/xcschemes/Runner.xcscheme | 1 + macos/Runner/AppDelegate.swift | 4 + .../system_network_proxy_macos/CHANGELOG.md | 13 ++ plugins/system_network_proxy_macos/LICENSE | 21 ++ plugins/system_network_proxy_macos/README.md | 15 ++ .../lib/system_network_proxy_macos.dart | 118 +++++++++++ .../SystemNetworkProxyMacosPlugin.swift | 19 ++ .../macos/system_network_proxy_macos.podspec | 22 ++ .../system_network_proxy_macos/pubspec.yaml | 61 ++++++ pubspec.lock | 112 +++++------ pubspec.yaml | 12 +- 27 files changed, 663 insertions(+), 93 deletions(-) create mode 100644 lib/src/pages/local_search/local_search_logic.dart create mode 100644 lib/src/pages/local_search/local_search_page.dart create mode 100644 lib/src/pages/local_search/local_search_state.dart create mode 100644 plugins/system_network_proxy_macos/CHANGELOG.md create mode 100644 plugins/system_network_proxy_macos/LICENSE create mode 100644 plugins/system_network_proxy_macos/README.md create mode 100644 plugins/system_network_proxy_macos/lib/system_network_proxy_macos.dart create mode 100644 plugins/system_network_proxy_macos/macos/Classes/SystemNetworkProxyMacosPlugin.swift create mode 100644 plugins/system_network_proxy_macos/macos/system_network_proxy_macos.podspec create mode 100644 plugins/system_network_proxy_macos/pubspec.yaml diff --git a/lib/src/config/theme_config.dart b/lib/src/config/theme_config.dart index 4879ae967..7d5eaee24 100644 --- a/lib/src/config/theme_config.dart +++ b/lib/src/config/theme_config.dart @@ -20,7 +20,7 @@ class ThemeConfig { return themeData.copyWith( appBarTheme: themeData.appBarTheme.copyWith(backgroundColor: themeData.colorScheme.surface), - dialogTheme: DialogTheme(backgroundColor: themeData.colorScheme.surface), + dialogTheme: DialogThemeData(backgroundColor: themeData.colorScheme.surface), ); } } diff --git a/lib/src/l18n/en_US.dart b/lib/src/l18n/en_US.dart index 88ad96f47..8d225b3fb 100644 --- a/lib/src/l18n/en_US.dart +++ b/lib/src/l18n/en_US.dart @@ -32,8 +32,11 @@ class en_US { 'receiveDataTimeoutHint': 'Network receive data timeout', 'archiveError': 'Download Archive Error', 'edit': 'Edit', + 'opened': 'Opened', 'home': "Home", + 'localSearch': "Local Search", + 'inputKeyword2Search': "Input keyword to search", 'gallery': "Gallery", 'setting': 'Setting', @@ -459,6 +462,7 @@ class en_US { 'keepScreenAwakeWhenReading': 'Keep Screen Awake When Reading', 'enableCustomReadBrightness': 'Enable Custom Brightness When Reading', 'spaceBetweenImages': 'Space Between Images', + 'resetReadProgress': 'Reset Read Progress', 'enableImmersiveHint': 'Hide System Bar', 'enableImmersiveHint4Windows': 'Hide Title Bar', 'deviceOrientation': 'Device Orientation', @@ -537,6 +541,7 @@ class en_US { /// download page 'local': 'Local', + 'downloadCategory': 'Download Category', 'reDownload': 'Re-Download', 'delete': 'Delete', 'deleteTask': 'Delete Task Only', diff --git a/lib/src/l18n/ko_KR.dart b/lib/src/l18n/ko_KR.dart index 2c352b9b2..fdc1a3b90 100644 --- a/lib/src/l18n/ko_KR.dart +++ b/lib/src/l18n/ko_KR.dart @@ -32,6 +32,7 @@ class ko_KR { 'receiveDataTimeoutHint': 'Network receive data timeout', 'archiveError': 'Download Archive Error', 'edit': 'Edit', + 'opened': 'Opened', 'home': "홈", 'gallery': "갤러리", @@ -459,6 +460,7 @@ class ko_KR { 'keepScreenAwakeWhenReading': '읽는 중에 화면 꺼짐 방지하기', 'enableCustomReadBrightness': 'Enable Custom Brightness When Reading', 'spaceBetweenImages': '이미지 간격', + 'resetReadProgress': 'Reset Read Progress', 'enableImmersiveHint': '시스템 바를 숨김', 'enableImmersiveHint4Windows': 'Hide Title Bar', 'deviceOrientation': 'Device Orientation', @@ -537,6 +539,7 @@ class ko_KR { /// download page 'local': '로컬', + 'downloadCategory': 'Download Category', 'reDownload': '다시 다운로드', 'delete': '삭제', 'deleteTask': '리스트에서만 삭제', diff --git a/lib/src/l18n/pt_BR.dart b/lib/src/l18n/pt_BR.dart index 262bb3ac6..94f37d35f 100644 --- a/lib/src/l18n/pt_BR.dart +++ b/lib/src/l18n/pt_BR.dart @@ -32,6 +32,7 @@ class pt_BR { 'receiveDataTimeoutHint': 'Network receive data timeout', 'archiveError': 'Download Archive Error', 'edit': 'Edit', + 'opened': 'Opened', 'home': "Home", 'gallery': "Galeria", @@ -460,6 +461,7 @@ class pt_BR { 'keepScreenAwakeWhenReading': 'Keep Screen Awake When Reading', 'enableCustomReadBrightness': 'Enable Custom Brightness When Reading', 'spaceBetweenImages': 'Space Between Images', + 'resetReadProgress': 'Reset Read Progress', 'enableImmersiveHint': 'Esconder barra do sistema', 'enableImmersiveHint4Windows': 'Hide Title Bar', 'deviceOrientation': 'Device Orientation', @@ -538,6 +540,7 @@ class pt_BR { /// download page 'local': 'Local', + 'downloadCategory': 'Download Category', 'reDownload': 'Baixar novamente', 'delete': 'Apagar', 'deleteTask': 'Apagar apenas tarefa', diff --git a/lib/src/l18n/ru_RU.dart b/lib/src/l18n/ru_RU.dart index 09a6ef077..977a9a07a 100644 --- a/lib/src/l18n/ru_RU.dart +++ b/lib/src/l18n/ru_RU.dart @@ -32,6 +32,7 @@ class ru_RU { 'receiveDataTimeoutHint': 'Тайм-аут получения данных', 'archiveError': 'Ошибка загрузки архива', 'edit': 'Редактировать', + 'opened': 'Opened', 'home': "Главная", 'gallery': "Галерея", @@ -466,6 +467,7 @@ class ru_RU { 'keepScreenAwakeWhenReading': 'Не выключать экран при чтении', 'enableCustomReadBrightness': 'Включить пользовательскую яркость при чтении', 'spaceBetweenImages': 'Пространство между изображениями', + 'resetReadProgress': 'Reset Read Progress', 'enableImmersiveHint': 'Скрыть системную панель', 'enableImmersiveHint4Windows': 'Скрыть заголовок окна', 'deviceOrientation': 'Ориентация устройства', @@ -544,6 +546,7 @@ class ru_RU { /// download page 'local': 'Локально', + 'downloadCategory': 'Download Category', 'reDownload': 'Перекачать', 'delete': 'Удалить', 'deleteTask': 'Удалить только задачу', diff --git a/lib/src/l18n/zh_CN.dart b/lib/src/l18n/zh_CN.dart index aff6c8b4b..35b02e048 100644 --- a/lib/src/l18n/zh_CN.dart +++ b/lib/src/l18n/zh_CN.dart @@ -32,8 +32,11 @@ class zh_CN { 'receiveDataTimeoutHint': '网络接收数据超时', 'archiveError': '下载归档错误', 'edit': '编辑', + 'opened': '已打开', 'home': "主页", + 'localSearch': "本地搜索", + 'inputKeyword2Search': "输入关键字以搜索", 'gallery': "画廊", 'setting': '设置', @@ -459,6 +462,7 @@ class zh_CN { 'keepScreenAwakeWhenReading': '阅读时屏幕不自动锁定', 'enableCustomReadBrightness': '阅读页自定义亮度', 'spaceBetweenImages': '图片间隔', + 'resetReadProgress': '重置阅读进度', 'enableImmersiveHint': '隐藏系统状态栏和底部导航栏', 'enableImmersiveHint4Windows': '隐藏顶部标题栏', 'deviceOrientation': '屏幕方向', @@ -537,6 +541,7 @@ class zh_CN { /// download page 'local': '本地', + 'downloadCategory': '下载分类', 'delete': '删除', 'deleteTask': '仅删除任务', 'deleteTaskAndImages': '删除任务和图片', diff --git a/lib/src/l18n/zh_TW.dart b/lib/src/l18n/zh_TW.dart index dd0177608..33e4611e8 100644 --- a/lib/src/l18n/zh_TW.dart +++ b/lib/src/l18n/zh_TW.dart @@ -32,6 +32,7 @@ class zh_TW { 'receiveDataTimeoutHint': '接收網路資料超時', 'archiveError': '下載歸檔錯誤', 'edit': '編輯', + 'opened': '已開啟', 'home': "首頁", 'gallery': "畫廊", @@ -459,6 +460,7 @@ class zh_TW { 'keepScreenAwakeWhenReading': '閱讀時螢幕不自動鎖定', 'enableCustomReadBrightness': '閱讀頁自訂亮度', 'spaceBetweenImages': '圖片間隔', + 'resetReadProgress': '重設閱讀進度', 'enableImmersiveHint': '隱藏系統狀態欄和底部導航欄', 'enableImmersiveHint4Windows': '隱藏頂部標題欄', 'deviceOrientation': '螢幕方向', @@ -537,6 +539,7 @@ class zh_TW { /// download page 'local': '本機', + 'downloadCategory': '下載分類', 'delete': '刪除', 'deleteTask': '僅刪除任務', 'deleteTaskAndImages': '刪除任務和圖片', diff --git a/lib/src/model/tab_bar_icon.dart b/lib/src/model/tab_bar_icon.dart index ac341d1ef..2d0cc6037 100644 --- a/lib/src/model/tab_bar_icon.dart +++ b/lib/src/model/tab_bar_icon.dart @@ -9,6 +9,7 @@ enum TabBarIconNameEnum { favorite, history, download, + localSearch, setting, } diff --git a/lib/src/pages/layout/desktop/desktop_layout_page_state.dart b/lib/src/pages/layout/desktop/desktop_layout_page_state.dart index 7fd5a19b6..5b0fa6764 100644 --- a/lib/src/pages/layout/desktop/desktop_layout_page_state.dart +++ b/lib/src/pages/layout/desktop/desktop_layout_page_state.dart @@ -3,6 +3,7 @@ import 'package:get/get.dart'; import 'package:jhentai/src/extension/list_extension.dart'; import 'package:jhentai/src/pages/download/download_base_page.dart'; import 'package:jhentai/src/pages/gallerys/simple/gallerys_page_logic.dart'; +import 'package:jhentai/src/pages/local_search/local_search_page.dart'; import 'package:jhentai/src/pages/history/history_page.dart'; import 'package:jhentai/src/pages/gallerys/simple/gallerys_page.dart'; import 'package:jhentai/src/pages/popular/popular_page.dart'; @@ -55,6 +56,14 @@ class DesktopLayoutPageState with DoubleTapToRefreshStateMixin { Get.find().state.tabLogics[Get.find().state.currentTabIndex].state.scrollController, shouldRender: true, ), + TabBarIcon( + name: TabBarIconNameEnum.localSearch, + routeName: Routes.localSearch, + selectedIcon: const Icon(Icons.manage_search, shadows: [Shadow(blurRadius: 2)]), + unselectedIcon: const Icon(Icons.manage_search), + page: () => LocalSearchPage(), + shouldRender: false, + ), TabBarIcon( name: TabBarIconNameEnum.popular, routeName: Routes.popular, diff --git a/lib/src/pages/layout/mobile_v2/mobile_layout_page_v2_state.dart b/lib/src/pages/layout/mobile_v2/mobile_layout_page_v2_state.dart index 3474ad8a4..b745693ac 100644 --- a/lib/src/pages/layout/mobile_v2/mobile_layout_page_v2_state.dart +++ b/lib/src/pages/layout/mobile_v2/mobile_layout_page_v2_state.dart @@ -3,6 +3,7 @@ import 'package:get/get.dart'; import 'package:jhentai/src/extension/list_extension.dart'; import 'package:jhentai/src/pages/gallerys/dashboard/dashboard_page_logic.dart'; import 'package:jhentai/src/pages/gallerys/dashboard/simple/simple_dashboard_page_logic.dart'; +import 'package:jhentai/src/pages/local_search/local_search_page.dart'; import 'package:jhentai/src/pages/search/mobile_v2/search_page_mobile_v2.dart'; import '../../../mixin/double_tap_to_refresh_state_mixin.dart'; @@ -59,6 +60,14 @@ class MobileLayoutPageV2State with DoubleTapToRefreshStateMixin { shouldRender: false, enterNewRoute: true, ), + TabBarIcon( + name: TabBarIconNameEnum.localSearch, + routeName: Routes.localSearch, + selectedIcon: const Icon(Icons.manage_search, shadows: [Shadow(blurRadius: 2)]), + unselectedIcon: const Icon(Icons.manage_search), + page: () => LocalSearchPage(), + shouldRender: false, + ), TabBarIcon( name: TabBarIconNameEnum.popular, routeName: Routes.popular, diff --git a/lib/src/pages/local_search/local_search_logic.dart b/lib/src/pages/local_search/local_search_logic.dart new file mode 100644 index 000000000..9bf17a5c8 --- /dev/null +++ b/lib/src/pages/local_search/local_search_logic.dart @@ -0,0 +1,189 @@ +import 'dart:collection'; + +import 'package:drift/drift.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:jhentai/src/database/database.dart'; +import 'package:jhentai/src/extension/get_logic_extension.dart'; +import 'package:jhentai/src/model/gallery.dart'; +import 'package:jhentai/src/model/gallery_count.dart'; +import 'package:jhentai/src/model/gallery_image.dart'; +import 'package:jhentai/src/model/gallery_page.dart'; +import 'package:jhentai/src/model/gallery_url.dart'; +import 'package:jhentai/src/pages/base/base_page_logic.dart'; +import 'package:jhentai/src/pages/local_search/local_search_state.dart'; +import 'package:jhentai/src/service/gallery_download_service.dart'; +import 'package:jhentai/src/service/log.dart'; +import 'package:jhentai/src/utils/uuid_util.dart'; +import 'package:jhentai/src/widget/loading_state_indicator.dart'; + +class LocalSearchLogic extends BasePageLogic { + @override + final LocalSearchState state = LocalSearchState(); + + int _searchTicket = 0; + late final TextEditingController textEditingController; + late final FocusNode searchFocusNode; + + @override + Future onInit() async { + textEditingController = TextEditingController(text: state.searchConfig.keyword ?? ''); + searchFocusNode = FocusNode(); + await super.onInit(); + } + + @override + void onClose() { + textEditingController.dispose(); + searchFocusNode.dispose(); + super.onClose(); + } + + @override + bool get useSearchConfig => false; + + @override + bool get autoLoadForFirstTime => false; + + Future handleSearch(String keyword) async { + keyword = keyword.trim(); + if (keyword.isEmpty) { + return; + } + state.searchConfig.keyword = keyword; + if (textEditingController.text != keyword) { + textEditingController.value = TextEditingValue( + text: keyword, + selection: TextSelection.collapsed(offset: keyword.length), + ); + } + await handleClearAndRefresh(); + } + + @override + Future handleClearAndRefresh() async { + String keyword = state.searchConfig.keyword?.trim() ?? ''; + if (keyword.isEmpty) { + return; + } + + int ticket = ++_searchTicket; + state.hasSearched = true; + state.loadingState = LoadingState.loading; + state.refreshState = LoadingState.idle; + state.gallerys.clear(); + state.prevGid = null; + state.nextGid = null; + state.seek = DateTime.now(); + state.totalCount = null; + state.favoriteSortOrder = null; + state.pageStorageKey = PageStorageKey('$runtimeType::$ticket::${newUUID()}'); + state.galleryCollectionKey = Key('$runtimeType::gallery::$ticket::${newUUID()}'); + + jump2Top(); + updateSafely(); + + GalleryPageInfo galleryPage; + try { + galleryPage = await _queryLocalGalleryPage(keyword); + } catch (e) { + if (ticket != _searchTicket) { + return; + } + log.error('Local search failed', e); + state.loadingState = LoadingState.error; + updateSafely(); + return; + } + + if (ticket != _searchTicket) { + return; + } + + List gallerys = await postHandleNewGallerys(galleryPage.gallerys); + if (ticket != _searchTicket) { + return; + } + + state.gallerys = gallerys; + state.totalCount = GalleryCount(count: gallerys.length.toString(), type: GalleryCountType.accurate); + state.prevGid = null; + state.nextGid = null; + state.favoriteSortOrder = galleryPage.favoriteSortOrder; + state.loadingState = gallerys.isEmpty ? LoadingState.noData : LoadingState.noMore; + + updateSafely(); + } + + @override + Future loadMore({bool checkLoadingState = true}) { + return handleClearAndRefresh(); + } + + @override + Future handlePullDown() { + return handleClearAndRefresh(); + } + + @override + Future getGalleryPage({String? prevGid, String? nextGid, DateTime? seek}) async { + return _queryLocalGalleryPage(state.searchConfig.keyword?.trim() ?? ''); + } + + Future _queryLocalGalleryPage(String keyword) async { + List galleries = await (appDb.select(appDb.galleryDownloaded) + ..where((t) => t.title.contains(keyword))) + .get(); + + List archives = await (appDb.select(appDb.archiveDownloaded) + ..where((t) => t.title.contains(keyword))) + .get(); + + List result = []; + final GalleryDownloadService galleryDownloadService = Get.find(); + + for (var g in galleries) { + GalleryImage? image = galleryDownloadService.galleryDownloadInfos[g.gid]?.images[0]; + + result.add(Gallery( + galleryUrl: GalleryUrl.parse(g.galleryUrl), + title: g.title, + category: g.category, + cover: image ?? GalleryImage(url: ''), + pageCount: g.pageCount, + rating: 0, + hasRated: false, + uploader: g.uploader, + publishTime: g.publishTime, + isExpunged: false, + tags: LinkedHashMap(), + )); + } + + for (var a in archives) { + if (result.any((e) => e.gid == a.gid)) { + continue; + } + result.add(Gallery( + galleryUrl: GalleryUrl.parse(a.galleryUrl), + title: a.title, + category: a.category, + cover: GalleryImage(url: a.coverUrl), + pageCount: a.pageCount, + rating: 0, + hasRated: false, + uploader: a.uploader, + publishTime: a.publishTime, + isExpunged: false, + tags: LinkedHashMap(), + )); + } + + return GalleryPageInfo( + gallerys: result, + totalCount: GalleryCount(count: result.length.toString(), type: GalleryCountType.accurate), + nextGid: null, + prevGid: null, + ); + } +} diff --git a/lib/src/pages/local_search/local_search_page.dart b/lib/src/pages/local_search/local_search_page.dart new file mode 100644 index 000000000..a17f4ce4a --- /dev/null +++ b/lib/src/pages/local_search/local_search_page.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:jhentai/src/pages/base/base_page.dart'; +import 'package:jhentai/src/pages/local_search/local_search_logic.dart'; +import 'package:jhentai/src/pages/local_search/local_search_state.dart'; + +class LocalSearchPage extends BasePage { + static const String tag = 'localSearch'; + + LocalSearchPage({Key? key}) : super(key: key, showJumpButton: false, showScroll2TopButton: true) { + logic = Get.isRegistered(tag: tag) + ? Get.find(tag: tag) + : Get.put(LocalSearchLogic(), tag: tag, permanent: true); + state = logic.state; + } + + @override + late final LocalSearchLogic logic; + + @override + late final LocalSearchState state; + + @override + Widget build(BuildContext context) { + return GetBuilder( + tag: tag, + builder: (_) => Scaffold( + appBar: buildAppBar(context), + body: SafeArea(child: buildBody(context)), + ), + ); + } + + @override + AppBar? buildAppBar(BuildContext context) { + return AppBar( + title: TextField( + controller: logic.textEditingController, + focusNode: logic.searchFocusNode, + decoration: InputDecoration( + hintText: 'localSearch'.tr, + border: InputBorder.none, + suffixIcon: IconButton( + icon: const Icon(Icons.search), + onPressed: () => logic.handleSearch(logic.textEditingController.text), + ), + ), + onSubmitted: logic.handleSearch, + onTapOutside: (_) => logic.searchFocusNode.unfocus(), + ), + ); + } + + @override + Widget buildBody(BuildContext context) { + if (!state.hasSearched) { + return Center(child: Text('inputKeyword2Search'.tr)); + } + return super.buildBody(context); + } +} diff --git a/lib/src/pages/local_search/local_search_state.dart b/lib/src/pages/local_search/local_search_state.dart new file mode 100644 index 000000000..09ace5136 --- /dev/null +++ b/lib/src/pages/local_search/local_search_state.dart @@ -0,0 +1,9 @@ +import 'package:jhentai/src/pages/base/base_page_state.dart'; +import 'package:jhentai/src/routes/routes.dart'; + +class LocalSearchState extends BasePageState { + bool hasSearched = false; + + @override + String get route => Routes.localSearch; +} diff --git a/lib/src/routes/routes.dart b/lib/src/routes/routes.dart index e5a04e6f8..26f232b6a 100644 --- a/lib/src/routes/routes.dart +++ b/lib/src/routes/routes.dart @@ -7,6 +7,7 @@ import 'package:jhentai/src/pages/download_search/download_search_page.dart'; import 'package:jhentai/src/pages/gallery_image/gallery_image_page.dart'; import 'package:jhentai/src/pages/gallerys/dashboard/dashboard_page.dart'; import 'package:jhentai/src/pages/history/history_page.dart'; +import 'package:jhentai/src/pages/local_search/local_search_page.dart'; import 'package:jhentai/src/pages/gallerys/simple/gallerys_page.dart'; import 'package:jhentai/src/pages/layout/desktop/desktop_home_page.dart'; import 'package:jhentai/src/pages/lock_page.dart'; @@ -79,6 +80,7 @@ class Routes { static const String setting = "/setting"; static const String desktopSearch = "/desktop_search"; static const String mobileV2Search = "/mobile_v2_search"; + static const String localSearch = "/local_search"; static const String downloadSearch = "/download_search"; /// right @@ -244,6 +246,12 @@ class Routes { transition: defaultTransition, side: Side.left, ), + EHPage( + name: localSearch, + page: () => LocalSearchPage(), + transition: defaultTransition, + side: Side.left, + ), EHPage( name: downloadSearch, page: () => DownloadSearchPage(), diff --git a/macos/Podfile.lock b/macos/Podfile.lock index efafc67a6..f7cc74767 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -133,28 +133,28 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos SPEC CHECKSUMS: - audio_session: dea1f41890dbf1718f04a56f1d6150fd50039b72 - battery_plus: 0ed4cf1e771b535a89ac7323e0855ec734cf745f - desktop_webview_window: d4365e71bcd4e1aa0c14cf0377aa24db0c16a7e2 - device_info_plus: 74e614483d05c89290d30a4c8feae15d555f7427 - FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - just_audio: 9b67ca7b97c61cfc9784ea23cd8cc55eb226d489 - local_auth_darwin: 66e40372f1c29f383a314c738c7446e2f7fdadc3 - package_info_plus: d2f71247aab4b6521434f887276093acc70d214c - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 - screen_brightness_macos: 2d6d3af2165592d9a55ffcd95b7550970e41ebda - screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 - share_plus: fd717ef89a2801d3491e737630112b80c310640e - smart_auth: b38e3ab4bfe089eacb1e233aca1a2340f96c28e9 - sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d + audio_session: 48ab6500f7a5e7c64363e206565a5dfe5a0c1441 + battery_plus: f51ad29136e025b714b96f7d096f44f604615da7 + desktop_webview_window: 7e37af677d6d19294cb433d9b1d878ef78dffa4d + device_info_plus: 724ebb83a99d517430554a1810445ff28fe71fa3 + FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 + just_audio: eb8b016ac4493159ab24db4f7215e55303b39a84 + local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391 + package_info_plus: 4f85435d47d3cdedb9b420d168afd080840708ea + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + screen_brightness_macos: 2a3ee243f8051c340381e8e51bcedced8360f421 + screen_retriever: 4f97c103641aab8ce183fa5af3b87029df167936 + share_plus: b818fd6bf0e537317735186d977a68a125703488 + smart_auth: e10f1d22af5d9e1a51964fb1826cb177f993a7e1 + sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983 - sqlite3_flutter_libs: 487032b9008b28de37c72a3aa66849ef3745f3e6 - url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 - video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3 - wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269 - webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4 - window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 + sqlite3_flutter_libs: f6acaa2172e6bb3e2e70c771661905080e8ebcf2 + url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 + video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b + wakelock_plus: 21ddc249ac4b8d018838dbdabd65c5976c308497 + webview_flutter_wkwebview: 44d4dee7d7056d5ad185d25b38404436d56c547c + window_manager: 1d01fa7ac65a6e6f83b965471b1a7fdd3f06166c PODFILE CHECKSUM: 0d3963a09fc94f580682bd88480486da345dc3f0 -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 1a7ad87c7..71b654d4d 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -405,7 +405,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -486,7 +486,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -533,7 +533,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 3253777c9..b44c2b2e0 100644 --- a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -48,6 +48,7 @@ ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" + enableGPUValidationMode = "1" allowLocationSimulation = "YES"> diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift index 8e02df288..b3c176141 100644 --- a/macos/Runner/AppDelegate.swift +++ b/macos/Runner/AppDelegate.swift @@ -6,4 +6,8 @@ class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } } diff --git a/plugins/system_network_proxy_macos/CHANGELOG.md b/plugins/system_network_proxy_macos/CHANGELOG.md new file mode 100644 index 000000000..84f5a2fb3 --- /dev/null +++ b/plugins/system_network_proxy_macos/CHANGELOG.md @@ -0,0 +1,13 @@ +## 1.0.0 + +- initial commit + +## 1.0.1 + +- update description +- update pubspec.lock files +- disable app sandbox of macos example app + +## 1.0.2 + +- update sound-null-safety diff --git a/plugins/system_network_proxy_macos/LICENSE b/plugins/system_network_proxy_macos/LICENSE new file mode 100644 index 000000000..504dbfb7b --- /dev/null +++ b/plugins/system_network_proxy_macos/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 liudonghua + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/system_network_proxy_macos/README.md b/plugins/system_network_proxy_macos/README.md new file mode 100644 index 000000000..689310906 --- /dev/null +++ b/plugins/system_network_proxy_macos/README.md @@ -0,0 +1,15 @@ +# system_network_proxy_macos + +A new flutter plugin project. + +## Getting Started + +This project is a starting point for a Flutter +[plug-in package](https://flutter.dev/developing-packages/), +a specialized package that includes platform-specific implementation code for +Android and/or iOS. + +For help getting started with Flutter, view our +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. + diff --git a/plugins/system_network_proxy_macos/lib/system_network_proxy_macos.dart b/plugins/system_network_proxy_macos/lib/system_network_proxy_macos.dart new file mode 100644 index 000000000..2b7a82b6a --- /dev/null +++ b/plugins/system_network_proxy_macos/lib/system_network_proxy_macos.dart @@ -0,0 +1,118 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:system_network_proxy_platform_interface/system_network_proxy_platform_interface.dart'; + +class SystemNetworkProxyMacos extends SystemNetworkProxyPlatform { + static SystemNetworkProxyMacos instance = SystemNetworkProxyMacos(); + + static normalizeOutput(String output) { + return output.trim().replaceAll("'", ""); + // return RegExp(r"^\s*'(?\w+)'\s*$").firstMatch(output)?.namedGroup('content'); + } + + static concatCommands(List commands) { + return commands.join(' && '); + } + + /// Returns `true` if this platform is able to getProxyEnable. + @override + Future getProxyEnable() async { + try { + var results = await Process.run('bash', [ + '-c', + concatCommands([ + 'networksetup -getwebproxy wi-fi', + ]) + ]); + print( + 'get proxyEnable, exitCode: ${results.exitCode}, stdout: ${results.stdout}'); + var proxyEnableLine = (results.stdout as String) + .split('\n') + .where((item) => item.contains('Enabled')) + .first + .trim(); + return proxyEnableLine.endsWith('Yes'); + } catch (e) { + print(e); + return false; + } + } + + /// Returns `true` if this platform is able to setProxyEnable [proxyEnable]. + @override + Future setProxyEnable(bool proxyEnable) async { + try { + var proxyMode = proxyEnable ? 'on' : 'off'; + var results = await Process.run('bash', [ + '-c', + concatCommands([ + 'networksetup -setwebproxystate wi-fi $proxyMode', + // 'networksetup -setsecurewebproxystate wi-fi $proxyMode', + ]) + ]); + print( + 'set proxyEnable, exitCode: ${results.exitCode}, stdout: ${results.stdout}'); + return results.exitCode == 0; + } catch (e) { + print(e); + return false; + } + } + + /// Returns `true` if this platform is able to getProxyServer. + @override + Future getProxyServer() async { + try { + var results = await Process.run('bash', [ + '-c', + concatCommands([ + 'networksetup -getwebproxy wi-fi', + ]) + ]); + print( + 'get proxyServer, exitCode: ${results.exitCode}, stdout: ${results.stdout}'); + + var match = RegExp( + r"^.*Enabled: (?.*)\nServer: (?.*)\nPort: (?.*)\n.*$", + multiLine: true) + .firstMatch(results.stdout); + var server = match?.namedGroup('server') ?? ''; + var port = match?.namedGroup('port') ?? ''; + if (server == '') { + return ''; + } + return '$server:$port'; + } catch (e) { + print(e); + return ''; + } + } + + /// Returns `true` if this platform is able to setProxyServer [proxyServer]. + @override + Future setProxyServer(String proxyServer) async { + try { + var match = RegExp(r"^(?:http://)?(?.+):(?\d+)$") + .firstMatch(proxyServer); + if (match == null) { + print('proxyServer parse error!'); + return false; + } + var host = match.namedGroup('host'); + var port = match.namedGroup('port'); + var results = await Process.run('bash', [ + '-c', + concatCommands([ + 'networksetup -setwebproxy wi-fi $host $port', + // 'networksetup -setsecurewebproxy wi-fi $host $port', + ]) + ]); + print( + 'set proxyServer, exitCode: ${results.exitCode}, stdout: ${results.stdout}'); + return results.exitCode == 0; + } catch (e) { + print(e); + return false; + } + } +} diff --git a/plugins/system_network_proxy_macos/macos/Classes/SystemNetworkProxyMacosPlugin.swift b/plugins/system_network_proxy_macos/macos/Classes/SystemNetworkProxyMacosPlugin.swift new file mode 100644 index 000000000..925134762 --- /dev/null +++ b/plugins/system_network_proxy_macos/macos/Classes/SystemNetworkProxyMacosPlugin.swift @@ -0,0 +1,19 @@ +import Cocoa +import FlutterMacOS + +public class SystemNetworkProxyMacosPlugin: NSObject, FlutterPlugin { + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "system_network_proxy_macos", binaryMessenger: registrar.messenger) + let instance = SystemNetworkProxyMacosPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "getPlatformVersion": + result("macOS " + ProcessInfo.processInfo.operatingSystemVersionString) + default: + result(FlutterMethodNotImplemented) + } + } +} diff --git a/plugins/system_network_proxy_macos/macos/system_network_proxy_macos.podspec b/plugins/system_network_proxy_macos/macos/system_network_proxy_macos.podspec new file mode 100644 index 000000000..3016e6865 --- /dev/null +++ b/plugins/system_network_proxy_macos/macos/system_network_proxy_macos.podspec @@ -0,0 +1,22 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint system_network_proxy_macos.podspec' to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'system_network_proxy_macos' + s.version = '0.0.1' + s.summary = 'A new flutter plugin project.' + s.description = <<-DESC +A new flutter plugin project. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'FlutterMacOS' + + s.platform = :osx, '10.11' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } + s.swift_version = '5.0' +end diff --git a/plugins/system_network_proxy_macos/pubspec.yaml b/plugins/system_network_proxy_macos/pubspec.yaml new file mode 100644 index 000000000..9fdbf0ee7 --- /dev/null +++ b/plugins/system_network_proxy_macos/pubspec.yaml @@ -0,0 +1,61 @@ +name: system_network_proxy_macos +description: It's a simple utility of system network proxy which can get and set status of proxy, details of proxy. +version: 1.0.2 +homepage: https://github.com/liudonghua123/system_network_proxy + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.20.0" + +dependencies: + flutter: + sdk: flutter + system_network_proxy_platform_interface: ^1.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + pedantic: ^1.8.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + # This section identifies this Flutter project as a plugin project. + # The 'pluginClass' and Android 'package' identifiers should not ordinarily + # be modified. They are used by the tooling to maintain consistency when + # adding or updating assets for this project. + plugin: + platforms: + macos: + dartPluginClass: SystemNetworkProxyMacos + # To add assets to your plugin package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + # To add custom fonts to your plugin package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/pubspec.lock b/pubspec.lock index 5a905e5c2..357141d97 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,23 +5,18 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "45cfa8471b89fb6643fe9bf51bd7931a76b8f5ec2d65de4fb176dba8d4f22c77" + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" url: "https://pub.dev" source: hosted - version: "73.0.0" - _macros: - dependency: transitive - description: dart - source: sdk - version: "0.3.2" + version: "67.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: "4959fec185fe70cce007c57e9ab6983101dbe593d2bf8bbfb4453aaec0cf470a" + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" url: "https://pub.dev" source: hosted - version: "6.8.0" + version: "6.4.1" analyzer_plugin: dependency: transitive description: @@ -218,10 +213,10 @@ packages: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.1" charcode: dependency: transitive description: @@ -266,10 +261,10 @@ packages: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" code_builder: dependency: transitive description: @@ -346,10 +341,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" + sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" url: "https://pub.dev" source: hosted - version: "2.3.7" + version: "2.3.6" dbus: dependency: transitive description: @@ -468,10 +463,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.3" ffi: dependency: transitive description: @@ -845,10 +840,10 @@ packages: dependency: "direct main" description: name: intl - sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" url: "https://pub.dev" source: hosted - version: "0.19.0" + version: "0.20.2" io: dependency: transitive description: @@ -934,26 +929,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" like_button: dependency: "direct main" description: @@ -991,10 +986,10 @@ packages: dependency: transitive description: name: local_auth_android - sha256: "6763aaf8965f21822624cb2fd3c03d2a8b3791037b5efb0fe4b13e110f5afc92" + sha256: a0bdfcc0607050a26ef5b31d6b4b254581c3d3ce3c1816ab4d4f4a9173e84467 url: "https://pub.dev" source: hosted - version: "1.0.46" + version: "1.0.56" local_auth_darwin: dependency: transitive description: @@ -1035,14 +1030,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" - macros: - dependency: transitive - description: - name: macros - sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" - url: "https://pub.dev" - source: hosted - version: "0.1.2-main.4" matcher: dependency: "direct overridden" description: @@ -1055,10 +1042,10 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" maybe_just_nothing: dependency: transitive description: @@ -1071,10 +1058,10 @@ packages: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.17.0" mime: dependency: transitive description: @@ -1135,10 +1122,10 @@ packages: dependency: "direct main" description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" path_drawing: dependency: transitive description: @@ -1377,10 +1364,10 @@ packages: dependency: transitive description: name: rxdart - sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" url: "https://pub.dev" source: hosted - version: "0.27.7" + version: "0.28.0" saver_gallery: dependency: "direct main" description: @@ -1514,7 +1501,7 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" smart_auth: dependency: transitive description: @@ -1631,18 +1618,18 @@ packages: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" stream_transform: dependency: transitive description: @@ -1663,18 +1650,18 @@ packages: dependency: "direct main" description: name: syncfusion_flutter_charts - sha256: cdc207291ad0caf34cb9e20caa8938ff65f67a413b3a86a14d3a7b341a772100 + sha256: "8ddcfa4916ae57376eb97bdddcc0186724cd51480879a83eaad9b692a4d92d28" url: "https://pub.dev" source: hosted - version: "27.1.48" + version: "33.2.5" syncfusion_flutter_core: dependency: transitive description: name: syncfusion_flutter_core - sha256: "325f519ce4ad8edd81811c21b853d72018529e353584490824da0555156ba076" + sha256: "25f2e349bc14a1a4f9da95f24484ea1751de2ab13dc11a2d4d96f54c4c7a030a" url: "https://pub.dev" source: hosted - version: "27.2.5" + version: "33.2.5" synchronized: dependency: transitive description: @@ -1701,12 +1688,11 @@ packages: source: hosted version: "1.0.2" system_network_proxy_macos: - dependency: transitive + dependency: "direct overridden" description: - name: system_network_proxy_macos - sha256: "89ed28d040f21b2794df1147150900e88f046372d60a7fa7848b5e4c9fe6263f" - url: "https://pub.dev" - source: hosted + path: "plugins/system_network_proxy_macos" + relative: true + source: path version: "1.0.2" system_network_proxy_platform_interface: dependency: transitive @@ -1744,10 +1730,10 @@ packages: dependency: "direct main" description: name: template_expressions - sha256: "5e5de0eab8314e697e8e65b89e33ded3e209dbf34c4c6c1167fc7f1fc74bf9f6" + sha256: "3e64a9b888fb8ea41e91e62d0053b706818414050f0762076b721576d782a34a" url: "https://pub.dev" source: hosted - version: "3.2.0+7" + version: "3.3.1+2" term_glyph: dependency: transitive description: @@ -1920,10 +1906,10 @@ packages: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" video_player: dependency: transitive description: @@ -2142,5 +2128,5 @@ packages: source: hosted version: "2.0.1" sdks: - dart: ">=3.5.1 <4.0.0" - flutter: ">=3.24.0" + dart: ">=3.9.0 <4.0.0" + flutter: ">=3.35.1" diff --git a/pubspec.yaml b/pubspec.yaml index ccea58b87..85a7234aa 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -28,8 +28,8 @@ dependencies: waterfall_flow: 3.0.2 extended_image: 8.1.0 font_awesome_flutter: 9.2.0 - intl: 0.19.0 - collection: 1.19.0 + intl: ^0.20.2 + collection: any clipboard: 0.1.3 scrollable_positioned_list: git: @@ -38,7 +38,7 @@ dependencies: flutter_list_view: 1.1.21 executor: 2.2.2 retry: 3.1.2 - path: 1.9.0 + path: ^1.9.0 drift: 2.21.0 sqlite3_flutter_libs: 0.5.32 path_provider: 2.1.3 @@ -72,7 +72,7 @@ dependencies: flutter_windowmanager_plus: 1.0.1 animate_do: 3.3.4 share_plus: 10.1.1 - syncfusion_flutter_charts: 27.1.48 + syncfusion_flutter_charts: ^33.2.5 archive: 3.6.1 # https://github.com/ponnamkarthik/FlutterToast/pull/542 fluttertoast: @@ -118,12 +118,14 @@ dependencies: j_downloader: git: https://github.com/jiangtian616/JDownloader device_info_plus: 11.1.0 - template_expressions: 3.2.0+7 + template_expressions: ^3.3.1+2 uuid: 4.4.0 xml: 6.5.0 telegram: 0.0.9 dependency_overrides: + system_network_proxy_macos: + path: plugins/system_network_proxy_macos test_api: 0.6.0 matcher: 0.12.16 chewie: 1.7.0 From 6c891029daaab173e00e0851bef5377382f087ab Mon Sep 17 00:00:00 2001 From: iphreetom Date: Sun, 10 May 2026 16:27:56 +0800 Subject: [PATCH 2/3] Add download author categories --- .../pages/download/download_base_page.dart | 116 ++++--- .../gallery_category_grid_download_page.dart | 263 ++++++++++++++ ...ery_category_grid_download_page_logic.dart | 96 ++++++ ...ery_category_grid_download_page_state.dart | 33 ++ .../grid/mixin/grid_download_page_mixin.dart | 5 + .../gallery_category_list_download_page.dart | 326 ++++++++++++++++++ ...ery_category_list_download_page_logic.dart | 113 ++++++ ...ery_category_list_download_page_state.dart | 34 ++ lib/src/service/gallery_download_service.dart | 44 +++ 9 files changed, 988 insertions(+), 42 deletions(-) create mode 100644 lib/src/pages/download/grid/gallery/gallery_category_grid_download_page.dart create mode 100644 lib/src/pages/download/grid/gallery/gallery_category_grid_download_page_logic.dart create mode 100644 lib/src/pages/download/grid/gallery/gallery_category_grid_download_page_state.dart create mode 100644 lib/src/pages/download/list/gallery/gallery_category_list_download_page.dart create mode 100644 lib/src/pages/download/list/gallery/gallery_category_list_download_page_logic.dart create mode 100644 lib/src/pages/download/list/gallery/gallery_category_list_download_page_state.dart diff --git a/lib/src/pages/download/download_base_page.dart b/lib/src/pages/download/download_base_page.dart index 39d07c047..67b60f9c3 100644 --- a/lib/src/pages/download/download_base_page.dart +++ b/lib/src/pages/download/download_base_page.dart @@ -5,9 +5,10 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:jhentai/src/enum/config_enum.dart'; import 'package:jhentai/src/extension/widget_extension.dart'; +import 'package:jhentai/src/pages/download/grid/gallery/gallery_category_grid_download_page.dart'; import 'package:jhentai/src/pages/download/grid/local/local_gallery_grid_page.dart'; +import 'package:jhentai/src/pages/download/list/gallery/gallery_category_list_download_page.dart'; import 'package:jhentai/src/service/local_config_service.dart'; -import 'package:jhentai/src/service/storage_service.dart'; import 'package:simple_animations/animation_controller_extension/animation_controller_extension.dart'; import 'package:simple_animations/animation_mixin/animation_mixin.dart'; import '../../config/ui_config.dart'; @@ -26,16 +27,21 @@ class DownloadPage extends StatefulWidget { class _DownloadPageState extends State { DownloadPageGalleryType galleryType = DownloadPageGalleryType.download; - DownloadPageBodyType bodyType = GetPlatform.isMobile ? DownloadPageBodyType.list : DownloadPageBodyType.grid; + DownloadPageBodyType bodyType = GetPlatform.isMobile + ? DownloadPageBodyType.list + : DownloadPageBodyType.grid; Completer bodyTypeCompleter = Completer(); @override void initState() { super.initState(); - localConfigService.read(configKey: ConfigEnum.downloadPageBodyType).then((bodyTypeString) { + localConfigService + .read(configKey: ConfigEnum.downloadPageBodyType) + .then((bodyTypeString) { if (bodyTypeString != null) { - bodyType = DownloadPageBodyType.values[int.tryParse(bodyTypeString) ?? 0]; + bodyType = + DownloadPageBodyType.values[int.tryParse(bodyTypeString) ?? 0]; } }).whenComplete(() { bodyTypeCompleter.complete(); @@ -51,31 +57,52 @@ class _DownloadPageState extends State { galleryType = notification.galleryType ?? galleryType; bodyType = notification.bodyType ?? bodyType; }); - localConfigService.write(configKey: ConfigEnum.downloadPageBodyType, value: (notification.bodyType ?? bodyType).index.toString()); + localConfigService.write( + configKey: ConfigEnum.downloadPageBodyType, + value: (notification.bodyType ?? bodyType).index.toString()); return true; }, child: FutureBuilder( future: bodyTypeCompleter.future, builder: (_, __) => !bodyTypeCompleter.isCompleted ? const Center() - : galleryType == DownloadPageGalleryType.download - ? bodyType == DownloadPageBodyType.list - ? GalleryListDownloadPage(key: const PageStorageKey('GalleryListDownloadBody')) - : GalleryGridDownloadPage(key: const PageStorageKey('GalleryGridDownloadBody')) - : galleryType == DownloadPageGalleryType.archive - ? bodyType == DownloadPageBodyType.list - ? ArchiveListDownloadPage(key: const PageStorageKey('ArchiveListDownloadBody')) - : ArchiveGridDownloadPage(key: const PageStorageKey('ArchiveGridDownloadBody')) - : bodyType == DownloadPageBodyType.list - ? LocalGalleryListPage(key: const PageStorageKey('LocalGalleryListBody')) - : LocalGalleryGridPage(key: const PageStorageKey('LocalGalleryGridBody')), + : _buildPageBody(), ), ), ).enableMouseDrag(); } + + Widget _buildPageBody() { + switch (galleryType) { + case DownloadPageGalleryType.download: + return bodyType == DownloadPageBodyType.list + ? GalleryListDownloadPage( + key: const PageStorageKey('GalleryListDownloadBody')) + : GalleryGridDownloadPage( + key: const PageStorageKey('GalleryGridDownloadBody')); + case DownloadPageGalleryType.archive: + return bodyType == DownloadPageBodyType.list + ? ArchiveListDownloadPage( + key: const PageStorageKey('ArchiveListDownloadBody')) + : ArchiveGridDownloadPage( + key: const PageStorageKey('ArchiveGridDownloadBody')); + case DownloadPageGalleryType.local: + return bodyType == DownloadPageBodyType.list + ? LocalGalleryListPage( + key: const PageStorageKey('LocalGalleryListBody')) + : LocalGalleryGridPage( + key: const PageStorageKey('LocalGalleryGridBody')); + case DownloadPageGalleryType.downloadCategory: + return bodyType == DownloadPageBodyType.list + ? GalleryCategoryListDownloadPage( + key: const PageStorageKey('GalleryCategoryListDownloadBody')) + : GalleryCategoryGridDownloadPage( + key: const PageStorageKey('GalleryCategoryGridDownloadBody')); + } + } } -enum DownloadPageGalleryType { download, archive, local } +enum DownloadPageGalleryType { download, archive, local, downloadCategory } enum DownloadPageBodyType { list, grid } @@ -89,7 +116,8 @@ class DownloadPageBodyTypeChangeNotification extends Notification { class DownloadPageSegmentControl extends StatelessWidget { final DownloadPageGalleryType galleryType; - const DownloadPageSegmentControl({Key? key, required this.galleryType}) : super(key: key); + const DownloadPageSegmentControl({Key? key, required this.galleryType}) + : super(key: key); @override Widget build(BuildContext context) { @@ -97,31 +125,33 @@ class DownloadPageSegmentControl extends StatelessWidget { groupValue: galleryType, padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 3), children: { - DownloadPageGalleryType.download: SizedBox( - width: UIConfig.downloadPageSegmentedControlWidth, - child: Center( - child: Text( - 'download'.tr, - style: const TextStyle(fontSize: UIConfig.downloadPageSegmentedTextSize, fontWeight: FontWeight.bold), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ), - DownloadPageGalleryType.archive: Text( - 'archive'.tr, - style: const TextStyle(fontSize: UIConfig.downloadPageSegmentedTextSize, fontWeight: FontWeight.bold), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - DownloadPageGalleryType.local: Text( - 'local'.tr, - style: const TextStyle(fontSize: UIConfig.downloadPageSegmentedTextSize, fontWeight: FontWeight.bold), + DownloadPageGalleryType.download: _buildSegment('download'.tr), + DownloadPageGalleryType.archive: _buildSegment('archive'.tr), + DownloadPageGalleryType.local: _buildSegment('local'.tr), + if (GetPlatform.isMacOS) + DownloadPageGalleryType.downloadCategory: + _buildSegment('downloadCategory'.tr, width: 72), + }, + onValueChanged: (value) => + DownloadPageBodyTypeChangeNotification(galleryType: value!) + .dispatch(context), + ); + } + + Widget _buildSegment(String text, + {double width = UIConfig.downloadPageSegmentedControlWidth}) { + return SizedBox( + width: width, + child: Center( + child: Text( + text, + style: const TextStyle( + fontSize: UIConfig.downloadPageSegmentedTextSize, + fontWeight: FontWeight.bold), maxLines: 1, overflow: TextOverflow.ellipsis, ), - }, - onValueChanged: (value) => DownloadPageBodyTypeChangeNotification(galleryType: value!).dispatch(context), + ), ); } } @@ -135,9 +165,11 @@ class GroupOpenIndicator extends StatefulWidget { State createState() => _GroupOpenIndicatorState(); } -class _GroupOpenIndicatorState extends State with AnimationMixin { +class _GroupOpenIndicatorState extends State + with AnimationMixin { bool isOpen = false; - late Animation animation = Tween(begin: 0.0, end: -0.25).animate(controller); + late Animation animation = + Tween(begin: 0.0, end: -0.25).animate(controller); @override void initState() { diff --git a/lib/src/pages/download/grid/gallery/gallery_category_grid_download_page.dart b/lib/src/pages/download/grid/gallery/gallery_category_grid_download_page.dart new file mode 100644 index 000000000..4d8229c7b --- /dev/null +++ b/lib/src/pages/download/grid/gallery/gallery_category_grid_download_page.dart @@ -0,0 +1,263 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:jhentai/src/database/database.dart'; +import 'package:jhentai/src/mixin/scroll_to_top_page_mixin.dart'; +import 'package:jhentai/src/pages/download/mixin/basic/multi_select/multi_select_download_page_logic_mixin.dart'; +import 'package:jhentai/src/pages/download/mixin/basic/multi_select/multi_select_download_page_mixin.dart'; +import 'package:jhentai/src/pages/download/mixin/basic/multi_select/multi_select_download_page_state_mixin.dart'; +import 'package:jhentai/src/pages/download/mixin/gallery/gallery_download_page_logic_mixin.dart'; +import 'package:jhentai/src/pages/download/mixin/gallery/gallery_download_page_mixin.dart'; +import 'package:jhentai/src/pages/download/mixin/gallery/gallery_download_page_state_mixin.dart'; +import 'package:jhentai/src/service/super_resolution_service.dart'; +import 'package:loading_animation_widget/loading_animation_widget.dart'; + +import '../../../../config/ui_config.dart'; +import '../../../../model/gallery_image.dart'; +import '../../../../service/gallery_download_service.dart'; +import '../../download_base_page.dart'; +import '../mixin/grid_download_page_mixin.dart'; +import 'gallery_category_grid_download_page_logic.dart'; +import 'gallery_category_grid_download_page_state.dart'; + +class GalleryCategoryGridDownloadPage extends StatelessWidget + with + Scroll2TopPageMixin, + MultiSelectDownloadPageMixin, + GalleryDownloadPageMixin, + GridBasePage { + GalleryCategoryGridDownloadPage({Key? key}) : super(key: key); + + @override + final DownloadPageGalleryType galleryType = + DownloadPageGalleryType.downloadCategory; + @override + final GalleryCategoryGridDownloadPageLogic logic = + Get.put( + GalleryCategoryGridDownloadPageLogic(), + permanent: true); + @override + final GalleryCategoryGridDownloadPageState state = + Get.find().state; + + @override + GalleryDownloadPageLogicMixin get galleryDownloadPageLogic => logic; + + @override + GalleryDownloadPageStateMixin get galleryDownloadPageState => state; + + @override + MultiSelectDownloadPageLogicMixin get multiSelectDownloadPageLogic => logic; + + @override + MultiSelectDownloadPageStateMixin get multiSelectDownloadPageState => state; + + @override + List buildAppBarActions(BuildContext context) { + return [ + PopupMenuButton( + itemBuilder: (context) { + return [ + PopupMenuItem( + value: 0, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.view_list), + const SizedBox(width: 12), + Text('switch2ListMode'.tr) + ], + ), + ), + PopupMenuItem( + value: 1, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.done_all), + const SizedBox(width: 12), + Text('multiSelect'.tr) + ], + ), + ), + PopupMenuItem( + value: 2, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.play_arrow), + const SizedBox(width: 12), + Text('resumeAllTasks'.tr) + ], + ), + ), + PopupMenuItem( + value: 3, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.pause), + const SizedBox(width: 12), + Text('pauseAllTasks'.tr) + ], + ), + ), + ]; + }, + onSelected: (value) { + if (value == 0) { + DownloadPageBodyTypeChangeNotification( + bodyType: DownloadPageBodyType.list) + .dispatch(context); + } + if (value == 1) { + logic.enterSelectMode(); + } + if (value == 2) { + logic.handleResumeAllTasks(); + } + if (value == 3) { + logic.handlePauseAllTasks(); + } + }, + ), + ]; + } + + @override + PreferredSizeWidget? buildAppBarBottom(BuildContext context) { + return _buildAuthorFilterBar(context); + } + + PreferredSizeWidget _buildAuthorFilterBar(BuildContext context) { + return PreferredSize( + preferredSize: const Size.fromHeight(48), + child: SizedBox( + height: 48, + child: Padding( + padding: const EdgeInsets.only(left: 16, right: 16, bottom: 8), + child: TextField( + controller: logic.authorFilterController, + onChanged: logic.updateAuthorFilterKeyword, + decoration: InputDecoration( + isDense: true, + hintText: 'filter'.tr, + prefixIcon: const Icon(Icons.search, size: 20), + border: + OutlineInputBorder(borderRadius: BorderRadius.circular(8)), + contentPadding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + ), + ), + ), + ), + ); + } + + @override + Widget? buildGridBottomAppBar(BuildContext context) { + return buildBottomAppBar(); + } + + @override + GridGroup groupBuilder( + BuildContext context, String groupName, bool inEditMode) { + List gallerys = + state.galleryObjectsWithGroup(groupName); + return GridGroup( + groupName: groupName, + contentSize: gallerys.length, + widgets: gallerys + .sublist(0, min(GridGroup.maxWidgetCount, gallerys.length)) + .map((gallery) => _buildGroupCover(context, gallery)) + .toList(), + onTap: () => logic.enterGroup(groupName), + ); + } + + @override + GridGallery galleryBuilder( + BuildContext context, GalleryDownloadedData gallery, bool inEditMode) { + return GridGallery( + title: gallery.title, + widget: GetBuilder( + id: '${logic.itemCardId}::${gallery.gid}', + builder: (_) => Stack( + children: [ + _buildCover(context, gallery), + if (state.selectedGids.contains(gallery.gid)) _buildSelectedIcon(), + ], + ), + ), + parseFromBot: false, + isOriginal: gallery.downloadOriginalImage, + gid: gallery.gid, + superResolutionType: SuperResolutionType.gallery, + onTapWidget: () => logic.handleTapItem(gallery), + onTapTitle: () => logic.handleTapItem(gallery), + onLongPress: () => + logic.handleLongPressOrSecondaryTapItem(gallery, context), + onSecondTap: () => + logic.handleLongPressOrSecondaryTapItem(gallery, context), + onTertiaryTap: () => logic.handleTapItem(gallery), + ); + } + + GetBuilder _buildGroupCover( + BuildContext context, GalleryDownloadedData gallery) { + return GetBuilder( + id: '${logic.downloadService.downloadImageUrlId}::${gallery.gid}::0', + builder: (_) { + GalleryImage? image = + logic.downloadService.galleryDownloadInfos[gallery.gid]?.images[0]; + + if (image?.downloadStatus == DownloadStatus.downloaded) { + return buildGroupInnerImage(image!); + } + + return Center( + child: LoadingAnimationWidget.horizontalRotatingDots( + color: UIConfig.downloadPageLoadingIndicatorColor(context), + size: 16), + ); + }, + ); + } + + GetBuilder _buildCover( + BuildContext context, GalleryDownloadedData gallery) { + return GetBuilder( + id: '${logic.downloadService.downloadImageUrlId}::${gallery.gid}::0', + builder: (_) { + GalleryImage? image = + logic.downloadService.galleryDownloadInfos[gallery.gid]?.images[0]; + + if (image?.downloadStatus == DownloadStatus.downloaded) { + return buildGalleryImage(image!); + } + + return Center( + child: LoadingAnimationWidget.horizontalRotatingDots( + color: UIConfig.downloadPageLoadingIndicatorColor(context), + size: 16), + ); + }, + ); + } + + Widget _buildSelectedIcon() { + return Center( + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + border: + Border.all(color: UIConfig.downloadPageGridViewSelectIconColor), + color: UIConfig.downloadPageGridViewSelectIconBackGroundColor, + ), + child: const Icon(Icons.check, + color: UIConfig.downloadPageGridViewSelectIconColor), + ), + ); + } +} diff --git a/lib/src/pages/download/grid/gallery/gallery_category_grid_download_page_logic.dart b/lib/src/pages/download/grid/gallery/gallery_category_grid_download_page_logic.dart new file mode 100644 index 000000000..5b5106af5 --- /dev/null +++ b/lib/src/pages/download/grid/gallery/gallery_category_grid_download_page_logic.dart @@ -0,0 +1,96 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:jhentai/src/extension/get_logic_extension.dart'; +import 'package:jhentai/src/mixin/scroll_to_top_logic_mixin.dart'; +import 'package:jhentai/src/mixin/scroll_to_top_state_mixin.dart'; +import 'package:jhentai/src/mixin/update_global_gallery_status_logic_mixin.dart'; +import 'package:jhentai/src/pages/download/mixin/basic/multi_select/multi_select_download_page_logic_mixin.dart'; + +import '../../../../database/database.dart'; +import '../../../../widget/eh_alert_dialog.dart'; +import '../../mixin/basic/multi_select/multi_select_download_page_state_mixin.dart'; +import '../../mixin/gallery/gallery_download_page_logic_mixin.dart'; +import '../mixin/grid_download_page_logic_mixin.dart'; +import '../mixin/grid_download_page_service_mixin.dart'; +import '../mixin/grid_download_page_state_mixin.dart'; +import 'gallery_category_grid_download_page_state.dart'; + +class GalleryCategoryGridDownloadPageLogic extends GetxController + with + Scroll2TopLogicMixin, + MultiSelectDownloadPageLogicMixin, + GalleryDownloadPageLogicMixin, + GridBasePageLogic, + UpdateGlobalGalleryStatusLogicMixin { + GalleryCategoryGridDownloadPageState state = + GalleryCategoryGridDownloadPageState(); + + final TextEditingController authorFilterController = TextEditingController(); + + @override + Scroll2TopStateMixin get scroll2TopState => state; + + @override + GridBasePageState get gridBasePageState => state; + + @override + MultiSelectDownloadPageStateMixin get multiSelectDownloadPageState => state; + + @override + GridBasePageServiceMixin get galleryService => downloadService; + + @override + void handleRemoveItem(GalleryDownloadedData gallery, bool deleteImages, + BuildContext context) async { + bool isUpdatingDependent = downloadService.isUpdatingDependent(gallery.gid); + + if (isUpdatingDependent) { + bool? result = await showDialog( + context: context, + builder: (_) => EHDialog( + title: 'delete'.tr + '?', + content: 'deleteUpdatingDependentHint'.tr, + ), + ); + if (result == null || !result) { + return; + } + } + + downloadService + .deleteGallery(gallery, deleteImages: deleteImages) + .then((_) => super.handleRemoveItem(gallery, deleteImages, context)); + } + + @override + Future selectAllItem() async { + multiSelectDownloadPageState.selectedGids.clear(); + multiSelectDownloadPageState.selectedGids.addAll(state.currentGalleryObjects + .cast() + .map((gallery) => gallery.gid)); + updateSafely(multiSelectDownloadPageState.selectedGids + .map((gid) => '$itemCardId::$gid') + .toList()); + } + + @override + Future saveGalleryOrderAfterDrag( + int beforeIndex, int afterIndex) async {} + + @override + Future saveGroupOrderAfterDrag(int beforeIndex, int afterIndex) async {} + + @override + void toggleEditMode() {} + + void updateAuthorFilterKeyword(String keyword) { + state.authorFilterKeyword = keyword; + updateSafely([bodyId]); + } + + @override + void onClose() { + authorFilterController.dispose(); + super.onClose(); + } +} diff --git a/lib/src/pages/download/grid/gallery/gallery_category_grid_download_page_state.dart b/lib/src/pages/download/grid/gallery/gallery_category_grid_download_page_state.dart new file mode 100644 index 000000000..fc54819c7 --- /dev/null +++ b/lib/src/pages/download/grid/gallery/gallery_category_grid_download_page_state.dart @@ -0,0 +1,33 @@ +import 'package:jhentai/src/database/database.dart'; + +import '../../../../mixin/scroll_to_top_state_mixin.dart'; +import '../../../../service/gallery_download_service.dart'; +import '../../mixin/basic/multi_select/multi_select_download_page_state_mixin.dart'; +import '../../mixin/gallery/gallery_download_page_state_mixin.dart'; +import '../mixin/grid_download_page_state_mixin.dart'; + +class GalleryCategoryGridDownloadPageState + with + Scroll2TopStateMixin, + MultiSelectDownloadPageStateMixin, + GalleryDownloadPageStateMixin, + GridBasePageState { + String authorFilterKeyword = ''; + + List get filteredAuthorGroups { + String keyword = authorFilterKeyword.trim().toLowerCase(); + if (keyword.isEmpty) { + return galleryDownloadService.authorGroups; + } + return galleryDownloadService.authorGroups + .where((author) => author.toLowerCase().contains(keyword)) + .toList(); + } + + @override + List get allRootGroups => filteredAuthorGroups; + + @override + List galleryObjectsWithGroup(String groupName) => + galleryDownloadService.gallerysWithAuthor(groupName); +} diff --git a/lib/src/pages/download/grid/mixin/grid_download_page_mixin.dart b/lib/src/pages/download/grid/mixin/grid_download_page_mixin.dart index 15ef1ffd4..a1522a9fc 100644 --- a/lib/src/pages/download/grid/mixin/grid_download_page_mixin.dart +++ b/lib/src/pages/download/grid/mixin/grid_download_page_mixin.dart @@ -62,6 +62,7 @@ mixin GridBasePage on StatelessWidget implements Scroll2TopPageMixin { titleSpacing: 0, title: DownloadPageSegmentControl(galleryType: galleryType), actions: buildAppBarActions(context), + bottom: buildAppBarBottom(context), ); } @@ -147,6 +148,10 @@ mixin GridBasePage on StatelessWidget implements Scroll2TopPageMixin { return []; } + PreferredSizeWidget? buildAppBarBottom(BuildContext context) { + return null; + } + Widget? buildGridBottomAppBar(BuildContext context) { return null; } diff --git a/lib/src/pages/download/list/gallery/gallery_category_list_download_page.dart b/lib/src/pages/download/list/gallery/gallery_category_list_download_page.dart new file mode 100644 index 000000000..6265d16dc --- /dev/null +++ b/lib/src/pages/download/list/gallery/gallery_category_list_download_page.dart @@ -0,0 +1,326 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; +import 'package:get/get.dart'; +import 'package:jhentai/src/database/database.dart'; +import 'package:jhentai/src/model/gallery_image.dart'; +import 'package:jhentai/src/pages/download/mixin/basic/multi_select/multi_select_download_page_logic_mixin.dart'; +import 'package:jhentai/src/pages/download/mixin/basic/multi_select/multi_select_download_page_mixin.dart'; +import 'package:jhentai/src/pages/download/mixin/basic/multi_select/multi_select_download_page_state_mixin.dart'; +import 'package:jhentai/src/pages/download/mixin/gallery/gallery_download_page_logic_mixin.dart'; +import 'package:jhentai/src/pages/download/mixin/gallery/gallery_download_page_mixin.dart'; +import 'package:jhentai/src/pages/download/mixin/gallery/gallery_download_page_state_mixin.dart'; +import 'package:jhentai/src/widget/grouped_list.dart'; + +import '../../../../config/ui_config.dart'; +import '../../../../mixin/scroll_to_top_page_mixin.dart'; +import '../../../../service/gallery_download_service.dart'; +import '../../../../setting/performance_setting.dart'; +import '../../../../widget/eh_image.dart'; +import '../../download_base_page.dart'; +import 'gallery_category_list_download_page_logic.dart'; +import 'gallery_category_list_download_page_state.dart'; + +class GalleryCategoryListDownloadPage extends StatelessWidget + with + Scroll2TopPageMixin, + MultiSelectDownloadPageMixin, + GalleryDownloadPageMixin { + GalleryCategoryListDownloadPage({Key? key}) : super(key: key); + + final GalleryCategoryListDownloadPageLogic logic = + Get.put( + GalleryCategoryListDownloadPageLogic(), + permanent: true); + final GalleryCategoryListDownloadPageState state = + Get.find().state; + + @override + MultiSelectDownloadPageLogicMixin get multiSelectDownloadPageLogic => logic; + + @override + MultiSelectDownloadPageStateMixin get multiSelectDownloadPageState => state; + + @override + GalleryDownloadPageLogicMixin get galleryDownloadPageLogic => logic; + + @override + GalleryDownloadPageStateMixin get galleryDownloadPageState => state; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: buildAppBar(context), + body: buildBody(context), + floatingActionButton: buildFloatingActionButton(), + bottomNavigationBar: buildBottomAppBar(), + ); + } + + AppBar buildAppBar(BuildContext context) { + return AppBar( + centerTitle: true, + titleSpacing: 0, + title: const DownloadPageSegmentControl( + galleryType: DownloadPageGalleryType.downloadCategory), + actions: [ + PopupMenuButton( + itemBuilder: (context) { + return [ + PopupMenuItem( + value: 0, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.grid_view), + const SizedBox(width: 12), + Text('switch2GridMode'.tr) + ], + ), + ), + PopupMenuItem( + value: 1, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.done_all), + const SizedBox(width: 12), + Text('multiSelect'.tr) + ], + ), + ), + PopupMenuItem( + value: 2, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.play_arrow), + const SizedBox(width: 12), + Text('resumeAllTasks'.tr) + ], + ), + ), + PopupMenuItem( + value: 3, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.pause), + const SizedBox(width: 12), + Text('pauseAllTasks'.tr) + ], + ), + ), + ]; + }, + onSelected: (value) { + if (value == 0) { + DownloadPageBodyTypeChangeNotification( + bodyType: DownloadPageBodyType.grid) + .dispatch(context); + } + if (value == 1) { + logic.enterSelectMode(); + } + if (value == 2) { + logic.handleResumeAllTasks(); + } + if (value == 3) { + logic.handlePauseAllTasks(); + } + }, + ), + ], + bottom: _buildAuthorFilterBar(context), + ); + } + + PreferredSizeWidget _buildAuthorFilterBar(BuildContext context) { + return PreferredSize( + preferredSize: const Size.fromHeight(48), + child: SizedBox( + height: 48, + child: Padding( + padding: const EdgeInsets.only(left: 16, right: 16, bottom: 8), + child: TextField( + controller: logic.authorFilterController, + onChanged: logic.updateAuthorFilterKeyword, + decoration: InputDecoration( + isDense: true, + hintText: 'filter'.tr, + prefixIcon: const Icon(Icons.search, size: 20), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), + contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + ), + ), + ), + ), + ); + } + + Widget buildBody(BuildContext context) { + return GetBuilder( + id: logic.downloadService.galleryCountChangedId, + builder: (_) => GetBuilder( + id: logic.bodyId, + builder: (_) => NotificationListener( + onNotification: logic.onUserScroll, + child: FutureBuilder( + future: state.displayGroupsCompleter.future, + builder: (_, __) => !state.displayGroupsCompleter.isCompleted + ? const Center() + : GroupedList( + maxGalleryNum4Animation: + performanceSetting.maxGalleryNum4Animation.value, + scrollController: state.scrollController, + controller: state.groupedListController, + groups: logic.getAuthorGroupOpenStates(), + elements: logic.downloadService.gallerys, + elementGroup: logic.downloadService.authorOfGallery, + groupBuilder: (context, groupName, isOpen) => + _groupBuilder(context, groupName, isOpen).marginAll(5), + elementBuilder: (BuildContext context, String group, + GalleryDownloadedData gallery, isOpen) => + _itemBuilder(context, gallery), + groupUniqueKey: (String group) => group, + elementUniqueKey: (GalleryDownloadedData gallery) => + gallery.gid.toString(), + ), + ), + ), + ), + ); + } + + Widget _groupBuilder(BuildContext context, String groupName, bool isOpen) { + return GestureDetector( + onTap: () => logic.toggleDisplayGroups(groupName), + child: Container( + height: UIConfig.groupListHeight, + decoration: BoxDecoration( + color: UIConfig.groupListColor(context), + boxShadow: [if (!Get.isDarkMode) UIConfig.groupListShadow(context)], + borderRadius: BorderRadius.circular(15), + ), + child: Row( + children: [ + const SizedBox( + width: UIConfig.downloadPageGroupHeaderWidth, + child: Center(child: Icon(Icons.folder_open))), + Expanded( + child: Text( + '$groupName(${logic.downloadService.gallerysWithAuthor(groupName).length})', + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + GroupOpenIndicator(isOpen: isOpen).marginOnly(right: 8), + ], + ), + ), + ); + } + + Widget _itemBuilder(BuildContext context, GalleryDownloadedData gallery) { + return Slidable( + key: Key(gallery.gid.toString()), + endActionPane: _buildEndActionPane(context, gallery), + child: GestureDetector( + onSecondaryTap: () => + logic.handleLongPressOrSecondaryTapItem(gallery, context), + onLongPress: () => + logic.handleLongPressOrSecondaryTapItem(gallery, context), + child: _buildGallery(context, gallery).marginAll(5), + ), + ); + } + + ActionPane _buildEndActionPane( + BuildContext context, GalleryDownloadedData gallery) { + return ActionPane( + motion: const DrawerMotion(), + extentRatio: 0.15, + children: [ + SlidableAction( + icon: Icons.delete, + foregroundColor: UIConfig.alertColor(context), + backgroundColor: UIConfig.downloadPageActionBackGroundColor(context), + onPressed: (BuildContext context) => + logic.handleRemoveItem(gallery, true, context), + ) + ], + ); + } + + Widget _buildGallery(BuildContext context, GalleryDownloadedData gallery) { + return GetBuilder( + id: '${logic.itemCardId}::${gallery.gid}', + builder: (_) => GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => logic.handleTapItem(gallery), + child: Container( + height: UIConfig.downloadPageCardHeight, + decoration: state.selectedGids.contains(gallery.gid) + ? BoxDecoration( + color: UIConfig.downloadPageCardSelectedColor(context), + borderRadius: BorderRadius.circular( + UIConfig.downloadPageCardBorderRadius), + ) + : null, + child: Row( + children: [ + _buildCover(context, gallery), + Expanded(child: _buildInfo(gallery)), + ], + ), + ), + ), + ); + } + + Widget _buildCover(BuildContext context, GalleryDownloadedData gallery) { + return GetBuilder( + id: '${logic.downloadService.downloadImageUrlId}::${gallery.gid}::0', + builder: (_) { + GalleryImage? image = + logic.downloadService.galleryDownloadInfos[gallery.gid]?.images[0]; + + if (image?.downloadStatus != DownloadStatus.downloaded) { + return SizedBox( + width: UIConfig.downloadPageCoverWidth, + height: UIConfig.downloadPageCoverHeight, + child: Center(child: UIConfig.loadingAnimation(context)), + ); + } + + return EHImage( + galleryImage: image!, + containerWidth: UIConfig.downloadPageCoverWidth, + containerHeight: UIConfig.downloadPageCoverHeight, + borderRadius: BorderRadius.circular(UIConfig.downloadPageCardBorderRadius), + fit: BoxFit.fitWidth, + maxBytes: 2 * 1024 * 1024, + ); + }, + ); + } + + Widget _buildInfo(GalleryDownloadedData gallery) { + return Stack( + children: [ + Container( + padding: const EdgeInsets.only(left: 6, right: 10, bottom: 6, top: 6), + alignment: Alignment.centerLeft, + child: Text( + gallery.title, + maxLines: 3, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: UIConfig.downloadPageCardTitleSize, height: 1.2), + ), + ), + if (state.selectedGids.contains(gallery.gid)) + const Positioned(child: Center(child: Icon(Icons.check_circle))), + ], + ); + } +} diff --git a/lib/src/pages/download/list/gallery/gallery_category_list_download_page_logic.dart b/lib/src/pages/download/list/gallery/gallery_category_list_download_page_logic.dart new file mode 100644 index 000000000..9b5e4bb07 --- /dev/null +++ b/lib/src/pages/download/list/gallery/gallery_category_list_download_page_logic.dart @@ -0,0 +1,113 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:jhentai/src/extension/get_logic_extension.dart'; +import 'package:jhentai/src/mixin/scroll_to_top_logic_mixin.dart'; +import 'package:jhentai/src/mixin/scroll_to_top_state_mixin.dart'; +import 'package:jhentai/src/mixin/update_global_gallery_status_logic_mixin.dart'; +import 'package:jhentai/src/pages/download/mixin/gallery/gallery_download_page_logic_mixin.dart'; + +import '../../../../database/database.dart'; +import '../../../../widget/eh_alert_dialog.dart'; +import '../../mixin/basic/multi_select/multi_select_download_page_logic_mixin.dart'; +import '../../mixin/basic/multi_select/multi_select_download_page_state_mixin.dart'; +import 'gallery_category_list_download_page_state.dart'; + +class GalleryCategoryListDownloadPageLogic extends GetxController + with + Scroll2TopLogicMixin, + MultiSelectDownloadPageLogicMixin, + GalleryDownloadPageLogicMixin, + UpdateGlobalGalleryStatusLogicMixin { + GalleryCategoryListDownloadPageState state = + GalleryCategoryListDownloadPageState(); + + final TextEditingController authorFilterController = TextEditingController(); + + @override + MultiSelectDownloadPageStateMixin get multiSelectDownloadPageState => state; + + @override + Scroll2TopStateMixin get scroll2TopState => state; + + @override + Future onInit() async { + super.onInit(); + + state.knownGroups.addAll(downloadService.authorGroups); + state.displayGroups.addAll(downloadService.authorGroups); + state.displayGroupsCompleter.complete(); + } + + Future toggleDisplayGroups(String groupName) async { + await state.displayGroupsCompleter.future; + + if (state.displayGroups.contains(groupName)) { + state.displayGroups.remove(groupName); + } else { + state.displayGroups.add(groupName); + } + + state.groupedListController.toggleGroup(groupName); + } + + @override + void handleRemoveItem(GalleryDownloadedData gallery, bool deleteImages, + BuildContext context) async { + bool isUpdatingDependent = downloadService.isUpdatingDependent(gallery.gid); + + if (isUpdatingDependent) { + bool? result = await showDialog( + context: context, + builder: (_) => EHDialog( + title: 'delete'.tr + '?', + content: 'deleteUpdatingDependentHint'.tr, + ), + ); + if (result == null || !result) { + return; + } + } + + state.selectedGids.remove(gallery.gid); + downloadService.deleteGallery(gallery, deleteImages: deleteImages); + updateGlobalGalleryStatus(); + } + + @override + Future selectAllItem() async { + await state.displayGroupsCompleter.future; + + List gallerys = []; + for (String group in state.filteredAuthorGroups + .where((group) => state.displayGroups.contains(group))) { + gallerys.addAll(downloadService.gallerysWithAuthor(group)); + } + + multiSelectDownloadPageState.selectedGids + .addAll(gallerys.map((gallery) => gallery.gid)); + updateSafely(multiSelectDownloadPageState.selectedGids + .map((gid) => '$itemCardId::$gid') + .toList()); + } + + Map getAuthorGroupOpenStates() { + for (String group in state.filteredAuthorGroups) { + if (state.knownGroups.add(group)) { + state.displayGroups.add(group); + } + } + return Map.fromEntries(state.filteredAuthorGroups + .map((group) => MapEntry(group, state.displayGroups.contains(group)))); + } + + void updateAuthorFilterKeyword(String keyword) { + state.authorFilterKeyword = keyword; + updateSafely([bodyId]); + } + + @override + void onClose() { + authorFilterController.dispose(); + super.onClose(); + } +} diff --git a/lib/src/pages/download/list/gallery/gallery_category_list_download_page_state.dart b/lib/src/pages/download/list/gallery/gallery_category_list_download_page_state.dart new file mode 100644 index 000000000..203305f42 --- /dev/null +++ b/lib/src/pages/download/list/gallery/gallery_category_list_download_page_state.dart @@ -0,0 +1,34 @@ +import 'dart:async'; + +import 'package:jhentai/src/mixin/scroll_to_top_state_mixin.dart'; +import 'package:jhentai/src/pages/download/mixin/gallery/gallery_download_page_state_mixin.dart'; +import 'package:jhentai/src/service/gallery_download_service.dart'; +import 'package:jhentai/src/widget/grouped_list.dart'; + +import '../../../../database/database.dart'; +import '../../mixin/basic/multi_select/multi_select_download_page_state_mixin.dart'; + +class GalleryCategoryListDownloadPageState + with + Scroll2TopStateMixin, + GalleryDownloadPageStateMixin, + MultiSelectDownloadPageStateMixin { + Set displayGroups = {}; + Set knownGroups = {}; + Completer displayGroupsCompleter = Completer(); + String authorFilterKeyword = ''; + + List get filteredAuthorGroups { + String keyword = authorFilterKeyword.trim().toLowerCase(); + if (keyword.isEmpty) { + return galleryDownloadService.authorGroups; + } + return galleryDownloadService.authorGroups + .where((author) => author.toLowerCase().contains(keyword)) + .toList(); + } + + final GroupedListController + groupedListController = + GroupedListController(); +} diff --git a/lib/src/service/gallery_download_service.dart b/lib/src/service/gallery_download_service.dart index 20ea7cb1d..23c002bc8 100644 --- a/lib/src/service/gallery_download_service.dart +++ b/lib/src/service/gallery_download_service.dart @@ -34,6 +34,7 @@ import 'package:jhentai/src/setting/download_setting.dart'; import 'package:jhentai/src/setting/site_setting.dart'; import 'package:jhentai/src/setting/user_setting.dart'; import 'package:jhentai/src/utils/convert_util.dart'; +import 'package:jhentai/src/utils/file_util.dart'; import 'package:jhentai/src/utils/jh_response_parser.dart'; import 'package:jhentai/src/utils/speed_computer.dart'; import 'package:jhentai/src/service/log.dart'; @@ -74,8 +75,13 @@ class GalleryDownloadService extends GetxController with GridBasePageServiceMixi List allGroups = []; List gallerys = []; Map galleryDownloadInfos = {}; + List authorGroups = []; + Map> author2Gallerys = {}; + Map galleryGid2Author = {}; List gallerysWithGroup(String group) => gallerys.where((g) => galleryDownloadInfos[g.gid]!.group == group).toList(); + List gallerysWithAuthor(String author) => author2Gallerys[author] ?? []; + String authorOfGallery(GalleryDownloadedData gallery) => galleryGid2Author[gallery.gid] ?? extractAuthorName(gallery.title); static const int _maxRetryTimes = 3; static const int _maxRetryTimes4FetchImageHashes = 1; @@ -895,6 +901,43 @@ class GalleryDownloadService extends GetxController with GridBasePageServiceMixi return bTime.difference(aTime).inMilliseconds; }); + + _rebuildAuthorCategory(); + } + + String extractAuthorName(String title) { + RegExpMatch? match = RegExp(r'^\s*[\[【]([^\]】]+)[\]】]').firstMatch(title); + if (match == null) { + return 'other'.tr; + } + + String authorWithAlias = match.group(1)?.trim() ?? ''; + RegExpMatch? aliasMatch = RegExp(r'[\((]').firstMatch(authorWithAlias); + String author = aliasMatch == null ? authorWithAlias : authorWithAlias.substring(0, aliasMatch.start).trim(); + + return author.isEmpty ? 'other'.tr : author; + } + + void _rebuildAuthorCategory() { + authorGroups.clear(); + author2Gallerys.clear(); + galleryGid2Author.clear(); + + for (GalleryDownloadedData gallery in gallerys) { + String author = extractAuthorName(gallery.title); + galleryGid2Author[gallery.gid] = author; + (author2Gallerys[author] ??= []).add(gallery); + } + + authorGroups.addAll(author2Gallerys.keys); + authorGroups.sort(FileUtil.naturalCompare); + for (List authorGallerys in author2Gallerys.values) { + authorGallerys.sort((a, b) { + DateTime aTime = DateFormat('yyyy-MM-dd HH:mm:ss').parse(a.insertTime); + DateTime bTime = DateFormat('yyyy-MM-dd HH:mm:ss').parse(b.insertTime); + return bTime.difference(aTime).inMilliseconds; + }); + } } bool _taskHasBeenPausedOrRemoved(GalleryDownloadedData gallery) { @@ -1570,6 +1613,7 @@ class GalleryDownloadService extends GetxController with GridBasePageServiceMixi void _clearGalleryInfoInMemory(GalleryDownloadedData gallery) { gallerys.removeWhere((g) => g.gid == gallery.gid); + _rebuildAuthorCategory(); GalleryDownloadInfo? galleryDownloadInfo = galleryDownloadInfos.remove(gallery.gid); galleryDownloadInfo?.speedComputer.dispose(); From 666d14980861ba5f2f1e8495d1321c15bbb1dca0 Mon Sep 17 00:00:00 2001 From: iphreetom Date: Sun, 10 May 2026 16:31:17 +0800 Subject: [PATCH 3/3] Show reading progress and visited markers --- lib/src/database/dao/gallery_history_dao.dart | 29 +++++-- lib/src/pages/base/base_page_logic.dart | 3 + .../archive/archive_grid_download_page.dart | 2 + .../gallery_category_grid_download_page.dart | 2 + .../gallery/gallery_grid_download_page.dart | 2 + .../grid/local/local_gallery_grid_page.dart | 2 + .../grid/mixin/grid_download_page_mixin.dart | 19 ++++- .../archive/archive_list_download_page.dart | 31 +++++-- .../gallery_category_list_download_page.dart | 32 ++++++-- .../gallery/gallery_list_download_page.dart | 32 ++++++-- .../list/local/local_gallery_list_page.dart | 29 +++++-- .../download_search/download_search_page.dart | 74 ++++++++++++----- lib/src/pages/read/read_page_logic.dart | 16 +++- .../pages/search/mixin/search_page_mixin.dart | 19 +++++ .../pages/setting/read/setting_read_page.dart | 23 ++++++ lib/src/service/history_service.dart | 72 ++++++++++++++--- lib/src/service/local_config_service.dart | 12 ++- lib/src/service/local_gallery_service.dart | 10 ++- lib/src/service/read_progress_service.dart | 41 ++++++++-- lib/src/widget/eh_gallery_collection.dart | 5 +- lib/src/widget/eh_gallery_list_card_.dart | 81 ++++++++----------- lib/src/widget/eh_gallery_waterflow_card.dart | 47 ++++++++--- lib/src/widget/gallery_visited_badge.dart | 56 +++++++++++++ lib/src/widget/read_progress_badge.dart | 81 +++++++++++++++++++ 24 files changed, 581 insertions(+), 139 deletions(-) create mode 100644 lib/src/widget/gallery_visited_badge.dart create mode 100644 lib/src/widget/read_progress_badge.dart diff --git a/lib/src/database/dao/gallery_history_dao.dart b/lib/src/database/dao/gallery_history_dao.dart index d7517a51e..e29d48bb3 100644 --- a/lib/src/database/dao/gallery_history_dao.dart +++ b/lib/src/database/dao/gallery_history_dao.dart @@ -9,16 +9,19 @@ class GalleryHistoryDao { static Future> selectAll() { return (appDb.select(appDb.galleryHistoryV2) ..orderBy([ - (tbl) => OrderingTerm(expression: tbl.lastReadTime, mode: OrderingMode.asc), + (tbl) => OrderingTerm( + expression: tbl.lastReadTime, mode: OrderingMode.asc), (tbl) => OrderingTerm(expression: tbl.gid, mode: OrderingMode.asc), ])) .get(); } - static Future> selectByPageIndex(int pageIndex, int pageSize) { + static Future> selectByPageIndex( + int pageIndex, int pageSize) { return (appDb.select(appDb.galleryHistoryV2) ..orderBy([ - (tbl) => OrderingTerm(expression: tbl.lastReadTime, mode: OrderingMode.desc), + (tbl) => OrderingTerm( + expression: tbl.lastReadTime, mode: OrderingMode.desc), (tbl) => OrderingTerm(expression: tbl.gid, mode: OrderingMode.desc), ]) ..limit(pageSize, offset: pageIndex * pageSize)) @@ -29,7 +32,15 @@ class GalleryHistoryDao { return appDb.into(appDb.galleryHistoryV2).insertOnConflictUpdate(history); } - static Future batchReplaceHistory(List histories) async { + static Future contains(int gid) { + return (appDb.select(appDb.galleryHistoryV2) + ..where((tbl) => tbl.gid.equals(gid))) + .getSingleOrNull() + .then((value) => value != null); + } + + static Future batchReplaceHistory( + List histories) async { if (histories.isEmpty) { return; } @@ -40,7 +51,9 @@ class GalleryHistoryDao { } static Future deleteHistory(int gid) { - return (appDb.delete(appDb.galleryHistoryV2)..where((tbl) => tbl.gid.equals(gid))).go(); + return (appDb.delete(appDb.galleryHistoryV2) + ..where((tbl) => tbl.gid.equals(gid))) + .go(); } static Future deleteAllHistory() { @@ -51,11 +64,13 @@ class GalleryHistoryDao { return appDb.galleryHistory.count().getSingle(); } - static Future> selectLargerThanLastReadTimeAndGidOld(String lastReadTime, int limit) { + static Future> selectLargerThanLastReadTimeAndGidOld( + String lastReadTime, int limit) { return (appDb.select(appDb.galleryHistory) ..where((tbl) => tbl.lastReadTime.isBiggerOrEqualValue(lastReadTime)) ..orderBy([ - (tbl) => OrderingTerm(expression: tbl.lastReadTime, mode: OrderingMode.asc), + (tbl) => OrderingTerm( + expression: tbl.lastReadTime, mode: OrderingMode.asc), (tbl) => OrderingTerm(expression: tbl.gid, mode: OrderingMode.asc), ]) ..limit(limit)) diff --git a/lib/src/pages/base/base_page_logic.dart b/lib/src/pages/base/base_page_logic.dart index 2bd6c3cfe..d1899dd03 100644 --- a/lib/src/pages/base/base_page_logic.dart +++ b/lib/src/pages/base/base_page_logic.dart @@ -21,8 +21,10 @@ import '../../mixin/scroll_to_top_state_mixin.dart'; import '../../model/gallery.dart'; import '../../network/eh_request.dart'; import '../../routes/routes.dart'; +import '../../service/history_service.dart'; import '../../service/local_block_rule_service.dart'; import '../../service/tag_translation_service.dart'; +import '../../utils/convert_util.dart'; import '../../setting/user_setting.dart'; import '../../utils/eh_spider_parser.dart'; import '../../service/log.dart'; @@ -382,6 +384,7 @@ abstract class BasePageLogic extends GetxController with Scroll2TopLogicMixin { } void handleTapGalleryCard(Gallery gallery) async { + unawaited(historyService.record(gallery2GalleryHistoryModel(gallery))); toRoute( Routes.details, arguments: DetailsPageArgument(galleryUrl: gallery.galleryUrl, gallery: gallery), diff --git a/lib/src/pages/download/grid/archive/archive_grid_download_page.dart b/lib/src/pages/download/grid/archive/archive_grid_download_page.dart index 68a49837b..93d2fd82f 100644 --- a/lib/src/pages/download/grid/archive/archive_grid_download_page.dart +++ b/lib/src/pages/download/grid/archive/archive_grid_download_page.dart @@ -207,6 +207,8 @@ class ArchiveGridDownloadPage extends StatelessWidget with Scroll2TopPageMixin, ), parseFromBot: archiveDownloadService.archiveDownloadInfos[archive.gid]?.parseSource == ArchiveParseSource.bot.code, isOriginal: archive.isOriginal, + readProgressRecordKey: archive.gid.toString(), + pageCount: archive.pageCount, gid: archive.gid, superResolutionType: SuperResolutionType.archive, onTapWidget: inEditMode ? null : () => logic.handleTapItem(archive), diff --git a/lib/src/pages/download/grid/gallery/gallery_category_grid_download_page.dart b/lib/src/pages/download/grid/gallery/gallery_category_grid_download_page.dart index 4d8229c7b..3b8cf644a 100644 --- a/lib/src/pages/download/grid/gallery/gallery_category_grid_download_page.dart +++ b/lib/src/pages/download/grid/gallery/gallery_category_grid_download_page.dart @@ -192,6 +192,8 @@ class GalleryCategoryGridDownloadPage extends StatelessWidget ), parseFromBot: false, isOriginal: gallery.downloadOriginalImage, + readProgressRecordKey: gallery.gid.toString(), + pageCount: gallery.pageCount, gid: gallery.gid, superResolutionType: SuperResolutionType.gallery, onTapWidget: () => logic.handleTapItem(gallery), diff --git a/lib/src/pages/download/grid/gallery/gallery_grid_download_page.dart b/lib/src/pages/download/grid/gallery/gallery_grid_download_page.dart index 405bcd7bc..7cbdadba3 100644 --- a/lib/src/pages/download/grid/gallery/gallery_grid_download_page.dart +++ b/lib/src/pages/download/grid/gallery/gallery_grid_download_page.dart @@ -222,6 +222,8 @@ class GalleryGridDownloadPage extends StatelessWidget with Scroll2TopPageMixin, ), parseFromBot: false, isOriginal: gallery.downloadOriginalImage, + readProgressRecordKey: gallery.gid.toString(), + pageCount: gallery.pageCount, gid: gallery.gid, superResolutionType: SuperResolutionType.gallery, onTapWidget: inEditMode ? null : () => logic.handleTapItem(gallery), diff --git a/lib/src/pages/download/grid/local/local_gallery_grid_page.dart b/lib/src/pages/download/grid/local/local_gallery_grid_page.dart index 7864f520c..5ef9ba84d 100644 --- a/lib/src/pages/download/grid/local/local_gallery_grid_page.dart +++ b/lib/src/pages/download/grid/local/local_gallery_grid_page.dart @@ -93,6 +93,8 @@ class LocalGalleryGridPage extends StatelessWidget with Scroll2TopPageMixin, Gri widget: buildGalleryImage(gallery.cover), parseFromBot: false, isOriginal: false, + readProgressRecordKey: gallery.cover.path, + pageCount: gallery.pageCount, onTapWidget: () => logic.goToReadPage(gallery), onTapTitle: () => logic.goToReadPage(gallery), onLongPress: () => logic.showBottomSheet(gallery, context), diff --git a/lib/src/pages/download/grid/mixin/grid_download_page_mixin.dart b/lib/src/pages/download/grid/mixin/grid_download_page_mixin.dart index a1522a9fc..cd5fb00f2 100644 --- a/lib/src/pages/download/grid/mixin/grid_download_page_mixin.dart +++ b/lib/src/pages/download/grid/mixin/grid_download_page_mixin.dart @@ -15,6 +15,7 @@ import '../../../../mixin/scroll_to_top_page_mixin.dart'; import '../../../../mixin/scroll_to_top_state_mixin.dart'; import '../../../../routes/routes.dart'; import '../../../../setting/style_setting.dart'; +import '../../../../widget/read_progress_badge.dart'; import '../../../layout/mobile_v2/notification/tap_menu_button_notification.dart'; import '../../download_base_page.dart'; import 'grid_download_page_logic_mixin.dart'; @@ -236,6 +237,8 @@ class GridGallery extends StatelessWidget { final Widget widget; final bool parseFromBot; final bool isOriginal; + final String? readProgressRecordKey; + final int? pageCount; final int? gid; final SuperResolutionType? superResolutionType; final VoidCallback? onTapWidget; @@ -250,6 +253,8 @@ class GridGallery extends StatelessWidget { required this.widget, required this.parseFromBot, required this.isOriginal, + this.readProgressRecordKey, + this.pageCount, this.gid, this.superResolutionType, this.onTapWidget, @@ -272,7 +277,19 @@ class GridGallery extends StatelessWidget { children: [ Expanded( child: Stack( - children: [widget, buildChips(context)], + children: [ + widget, + if (readProgressRecordKey != null) + Positioned( + top: 4, + right: 4, + child: ReadProgressBadge( + recordKey: readProgressRecordKey!, + pageCount: pageCount, + ), + ), + buildChips(context), + ], ), ), GestureDetector( diff --git a/lib/src/pages/download/list/archive/archive_list_download_page.dart b/lib/src/pages/download/list/archive/archive_list_download_page.dart index faa5dbad3..104f9ec31 100644 --- a/lib/src/pages/download/list/archive/archive_list_download_page.dart +++ b/lib/src/pages/download/list/archive/archive_list_download_page.dart @@ -23,6 +23,7 @@ import '../../../../utils/date_util.dart'; import '../../../../utils/route_util.dart'; import '../../../../widget/eh_gallery_category_tag.dart'; import '../../../../widget/eh_image.dart'; +import '../../../../widget/read_progress_badge.dart'; import '../../../details/details_page_logic.dart'; import '../../../layout/mobile_v2/notification/tap_menu_button_notification.dart'; import '../../download_base_page.dart'; @@ -250,13 +251,29 @@ class ArchiveListDownloadPage extends StatelessWidget with Scroll2TopPageMixin, Routes.details, arguments: DetailsPageArgument(galleryUrl: GalleryUrl.parse(archive.galleryUrl)), ), - child: EHImage( - galleryImage: GalleryImage(url: archive.coverUrl), - containerWidth: UIConfig.downloadPageCoverWidth, - containerHeight: UIConfig.downloadPageCoverHeight, - borderRadius: BorderRadius.circular(UIConfig.downloadPageCardBorderRadius), - fit: BoxFit.fitWidth, - maxBytes: 2 * 1024 * 1024, + child: SizedBox( + width: UIConfig.downloadPageCoverWidth, + height: UIConfig.downloadPageCoverHeight, + child: Stack( + children: [ + EHImage( + galleryImage: GalleryImage(url: archive.coverUrl), + containerWidth: UIConfig.downloadPageCoverWidth, + containerHeight: UIConfig.downloadPageCoverHeight, + borderRadius: BorderRadius.circular(UIConfig.downloadPageCardBorderRadius), + fit: BoxFit.fitWidth, + maxBytes: 2 * 1024 * 1024, + ), + Positioned( + top: 4, + right: 4, + child: ReadProgressBadge( + recordKey: archive.gid.toString(), + pageCount: archive.pageCount, + ), + ), + ], + ), ), ); } diff --git a/lib/src/pages/download/list/gallery/gallery_category_list_download_page.dart b/lib/src/pages/download/list/gallery/gallery_category_list_download_page.dart index 6265d16dc..8418a0a70 100644 --- a/lib/src/pages/download/list/gallery/gallery_category_list_download_page.dart +++ b/lib/src/pages/download/list/gallery/gallery_category_list_download_page.dart @@ -16,6 +16,7 @@ import '../../../../mixin/scroll_to_top_page_mixin.dart'; import '../../../../service/gallery_download_service.dart'; import '../../../../setting/performance_setting.dart'; import '../../../../widget/eh_image.dart'; +import '../../../../widget/read_progress_badge.dart'; import '../../download_base_page.dart'; import 'gallery_category_list_download_page_logic.dart'; import 'gallery_category_list_download_page_state.dart'; @@ -292,13 +293,30 @@ class GalleryCategoryListDownloadPage extends StatelessWidget ); } - return EHImage( - galleryImage: image!, - containerWidth: UIConfig.downloadPageCoverWidth, - containerHeight: UIConfig.downloadPageCoverHeight, - borderRadius: BorderRadius.circular(UIConfig.downloadPageCardBorderRadius), - fit: BoxFit.fitWidth, - maxBytes: 2 * 1024 * 1024, + return SizedBox( + width: UIConfig.downloadPageCoverWidth, + height: UIConfig.downloadPageCoverHeight, + child: Stack( + children: [ + EHImage( + galleryImage: image!, + containerWidth: UIConfig.downloadPageCoverWidth, + containerHeight: UIConfig.downloadPageCoverHeight, + borderRadius: + BorderRadius.circular(UIConfig.downloadPageCardBorderRadius), + fit: BoxFit.fitWidth, + maxBytes: 2 * 1024 * 1024, + ), + Positioned( + top: 4, + right: 4, + child: ReadProgressBadge( + recordKey: gallery.gid.toString(), + pageCount: gallery.pageCount, + ), + ), + ], + ), ); }, ); diff --git a/lib/src/pages/download/list/gallery/gallery_list_download_page.dart b/lib/src/pages/download/list/gallery/gallery_list_download_page.dart index ce79ad071..342109c6c 100644 --- a/lib/src/pages/download/list/gallery/gallery_list_download_page.dart +++ b/lib/src/pages/download/list/gallery/gallery_list_download_page.dart @@ -3,7 +3,6 @@ import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; import 'package:jhentai/src/config/ui_config.dart'; -import 'package:jhentai/src/extension/widget_extension.dart'; import 'package:jhentai/src/model/gallery_url.dart'; import 'package:jhentai/src/pages/download/mixin/gallery/gallery_download_page_mixin.dart'; import 'package:jhentai/src/service/super_resolution_service.dart' as srs; @@ -21,6 +20,7 @@ import '../../../../utils/date_util.dart'; import '../../../../utils/route_util.dart'; import '../../../../widget/eh_gallery_category_tag.dart'; import '../../../../widget/eh_image.dart'; +import '../../../../widget/read_progress_badge.dart'; import '../../../details/details_page_logic.dart'; import '../../../layout/mobile_v2/notification/tap_menu_button_notification.dart'; import '../../download_base_page.dart'; @@ -276,13 +276,29 @@ class GalleryListDownloadPage extends StatelessWidget with Scroll2TopPageMixin, ); } - return EHImage( - galleryImage: image!, - containerWidth: UIConfig.downloadPageCoverWidth, - containerHeight: UIConfig.downloadPageCoverHeight, - borderRadius: BorderRadius.circular(UIConfig.downloadPageCardBorderRadius), - fit: BoxFit.fitWidth, - maxBytes: 2 * 1024 * 1024, + return SizedBox( + width: UIConfig.downloadPageCoverWidth, + height: UIConfig.downloadPageCoverHeight, + child: Stack( + children: [ + EHImage( + galleryImage: image!, + containerWidth: UIConfig.downloadPageCoverWidth, + containerHeight: UIConfig.downloadPageCoverHeight, + borderRadius: BorderRadius.circular(UIConfig.downloadPageCardBorderRadius), + fit: BoxFit.fitWidth, + maxBytes: 2 * 1024 * 1024, + ), + Positioned( + top: 4, + right: 4, + child: ReadProgressBadge( + recordKey: gallery.gid.toString(), + pageCount: gallery.pageCount, + ), + ), + ], + ), ); }, ), diff --git a/lib/src/pages/download/list/local/local_gallery_list_page.dart b/lib/src/pages/download/list/local/local_gallery_list_page.dart index 9f63162ec..aaa05442b 100644 --- a/lib/src/pages/download/list/local/local_gallery_list_page.dart +++ b/lib/src/pages/download/list/local/local_gallery_list_page.dart @@ -13,6 +13,7 @@ import '../../../../mixin/scroll_to_top_state_mixin.dart'; import '../../../../utils/toast_util.dart'; import '../../../../widget/eh_image.dart'; import '../../../../widget/eh_wheel_speed_controller.dart'; +import '../../../../widget/read_progress_badge.dart'; import '../../download_base_page.dart'; import 'local_gallery_list_page_logic.dart'; import 'local_gallery_list_page_state.dart'; @@ -237,12 +238,28 @@ class LocalGalleryListPage extends StatelessWidget with Scroll2TopPageMixin { } Widget _buildCover(LocalGallery gallery, BuildContext context) { - return EHImage( - galleryImage: gallery.cover, - containerWidth: UIConfig.downloadPageCoverWidth, - containerHeight: UIConfig.downloadPageCoverHeight, - fit: BoxFit.fitWidth, - maxBytes: 2 * 1024 * 1024, + return SizedBox( + width: UIConfig.downloadPageCoverWidth, + height: UIConfig.downloadPageCoverHeight, + child: Stack( + children: [ + EHImage( + galleryImage: gallery.cover, + containerWidth: UIConfig.downloadPageCoverWidth, + containerHeight: UIConfig.downloadPageCoverHeight, + fit: BoxFit.fitWidth, + maxBytes: 2 * 1024 * 1024, + ), + Positioned( + top: 4, + right: 4, + child: ReadProgressBadge( + recordKey: gallery.cover.path!, + pageCount: gallery.pageCount, + ), + ), + ], + ), ); } diff --git a/lib/src/pages/download_search/download_search_page.dart b/lib/src/pages/download_search/download_search_page.dart index 0be66c87a..ef868ad41 100644 --- a/lib/src/pages/download_search/download_search_page.dart +++ b/lib/src/pages/download_search/download_search_page.dart @@ -18,6 +18,7 @@ import '../../utils/byte_util.dart'; import '../../utils/date_util.dart'; import '../../utils/route_util.dart'; import '../../widget/eh_gallery_category_tag.dart'; +import '../../widget/read_progress_badge.dart'; import '../details/details_page_logic.dart'; import 'download_search_logic.dart'; @@ -103,7 +104,6 @@ class DownloadSearchPage extends StatelessWidget { child: GetBuilder( id: '${galleryDownloadService.galleryDownloadProgressId}::${gallery.gid}', builder: (_) { - GalleryImage? cover = galleryDownloadService.galleryDownloadInfos[gallery.gid]?.images[0]; GalleryDownloadProgress? downloadProgress = galleryDownloadService.galleryDownloadInfos[gallery.gid]?.downloadProgress; String? groupName = galleryDownloadService.galleryDownloadInfos[gallery.gid]?.group; @@ -186,23 +186,41 @@ class DownloadSearchPage extends StatelessWidget { builder: (_) { GalleryImage? image = galleryDownloadService.galleryDownloadInfos[gallery.gid]?.images[0]; - /// cover is the first image, if we haven't downloaded first image, then return a [UIConfig.loadingAnimation] - if (image?.downloadStatus != DownloadStatus.downloaded) { - return SizedBox( + Widget cover; + if (image?.downloadStatus == DownloadStatus.downloaded) { + cover = EHImage( + galleryImage: image!, + containerWidth: UIConfig.downloadSearchPageCoverWidth, + containerHeight: UIConfig.downloadSearchPageCoverHeight, + containerColor: UIConfig.galleryCardBackGroundColor(context), + borderRadius: BorderRadius.circular(UIConfig.downloadPageCardBorderRadius), + fit: BoxFit.fitWidth, + maxBytes: 2 * 1024 * 1024, + ); + } else { + cover = SizedBox( width: UIConfig.downloadSearchPageCoverWidth, height: UIConfig.downloadSearchPageCoverHeight, child: Center(child: UIConfig.loadingAnimation(context)), ); } - return EHImage( - galleryImage: image!, - containerWidth: UIConfig.downloadSearchPageCoverWidth, - containerHeight: UIConfig.downloadSearchPageCoverHeight, - containerColor: UIConfig.galleryCardBackGroundColor(context), - borderRadius: BorderRadius.circular(UIConfig.downloadPageCardBorderRadius), - fit: BoxFit.fitWidth, - maxBytes: 2 * 1024 * 1024, + return SizedBox( + width: UIConfig.downloadSearchPageCoverWidth, + height: UIConfig.downloadSearchPageCoverHeight, + child: Stack( + children: [ + cover, + Positioned( + top: 4, + right: 4, + child: ReadProgressBadge( + recordKey: gallery.gid.toString(), + pageCount: gallery.pageCount, + ), + ), + ], + ), ); }, ), @@ -411,14 +429,30 @@ class DownloadSearchPage extends StatelessWidget { Routes.details, arguments: DetailsPageArgument(galleryUrl: GalleryUrl.parse(archive.galleryUrl)), ), - child: EHImage( - galleryImage: GalleryImage(url: archive.coverUrl), - containerWidth: UIConfig.downloadSearchPageCoverWidth, - containerHeight: UIConfig.downloadSearchPageCoverHeight, - containerColor: UIConfig.galleryCardBackGroundColor(context), - borderRadius: BorderRadius.circular(UIConfig.downloadPageCardBorderRadius), - fit: BoxFit.fitWidth, - maxBytes: 2 * 1024 * 1024, + child: SizedBox( + width: UIConfig.downloadSearchPageCoverWidth, + height: UIConfig.downloadSearchPageCoverHeight, + child: Stack( + children: [ + EHImage( + galleryImage: GalleryImage(url: archive.coverUrl), + containerWidth: UIConfig.downloadSearchPageCoverWidth, + containerHeight: UIConfig.downloadSearchPageCoverHeight, + containerColor: UIConfig.galleryCardBackGroundColor(context), + borderRadius: BorderRadius.circular(UIConfig.downloadPageCardBorderRadius), + fit: BoxFit.fitWidth, + maxBytes: 2 * 1024 * 1024, + ), + Positioned( + top: 4, + right: 4, + child: ReadProgressBadge( + recordKey: archive.gid.toString(), + pageCount: archive.pageCount, + ), + ), + ], + ), ), ); } diff --git a/lib/src/pages/read/read_page_logic.dart b/lib/src/pages/read/read_page_logic.dart index d80abf35c..4af9a37d5 100644 --- a/lib/src/pages/read/read_page_logic.dart +++ b/lib/src/pages/read/read_page_logic.dart @@ -7,7 +7,6 @@ import 'package:executor/executor.dart'; import 'package:extended_image/extended_image.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; -import 'package:jhentai/src/enum/config_enum.dart'; import 'package:jhentai/src/exception/eh_parse_exception.dart'; import 'package:jhentai/src/exception/eh_site_exception.dart'; import 'package:jhentai/src/extension/dio_exception_extension.dart'; @@ -18,7 +17,6 @@ import 'package:jhentai/src/pages/read/layout/horizontal_list/horizontal_list_la import 'package:jhentai/src/pages/read/layout/horizontal_page/horizontal_page_layout_logic.dart'; import 'package:jhentai/src/pages/read/layout/vertical_list/vertical_list_layout_logic.dart'; import 'package:jhentai/src/pages/read/read_page_state.dart'; -import 'package:jhentai/src/service/local_config_service.dart'; import 'package:jhentai/src/service/super_resolution_service.dart'; import 'package:jhentai/src/service/volume_service.dart'; import 'package:jhentai/src/utils/eh_executor.dart'; @@ -32,7 +30,6 @@ import '../../model/detail_page_info.dart'; import '../../model/gallery_image.dart'; import '../../model/read_page_info.dart'; import '../../network/eh_request.dart'; -import '../../service/storage_service.dart'; import '../../setting/read_setting.dart'; import '../../utils/eh_spider_parser.dart'; import '../../service/log.dart'; @@ -89,6 +86,7 @@ class ReadPageLogic extends GetxController { final int normalPriority = 10000; bool inited = false; + int? _lastSavedReadProgressIndex; Completer delayInitCompleter = Completer(); @override @@ -607,12 +605,22 @@ class ReadPageLogic extends GetxController { } void recordReadProgress(int index) { + if (state.readPageInfo.currentImageIndex == index && _lastSavedReadProgressIndex == index) { + return; + } + state.readPageInfo.currentImageIndex = index; + _lastSavedReadProgressIndex = index; + unawaited(readProgressService.updateReadProgress( + state.readPageInfo.readProgressRecordStorageKey, + state.readPageInfo.currentImageIndex, + )); update([sliderId, pageNoId, thumbnailNoId]); } Future _flushReadProgress() async { - readProgressService.updateReadProgress( + _lastSavedReadProgressIndex = state.readPageInfo.currentImageIndex; + await readProgressService.updateReadProgress( state.readPageInfo.readProgressRecordStorageKey, state.readPageInfo.currentImageIndex, ); diff --git a/lib/src/pages/search/mixin/search_page_mixin.dart b/lib/src/pages/search/mixin/search_page_mixin.dart index 9ba8193a1..ca2241548 100644 --- a/lib/src/pages/search/mixin/search_page_mixin.dart +++ b/lib/src/pages/search/mixin/search_page_mixin.dart @@ -17,6 +17,7 @@ import '../../../routes/routes.dart'; import '../../../service/search_history_service.dart'; import '../../../service/tag_translation_service.dart'; import '../../../utils/route_util.dart'; +import '../../../widget/eh_gallery_collection.dart'; import '../../../widget/eh_search_config_dialog.dart'; import '../../../widget/eh_tag.dart'; import '../../../widget/eh_wheel_speed_controller.dart'; @@ -28,6 +29,24 @@ mixin SearchPageMixin EHGalleryCollection( + key: state.galleryCollectionKey, + context: context, + gallerys: state.gallerys, + listMode: styleSetting.pageListMode[state.route] ?? styleSetting.listMode.value, + loadingState: state.loadingState, + handleTapCard: logic.handleTapGalleryCard, + handleLongPressCard: (gallery) => logic.handleLongPressCard(context, gallery), + handleSecondaryTapCard: (gallery) => logic.handleSecondaryTapCard(context, gallery), + handleLoadMore: logic.loadMore, + showVisitedBadge: true, + ), + ); + } + List buildActionButtons({VisualDensity? visualDensity}) { return [ IconButton( diff --git a/lib/src/pages/setting/read/setting_read_page.dart b/lib/src/pages/setting/read/setting_read_page.dart index a521ec4de..d2d52c9c0 100644 --- a/lib/src/pages/setting/read/setting_read_page.dart +++ b/lib/src/pages/setting/read/setting_read_page.dart @@ -4,11 +4,13 @@ import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:jhentai/src/config/ui_config.dart'; import 'package:jhentai/src/extension/widget_extension.dart'; +import 'package:jhentai/src/service/read_progress_service.dart'; import 'package:jhentai/src/setting/read_setting.dart'; import '../../../service/log.dart'; import '../../../utils/text_input_formatter.dart'; import '../../../utils/toast_util.dart'; +import '../../../widget/eh_alert_dialog.dart'; class SettingReadPage extends StatelessWidget { final TextEditingController imageRegionWidthRatioController = TextEditingController(text: readSetting.imageRegionWidthRatio.value.toString()); @@ -57,6 +59,7 @@ class SettingReadPage extends StatelessWidget { if (readSetting.isInListReadDirection) _buildAutoModeStyle().fadeIn(const Key('autoModeStyle')).center(), if (readSetting.isInListReadDirection) _buildTurnPageMode().fadeIn(const Key('turnPageMode')).center(), _buildImageSpace().center(), + _buildResetReadProgress().center(), ], ).withListTileTheme(context), ), @@ -164,6 +167,26 @@ class SettingReadPage extends StatelessWidget { ).marginOnly(right: 12); } + Widget _buildResetReadProgress() { + return ListTile( + title: Text('resetReadProgress'.tr), + trailing: const Icon(Icons.restart_alt), + onTap: _handleResetReadProgress, + ); + } + + Future _handleResetReadProgress() async { + bool? result = await Get.dialog( + EHDialog(title: '${'resetReadProgress'.tr}?'), + ); + if (result != true) { + return; + } + + await readProgressService.clearAllReadProgress(); + toast('clearSuccess'.tr); + } + Widget _buildShowStatusInfo() { return SwitchListTile( title: Text('showStatusInfo'.tr), diff --git a/lib/src/service/history_service.dart b/lib/src/service/history_service.dart index a57b9c225..5c54edd20 100644 --- a/lib/src/service/history_service.dart +++ b/lib/src/service/history_service.dart @@ -1,7 +1,9 @@ import 'dart:convert'; +import 'package:get/get.dart'; import 'package:jhentai/src/database/dao/gallery_history_dao.dart'; import 'package:jhentai/src/database/database.dart'; +import 'package:jhentai/src/extension/get_logic_extension.dart'; import 'package:jhentai/src/extension/list_extension.dart'; import 'package:jhentai/src/model/gallery_history_model.dart'; import 'jh_service.dart'; @@ -9,13 +11,20 @@ import 'log.dart'; HistoryService historyService = HistoryService(); -class HistoryService with JHLifeCircleBeanErrorCatch implements JHLifeCircleBean { +class HistoryService extends GetxController + with JHLifeCircleBeanErrorCatch + implements JHLifeCircleBean { static const String historyUpdateId = 'historyUpdateId'; static const int pageSize = 100; + final Set _visitedGids = {}; + final Set _unvisitedGids = {}; + @override - Future doInitBean() async {} + Future doInitBean() async { + Get.put(this, permanent: true); + } @override Future doAfterBeanReady() async {} @@ -26,21 +35,33 @@ class HistoryService with JHLifeCircleBeanErrorCatch implements JHLifeCircleBean } Future> getByPageIndex(int pageIndex) async { - List historys = await GalleryHistoryDao.selectByPageIndex(pageIndex, pageSize); - return historys.map((h) => GalleryHistoryModel.fromJson(jsonDecode(h.jsonBody))).toList(); + List historys = + await GalleryHistoryDao.selectByPageIndex(pageIndex, pageSize); + return historys + .map( + (h) => GalleryHistoryModel.fromJson(jsonDecode(h.jsonBody))) + .toList(); } Future> getLatest10000RawHistory() async { - return appDb.managers.galleryHistoryV2.orderBy((o) => o.lastReadTime.desc() & o.gid.desc()).limit(10000).get(); + return appDb.managers.galleryHistoryV2 + .orderBy((o) => o.lastReadTime.desc() & o.gid.desc()) + .limit(10000) + .get(); } Future record(GalleryHistoryModel gallery) async { - log.trace('Record history: ${gallery.galleryUrl.gid}'); + int gid = gallery.galleryUrl.gid; + log.trace('Record history: $gid'); + + _visitedGids.add(gid); + _unvisitedGids.remove(gid); + updateSafely(['$historyUpdateId::$gid']); try { await GalleryHistoryDao.replaceHistory( GalleryHistoryV2Data( - gid: gallery.galleryUrl.gid, + gid: gid, jsonBody: jsonEncode(gallery), lastReadTime: DateTime.now().toString(), ), @@ -50,12 +71,31 @@ class HistoryService with JHLifeCircleBeanErrorCatch implements JHLifeCircleBean } } + Future hasVisited(int gid) async { + if (_visitedGids.contains(gid)) { + return true; + } + if (_unvisitedGids.contains(gid)) { + return false; + } + + bool hasVisited = await GalleryHistoryDao.contains(gid); + if (hasVisited) { + _visitedGids.add(gid); + } else { + _unvisitedGids.add(gid); + } + return hasVisited; + } + Future batchRecord(List gallerys) async { log.trace('Batch record history, size: ${gallerys.length}'); try { for (List partition in gallerys.partition(2000)) { await GalleryHistoryDao.batchReplaceHistory(partition); + _visitedGids.addAll(partition.map((gallery) => gallery.gid)); + _unvisitedGids.removeAll(partition.map((gallery) => gallery.gid)); await Future.delayed(const Duration(milliseconds: 200)); } } on Exception catch (e) { @@ -66,11 +106,25 @@ class HistoryService with JHLifeCircleBeanErrorCatch implements JHLifeCircleBean Future delete(int gid) async { log.info('Delete history: $gid'); - return await GalleryHistoryDao.deleteHistory(gid) > 0; + bool deleted = await GalleryHistoryDao.deleteHistory(gid) > 0; + if (deleted) { + _visitedGids.remove(gid); + _unvisitedGids.add(gid); + updateSafely(['$historyUpdateId::$gid']); + } + return deleted; } Future deleteAll() async { log.info('Delete all historys'); - return await GalleryHistoryDao.deleteAllHistory() > 0; + bool deleted = await GalleryHistoryDao.deleteAllHistory() > 0; + if (deleted) { + Set visitedGids = {..._visitedGids}; + _visitedGids.clear(); + _unvisitedGids.clear(); + updateSafely(visitedGids.map((gid) => '$historyUpdateId::$gid').toList()); + updateSafely(); + } + return deleted; } } diff --git a/lib/src/service/local_config_service.dart b/lib/src/service/local_config_service.dart index 1e14e7977..37ead976f 100644 --- a/lib/src/service/local_config_service.dart +++ b/lib/src/service/local_config_service.dart @@ -19,10 +19,10 @@ class LocalConfig { Map toJson() { return { - "configKey": this.configKey.key, - "subConfigKey": this.subConfigKey, - "value": this.value, - "utime": this.utime, + "configKey": configKey.key, + "subConfigKey": subConfigKey, + "value": value, + "utime": utime, }; } @@ -94,4 +94,8 @@ class LocalConfigService with JHLifeCircleBeanErrorCatch implements JHLifeCircle .delete() .then((value) => value > 0); } + + Future deleteAll({required ConfigEnum configKey}) { + return appDb.managers.localConfig.filter((config) => config.configKey.equals(configKey.key)).delete(); + } } diff --git a/lib/src/service/local_gallery_service.dart b/lib/src/service/local_gallery_service.dart index 9bae6eed1..cbc4b1bc1 100644 --- a/lib/src/service/local_gallery_service.dart +++ b/lib/src/service/local_gallery_service.dart @@ -91,12 +91,14 @@ class LocalGalleryService extends GetxController with GridBasePageServiceMixin, dir.delete(recursive: true).catchError((e) { log.error('Delete local gallery error!', e); log.uploadError(e); + return dir; }); } else { for (File file in imageFiles) { file.delete().catchError((e) { log.error('Delete local gallery error!', e); log.uploadError(e); + return file; }); } } @@ -179,7 +181,7 @@ class LocalGalleryService extends GetxController with GridBasePageServiceMixin, onDone: () { if (result.isLegalGalleryDir) { images.sort(FileUtil.naturalCompareFile); - _initGalleryInfoInMemory(directory, images[0], parentPath); + _initGalleryInfoInMemory(directory, images[0], images.length, parentPath); } Future.wait(subFutures).then((_) { @@ -200,10 +202,11 @@ class LocalGalleryService extends GetxController with GridBasePageServiceMixin, return completer.future; } - void _initGalleryInfoInMemory(Directory galleryDir, File coverImage, String parentPath) { + void _initGalleryInfoInMemory(Directory galleryDir, File coverImage, int pageCount, String parentPath) { LocalGallery gallery = LocalGallery( title: basename(galleryDir.path), path: galleryDir.path, + pageCount: pageCount, cover: GalleryImage( url: '', path: relative(coverImage.path, from: pathService.getVisibleDir().path), @@ -219,9 +222,10 @@ class LocalGalleryService extends GetxController with GridBasePageServiceMixin, class LocalGallery { String title; String path; + int pageCount; GalleryImage cover; - LocalGallery({required this.title, required this.path, required this.cover}); + LocalGallery({required this.title, required this.path, required this.pageCount, required this.cover}); } class LocalGalleryParseResult { diff --git a/lib/src/service/read_progress_service.dart b/lib/src/service/read_progress_service.dart index 50b7aeafd..3889c33b3 100644 --- a/lib/src/service/read_progress_service.dart +++ b/lib/src/service/read_progress_service.dart @@ -14,6 +14,7 @@ class ReadProgressService extends GetxController with JHLifeCircleBeanErrorCatch /// Cache for read progress: gid -> readIndex final Map _progressCache = {}; + final Set _emptyProgressKeys = {}; @override Future doInitBean() async { @@ -25,25 +26,38 @@ class ReadProgressService extends GetxController with JHLifeCircleBeanErrorCatch /// Get read progress for a gallery with cache Future getReadProgress(int gid) async { + return await getReadProgressByKey(gid.toString()) ?? 0; + } + + Future getReadProgressByKey(String recordKey) async { // Return from cache if available - if (_progressCache.containsKey(gid.toString())) { - return _progressCache[gid.toString()]!; + if (_progressCache.containsKey(recordKey)) { + return _progressCache[recordKey]!; + } + if (_emptyProgressKeys.contains(recordKey)) { + return null; } // Read from storage final data = await localConfigService.read( configKey: ConfigEnum.readIndexRecord, - subConfigKey: gid.toString(), + subConfigKey: recordKey, ); - final progress = int.tryParse(data ?? '') ?? 0; - _progressCache[gid.toString()] = progress; + final progress = int.tryParse(data ?? ''); + if (progress == null) { + _emptyProgressKeys.add(recordKey); + return null; + } + _emptyProgressKeys.remove(recordKey); + _progressCache[recordKey] = progress; return progress; } /// Update read progress and notify listeners Future updateReadProgress(String recordKey, int index) async { _progressCache[recordKey] = index; + _emptyProgressKeys.remove(recordKey); await localConfigService.write( configKey: ConfigEnum.readIndexRecord, subConfigKey: recordKey, @@ -51,4 +65,21 @@ class ReadProgressService extends GetxController with JHLifeCircleBeanErrorCatch ); updateSafely(['$readProgressUpdateId::$recordKey']); } + + Future clearAllReadProgress() async { + final records = await localConfigService.readWithAllSubKeys(configKey: ConfigEnum.readIndexRecord); + final recordKeys = { + ..._progressCache.keys, + ...records.map((record) => record.subConfigKey), + }; + + _progressCache.clear(); + _emptyProgressKeys.clear(); + await localConfigService.deleteAll(configKey: ConfigEnum.readIndexRecord); + + if (recordKeys.isNotEmpty) { + updateSafely(recordKeys.map((key) => '$readProgressUpdateId::$key').toList()); + } + updateSafely(); + } } diff --git a/lib/src/widget/eh_gallery_collection.dart b/lib/src/widget/eh_gallery_collection.dart index 2810a1278..02acea670 100644 --- a/lib/src/widget/eh_gallery_collection.dart +++ b/lib/src/widget/eh_gallery_collection.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_list_view/flutter_list_view.dart'; -import 'package:get/get.dart'; import 'package:jhentai/src/config/ui_config.dart'; import 'package:jhentai/src/service/archive_download_service.dart'; import 'package:jhentai/src/service/gallery_download_service.dart'; @@ -14,6 +13,7 @@ import '../setting/style_setting.dart'; import 'eh_gallery_waterflow_card.dart'; /// Act as a List or WaterfallFlow according to Style Setting +// ignore: non_constant_identifier_names Widget EHGalleryCollection({ Key? key, required BuildContext context, @@ -24,6 +24,7 @@ Widget EHGalleryCollection({ CardCallback? handleLongPressCard, CardCallback? handleSecondaryTapCard, VoidCallback? handleLoadMore, + bool showVisitedBadge = false, }) { Widget _buildGalleryList() { /// use FlutterSliverList to [keepPosition] when insert items at top @@ -50,6 +51,7 @@ Widget EHGalleryCollection({ handleLongPressCard: handleLongPressCard == null ? null : (gallery) => handleLongPressCard(gallery), handleSecondaryTapCard: handleSecondaryTapCard == null ? null : (gallery) => handleSecondaryTapCard(gallery), withTags: listMode == ListMode.listWithTags || listMode == ListMode.flat, + showVisitedBadge: showVisitedBadge, ), ); }, @@ -90,6 +92,7 @@ Widget EHGalleryCollection({ handleTapCard: handleTapCard, handleLongPressCard: handleLongPressCard == null ? null : (gallery) => handleLongPressCard(gallery), handleSecondaryTapCard: handleSecondaryTapCard == null ? null : (gallery) => handleSecondaryTapCard(gallery), + showVisitedBadge: showVisitedBadge, ); }, childCount: gallerys.length, diff --git a/lib/src/widget/eh_gallery_list_card_.dart b/lib/src/widget/eh_gallery_list_card_.dart index 29a3c3017..8ae789574 100644 --- a/lib/src/widget/eh_gallery_list_card_.dart +++ b/lib/src/widget/eh_gallery_list_card_.dart @@ -18,7 +18,8 @@ import '../utils/date_util.dart'; import 'eh_image.dart'; import 'eh_tag.dart'; import 'eh_gallery_category_tag.dart'; -import '../service/read_progress_service.dart'; +import 'gallery_visited_badge.dart'; +import 'read_progress_badge.dart'; typedef CardCallback = FutureOr Function(Gallery gallery); @@ -30,6 +31,7 @@ class EHGalleryListCard extends StatelessWidget { final CardCallback? handleLongPressCard; final CardCallback? handleSecondaryTapCard; final bool withTags; + final bool showVisitedBadge; const EHGalleryListCard({ Key? key, @@ -38,6 +40,7 @@ class EHGalleryListCard extends StatelessWidget { required this.listMode, required this.handleTapCard, this.withTags = true, + this.showVisitedBadge = false, this.handleLongPressCard, this.handleSecondaryTapCard, }) : super(key: key); @@ -116,13 +119,37 @@ class EHGalleryListCard extends StatelessWidget { } Widget buildGalleryCardCover(BuildContext context) { - return EHImage( - galleryImage: gallery.cover, - containerColor: UIConfig.galleryCardBackGroundColor(context), - containerHeight: withTags ? UIConfig.galleryCardHeight : UIConfig.galleryCardHeightWithoutTags, - containerWidth: withTags ? UIConfig.galleryCardCoverWidth : UIConfig.galleryCardCoverWidthWithoutTags, - heroTag: gallery.blockedByLocalRules ? null : gallery.cover, - fit: BoxFit.fitWidth, + final height = withTags ? UIConfig.galleryCardHeight : UIConfig.galleryCardHeightWithoutTags; + final width = withTags ? UIConfig.galleryCardCoverWidth : UIConfig.galleryCardCoverWidthWithoutTags; + + return SizedBox( + width: width, + height: height, + child: Stack( + children: [ + EHImage( + galleryImage: gallery.cover, + containerColor: UIConfig.galleryCardBackGroundColor(context), + containerHeight: height, + containerWidth: width, + heroTag: gallery.blockedByLocalRules ? null : gallery.cover, + fit: BoxFit.fitWidth, + ), + Positioned( + top: 4, + left: 4, + child: showVisitedBadge ? GalleryVisitedBadge(gid: gallery.gid) : const SizedBox.shrink(), + ), + Positioned( + top: 4, + right: 4, + child: ReadProgressBadge( + recordKey: gallery.gid.toString(), + pageCount: gallery.pageCount, + ), + ), + ], + ), ); } @@ -202,7 +229,6 @@ class EHGalleryListCard extends StatelessWidget { children: [ EHGalleryCategoryTag(category: gallery.category), const Expanded(child: SizedBox()), - if (gallery.pageCount != null) _buildReadingProgress(context).marginOnly(right: 8), if (downloaded) _buildDownloadIcon(context).marginOnly(right: 4), if (gallery.isFavorite) _buildFavoriteIcon().marginOnly(right: 4), if (gallery.language != null) _buildLanguage(context).marginOnly(right: 4), @@ -237,43 +263,6 @@ class EHGalleryListCard extends StatelessWidget { ); } - Widget _buildReadingProgress(BuildContext context) { - return GetBuilder( - init: Get.find(), - id: '${ReadProgressService.readProgressUpdateId}::${gallery.gid}', - builder: (_) { - return FutureBuilder( - future: readProgressService.getReadProgress(gallery.gid), - builder: (context, snapshot) { - if (snapshot.connectionState != ConnectionState.done) { - return const SizedBox.shrink(); - } - - final readIndex = snapshot.data ?? 0; - - double progress = gallery.pageCount != null && gallery.pageCount! > 0 ? ((readIndex + 1) / gallery.pageCount!).clamp(0.0, 1.0) : 0.0; - - // Don't show indicator if no progress - if (readIndex == 0.0) { - return const SizedBox.shrink(); - } - - return SizedBox( - width: UIConfig.galleryCardReadProgressIndicatorSize, - height: UIConfig.galleryCardReadProgressIndicatorSize, - child: CircularProgressIndicator( - value: progress, - strokeWidth: 2, - backgroundColor: UIConfig.galleryCardTextColor(context).withOpacity(0.2), - valueColor: AlwaysStoppedAnimation(UIConfig.galleryCardTextColor(context)), - ), - ); - }, - ); - }, - ); - } - Widget _buildDownloadIcon(BuildContext context) => Icon(Icons.downloading, size: 11, color: UIConfig.galleryCardTextColor(context)); Widget _buildFavoriteIcon() => Icon(Icons.favorite, size: 11, color: UIConfig.favoriteTagColor[gallery.favoriteTagIndex!]); diff --git a/lib/src/widget/eh_gallery_waterflow_card.dart b/lib/src/widget/eh_gallery_waterflow_card.dart index aab9f5fff..9d7a92e58 100644 --- a/lib/src/widget/eh_gallery_waterflow_card.dart +++ b/lib/src/widget/eh_gallery_waterflow_card.dart @@ -18,6 +18,8 @@ import '../model/gallery_tag.dart'; import 'eh_gallery_category_tag.dart'; import 'eh_gallery_list_card_.dart'; import 'eh_image.dart'; +import 'gallery_visited_badge.dart'; +import 'read_progress_badge.dart'; class EHGalleryWaterFlowCard extends StatelessWidget { final Gallery gallery; @@ -26,6 +28,7 @@ class EHGalleryWaterFlowCard extends StatelessWidget { final CardCallback handleTapCard; final CardCallback? handleLongPressCard; final CardCallback? handleSecondaryTapCard; + final bool showVisitedBadge; const EHGalleryWaterFlowCard({ Key? key, @@ -33,6 +36,7 @@ class EHGalleryWaterFlowCard extends StatelessWidget { required this.downloaded, required this.listMode, required this.handleTapCard, + this.showVisitedBadge = false, this.handleLongPressCard, this.handleSecondaryTapCard, }) : super(key: key); @@ -154,17 +158,38 @@ class EHGalleryWaterFlowCard extends StatelessWidget { ), ); - return EHImage( - galleryImage: gallery.cover, - containerHeight: fittedSizes.destination.height, - containerWidth: fittedSizes.destination.width, - containerColor: UIConfig.waterFallFlowCardBackGroundColor(context), - heroTag: gallery.blockedByLocalRules ? null : gallery.cover, - borderRadius: BorderRadius.only( - topLeft: Radius.circular(listMode == ListMode.waterfallFlowBig ? 12 : 8), - topRight: Radius.circular(listMode == ListMode.waterfallFlowBig ? 12 : 8), - bottomLeft: Radius.circular(listMode == ListMode.waterfallFlowBig || listMode == ListMode.waterfallFlowMedium ? 0 : 8), - bottomRight: Radius.circular(listMode == ListMode.waterfallFlowBig || listMode == ListMode.waterfallFlowMedium ? 0 : 8), + return SizedBox( + width: fittedSizes.destination.width, + height: fittedSizes.destination.height, + child: Stack( + children: [ + EHImage( + galleryImage: gallery.cover, + containerHeight: fittedSizes.destination.height, + containerWidth: fittedSizes.destination.width, + containerColor: UIConfig.waterFallFlowCardBackGroundColor(context), + heroTag: gallery.blockedByLocalRules ? null : gallery.cover, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(listMode == ListMode.waterfallFlowBig ? 12 : 8), + topRight: Radius.circular(listMode == ListMode.waterfallFlowBig ? 12 : 8), + bottomLeft: Radius.circular(listMode == ListMode.waterfallFlowBig || listMode == ListMode.waterfallFlowMedium ? 0 : 8), + bottomRight: Radius.circular(listMode == ListMode.waterfallFlowBig || listMode == ListMode.waterfallFlowMedium ? 0 : 8), + ), + ), + Positioned( + top: 4, + left: 4, + child: showVisitedBadge ? GalleryVisitedBadge(gid: gallery.gid) : const SizedBox.shrink(), + ), + Positioned( + top: 4, + right: 4, + child: ReadProgressBadge( + recordKey: gallery.gid.toString(), + pageCount: gallery.pageCount, + ), + ), + ], ), ); }, diff --git a/lib/src/widget/gallery_visited_badge.dart b/lib/src/widget/gallery_visited_badge.dart new file mode 100644 index 000000000..11471be90 --- /dev/null +++ b/lib/src/widget/gallery_visited_badge.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import '../service/history_service.dart'; + +class GalleryVisitedBadge extends StatelessWidget { + final int gid; + + const GalleryVisitedBadge({ + Key? key, + required this.gid, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return GetBuilder( + init: Get.find(), + id: '${HistoryService.historyUpdateId}::$gid', + builder: (_) { + return FutureBuilder( + future: historyService.hasVisited(gid), + builder: (context, snapshot) { + if (snapshot.connectionState != ConnectionState.done || + snapshot.data != true) { + return const SizedBox.shrink(); + } + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2), + decoration: BoxDecoration( + color: Colors.green.shade700.withValues(alpha: 0.88), + borderRadius: BorderRadius.circular(4), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.visibility, color: Colors.white, size: 10), + const SizedBox(width: 2), + Text( + 'opened'.tr, + style: const TextStyle( + color: Colors.white, + fontSize: 10, + fontWeight: FontWeight.w600, + height: 1.1, + ), + ), + ], + ), + ); + }, + ); + }, + ); + } +} diff --git a/lib/src/widget/read_progress_badge.dart b/lib/src/widget/read_progress_badge.dart new file mode 100644 index 000000000..24ba97500 --- /dev/null +++ b/lib/src/widget/read_progress_badge.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import '../service/read_progress_service.dart'; + +class ReadProgressBadge extends StatelessWidget { + final String recordKey; + final int? pageCount; + + const ReadProgressBadge({ + Key? key, + required this.recordKey, + required this.pageCount, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + if (pageCount == null || pageCount! <= 0) { + return const SizedBox.shrink(); + } + + return GetBuilder( + init: Get.find(), + id: '${ReadProgressService.readProgressUpdateId}::$recordKey', + builder: (_) { + return FutureBuilder( + future: readProgressService.getReadProgressByKey(recordKey), + builder: (context, snapshot) { + if (snapshot.connectionState != ConnectionState.done) { + return const SizedBox.shrink(); + } + + if (snapshot.data == null || snapshot.data! <= 0) { + return _buildStatusBadge(Icons.radio_button_unchecked, Colors.redAccent); + } + + final readIndex = snapshot.data!.clamp(0, pageCount! - 1); + final progress = ((readIndex + 1) / pageCount!).clamp(0.0, 1.0); + if (progress > 0.9) { + return _buildStatusBadge(Icons.check_circle, Colors.greenAccent); + } + + final percent = (progress * 100).ceil(); + + return Container( + constraints: const BoxConstraints(minWidth: 30), + padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2), + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.68), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + '$percent%', + textAlign: TextAlign.center, + style: const TextStyle( + color: Colors.white, + fontSize: 10, + fontWeight: FontWeight.w600, + height: 1.1, + ), + ), + ); + }, + ); + }, + ); + } + + Widget _buildStatusBadge(IconData icon, Color color) { + return Container( + width: 22, + height: 22, + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.68), + borderRadius: BorderRadius.circular(4), + ), + alignment: Alignment.center, + child: Icon(icon, color: color, size: 16), + ); + } +}