From 1f325bbb444615939d34b1e7d84fbb1354ea178e Mon Sep 17 00:00:00 2001 From: Wen_He Date: Mon, 16 Mar 2026 19:05:04 +0800 Subject: [PATCH 01/16] feat: add push archive to aria2 --- lib/src/l18n/en_US.dart | 16 ++ lib/src/l18n/zh_CN.dart | 16 ++ lib/src/pages/details/details_page.dart | 11 ++ lib/src/pages/details/details_page_logic.dart | 71 +++++--- .../archive_download_page_logic_mixin.dart | 7 + .../download/setting_download_page.dart | 139 ++++++++++++++++ lib/src/service/archive_download_service.dart | 154 ++++++++++++++++++ lib/src/service/aria2_service.dart | 117 +++++++++++++ lib/src/setting/download_setting.dart | 45 +++++ 9 files changed, 555 insertions(+), 21 deletions(-) create mode 100644 lib/src/service/aria2_service.dart diff --git a/lib/src/l18n/en_US.dart b/lib/src/l18n/en_US.dart index 88ad96f47..14cdd5499 100644 --- a/lib/src/l18n/en_US.dart +++ b/lib/src/l18n/en_US.dart @@ -718,6 +718,22 @@ class en_US { 'brokenDownloadPathHint': 'Seems your download path is broken, download function may be ineffective', 'brokenExtraScanPathHint': 'Seems your default local gallery path is broken, local gallery may be not recognized', 'useJH2UpdateGallery': 'Use JH server to accelerate gallery updates', + 'enableAria2Push': 'Enable Push Archive to aria2', + 'enableAria2PushHint': 'Push archive link to aria2 instead of downloading in app', + 'aria2Settings': 'aria2 Settings', + 'aria2RpcUrl': 'RPC URL', + 'aria2RpcUrlHint': 'http://127.0.0.1:6800/jsonrpc', + 'aria2Secret': 'RPC Secret', + 'aria2DownloadDir': 'aria2 Download Directory', + 'aria2DownloadDirHint': 'Optional, use aria2 default when empty', + 'aria2ConnectTimeout': 'RPC Timeout', + 'testConnection': 'Test Connection', + 'aria2ConnectionSucceeded': 'aria2 connection succeeded', + 'aria2ConnectionFailed': 'aria2 connection failed', + 'pushArchiveToAria2': 'Push Archive to aria2', + 'aria2PushSuccess': 'Pushed to aria2: %s', + 'aria2PushFailed': 'Push to aria2 failed', + 'aria2NotConfiguredHint': 'Please enable and configure aria2 in Download Settings', /// archive bot settings 'archiveBotSettings': 'Archive Bot Settings', diff --git a/lib/src/l18n/zh_CN.dart b/lib/src/l18n/zh_CN.dart index aff6c8b4b..e0874ea16 100644 --- a/lib/src/l18n/zh_CN.dart +++ b/lib/src/l18n/zh_CN.dart @@ -723,6 +723,22 @@ favnote:匹配收藏备注 'brokenDownloadPathHint': '似乎你的下载路径已经损坏,下载功能可能失效', 'brokenExtraScanPathHint': '似乎你的默认本地画廊路径已经损坏,本地画廊可能无法被识别', 'useJH2UpdateGallery': '使用JH服务器加速画廊更新', + 'enableAria2Push': '启用推送归档至aria2', + 'enableAria2PushHint': '将归档下载链接推送到aria2,不在应用内下载', + 'aria2Settings': 'aria2设置', + 'aria2RpcUrl': 'RPC地址', + 'aria2RpcUrlHint': 'http://127.0.0.1:6800/jsonrpc', + 'aria2Secret': 'RPC密钥', + 'aria2DownloadDir': 'aria2下载目录', + 'aria2DownloadDirHint': '可选,留空则使用aria2默认目录', + 'aria2ConnectTimeout': 'RPC超时', + 'testConnection': '测试连接', + 'aria2ConnectionSucceeded': 'aria2连接成功', + 'aria2ConnectionFailed': 'aria2连接失败', + 'pushArchiveToAria2': '推送归档至aria2', + 'aria2PushSuccess': '已推送到aria2:%s', + 'aria2PushFailed': '推送到aria2失败', + 'aria2NotConfiguredHint': '请先在下载设置中启用并配置aria2', /// archive bot settings 'archiveBotSettings': '归档机器人设置', diff --git a/lib/src/pages/details/details_page.dart b/lib/src/pages/details/details_page.dart index 77b32e66d..63339e705 100644 --- a/lib/src/pages/details/details_page.dart +++ b/lib/src/pages/details/details_page.dart @@ -157,6 +157,14 @@ class DetailsPage extends StatelessWidget with Scroll2TopPageMixin { children: [Text('block'.tr), const Icon(Icons.block)], ), ), + if (state.galleryDetails != null) + PopupMenuItem( + value: 6, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [Text('pushArchiveToAria2'.tr), const Icon(Icons.send_and_archive_outlined)], + ), + ), ]; }, onSelected: (value) { @@ -182,6 +190,9 @@ class DetailsPage extends StatelessWidget with Scroll2TopPageMixin { if (value == 5) { logic.blockGallery(); } + if (value == 6) { + logic.handlePushArchiveToAria2(); + } }, ); }, diff --git a/lib/src/pages/details/details_page_logic.dart b/lib/src/pages/details/details_page_logic.dart index 0bf27a7b7..f548c2baa 100644 --- a/lib/src/pages/details/details_page_logic.dart +++ b/lib/src/pages/details/details_page_logic.dart @@ -651,27 +651,7 @@ class DetailsPageLogic extends GetxController with LoginRequiredMixin, Scroll2To return; } - ArchiveDownloadedData archive = ArchiveDownloadedData( - gid: state.galleryDetails!.galleryUrl.gid, - token: state.galleryDetails!.galleryUrl.token, - title: mainTitleText, - category: state.galleryDetails!.category, - pageCount: state.galleryDetails!.pageCount, - galleryUrl: state.galleryDetails!.galleryUrl.url, - uploader: state.galleryDetails!.uploader, - size: result.size, - coverUrl: state.galleryDetails!.cover.url, - publishTime: state.galleryDetails!.publishTime, - archiveStatusCode: ArchiveStatus.unlocking.code, - archivePageUrl: state.galleryDetails!.archivePageUrl, - isOriginal: result.isOriginal, - insertTime: DateTime.now().toString(), - sortOrder: 0, - groupName: result.group, - tags: state.galleryDetails != null ? tagMap2TagString(state.galleryDetails!.tags) : tagMap2TagString(state.gallery!.tags), - tagRefreshTime: DateTime.now().toString(), - parseSource: result.useBot ? ArchiveParseSource.bot.code : ArchiveParseSource.official.code, - ); + ArchiveDownloadedData archive = _buildArchiveTaskFromDialogResult(result); archiveDownloadService.downloadArchive(archive); updateGlobalGalleryStatus(); @@ -722,6 +702,55 @@ class DetailsPageLogic extends GetxController with LoginRequiredMixin, Scroll2To } } + Future handlePushArchiveToAria2() async { + if (state.galleryDetails == null) { + return; + } + if (!userSetting.hasLoggedIn()) { + showLoginToast(); + return; + } + + ({bool useBot, bool isOriginal, int size, String group})? result = await Get.dialog( + EHArchiveDialog( + title: 'chooseArchive'.tr, + archivePageUrl: state.galleryDetails!.archivePageUrl, + currentGroup: downloadSetting.defaultArchiveGroup.value, + candidates: archiveDownloadService.allGroups, + ), + ); + if (result == null) { + return; + } + + ArchiveDownloadedData archive = _buildArchiveTaskFromDialogResult(result); + await archiveDownloadService.pushArchiveToAria2(archive); + } + + ArchiveDownloadedData _buildArchiveTaskFromDialogResult(({bool useBot, bool isOriginal, int size, String group}) result) { + return ArchiveDownloadedData( + gid: state.galleryDetails!.galleryUrl.gid, + token: state.galleryDetails!.galleryUrl.token, + title: mainTitleText, + category: state.galleryDetails!.category, + pageCount: state.galleryDetails!.pageCount, + galleryUrl: state.galleryDetails!.galleryUrl.url, + uploader: state.galleryDetails!.uploader, + size: result.size, + coverUrl: state.galleryDetails!.cover.url, + publishTime: state.galleryDetails!.publishTime, + archiveStatusCode: ArchiveStatus.unlocking.code, + archivePageUrl: state.galleryDetails!.archivePageUrl, + isOriginal: result.isOriginal, + insertTime: DateTime.now().toString(), + sortOrder: 0, + groupName: result.group, + tags: state.galleryDetails != null ? tagMap2TagString(state.galleryDetails!.tags) : tagMap2TagString(state.gallery!.tags), + tagRefreshTime: DateTime.now().toString(), + parseSource: result.useBot ? ArchiveParseSource.bot.code : ArchiveParseSource.official.code, + ); + } + Future handleTapHH() async { if (!userSetting.hasLoggedIn()) { showLoginToast(); diff --git a/lib/src/pages/download/mixin/archive/archive_download_page_logic_mixin.dart b/lib/src/pages/download/mixin/archive/archive_download_page_logic_mixin.dart index 2c77051cb..d3f5c4274 100644 --- a/lib/src/pages/download/mixin/archive/archive_download_page_logic_mixin.dart +++ b/lib/src/pages/download/mixin/archive/archive_download_page_logic_mixin.dart @@ -228,6 +228,13 @@ mixin ArchiveDownloadPageLogicMixin on GetxController changeParseSource(archive.gid, ArchiveParseSource.bot); }, ), + CupertinoActionSheetAction( + child: Text('pushArchiveToAria2'.tr), + onPressed: () { + backRoute(); + archiveDownloadService.pushArchiveToAria2(archive); + }, + ), CupertinoActionSheetAction( child: Text('changeGroup'.tr), onPressed: () { diff --git a/lib/src/pages/setting/download/setting_download_page.dart b/lib/src/pages/setting/download/setting_download_page.dart index 72ff5d54a..0e0b16170 100644 --- a/lib/src/pages/setting/download/setting_download_page.dart +++ b/lib/src/pages/setting/download/setting_download_page.dart @@ -18,10 +18,12 @@ import 'package:path/path.dart'; import '../../../routes/routes.dart'; import '../../../service/archive_download_service.dart'; +import '../../../service/aria2_service.dart'; import '../../../service/gallery_download_service.dart'; import '../../../service/log.dart'; import '../../../utils/permission_util.dart'; import '../../../utils/route_util.dart'; +import '../../../utils/snack_util.dart'; import '../../../widget/eh_download_dialog.dart'; class SettingDownloadPage extends StatefulWidget { @@ -61,6 +63,8 @@ class _SettingDownloadPageState extends State { _buildDefaultGalleryGroup(context), _buildDefaultArchiveGroup(context), _buildArchiveBotSettings(), + _buildEnableAria2Push(), + _buildAria2Setting(context), _buildDownloadConcurrency(), _buildSpeedLimit(context), _buildDownloadAllGallerysOfSamePriority(), @@ -207,6 +211,37 @@ class _SettingDownloadPageState extends State { ); } + Widget _buildEnableAria2Push() { + return SwitchListTile( + title: Text('enableAria2Push'.tr), + subtitle: Text('enableAria2PushHint'.tr), + value: downloadSetting.enableAria2Push.value, + onChanged: downloadSetting.saveEnableAria2Push, + ); + } + + Widget _buildAria2Setting(BuildContext context) { + String rpcUrl = downloadSetting.aria2RpcUrl.value.trim().isEmpty ? 'aria2RpcUrlHint'.tr : downloadSetting.aria2RpcUrl.value; + String downloadDir = downloadSetting.aria2DownloadDir.value.trim().isEmpty ? 'default'.tr : downloadSetting.aria2DownloadDir.value; + + return ListTile( + title: Text('aria2Settings'.tr), + subtitle: Text('${'aria2RpcUrl'.tr}: ${rpcUrl.breakWord}\n${'aria2DownloadDir'.tr}: ${downloadDir.breakWord}'), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + onPressed: _testAria2Connection, + icon: const Icon(Icons.wifi_tethering), + tooltip: 'testConnection'.tr, + ), + const Icon(Icons.keyboard_arrow_right), + ], + ), + onTap: () => _showAria2SettingsDialog(context), + ); + } + Widget _buildSpeedLimit(BuildContext context) { return ListTile( title: Text('speedLimit'.tr), @@ -450,4 +485,108 @@ class _SettingDownloadPageState extends State { isShort: false, ); } + + Future _showAria2SettingsDialog(BuildContext context) async { + TextEditingController rpcUrlController = TextEditingController(text: downloadSetting.aria2RpcUrl.value); + TextEditingController secretController = TextEditingController(text: downloadSetting.aria2Secret.value); + TextEditingController dirController = TextEditingController(text: downloadSetting.aria2DownloadDir.value); + int timeout = downloadSetting.aria2ConnectTimeout.value; + bool obscureSecret = true; + + bool? result = await showDialog( + context: context, + builder: (_) { + return StatefulBuilder( + builder: (context, setState) { + return AlertDialog( + title: Text('aria2Settings'.tr), + content: SizedBox( + width: 520, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: rpcUrlController, + decoration: InputDecoration( + labelText: 'aria2RpcUrl'.tr, + hintText: 'aria2RpcUrlHint'.tr, + border: const OutlineInputBorder(), + ), + ), + const SizedBox(height: 12), + TextField( + controller: secretController, + obscureText: obscureSecret, + decoration: InputDecoration( + labelText: 'aria2Secret'.tr, + border: const OutlineInputBorder(), + suffixIcon: IconButton( + onPressed: () => setState(() => obscureSecret = !obscureSecret), + icon: Icon(obscureSecret ? Icons.visibility_off : Icons.visibility), + ), + ), + ), + const SizedBox(height: 12), + TextField( + controller: dirController, + decoration: InputDecoration( + labelText: 'aria2DownloadDir'.tr, + hintText: 'aria2DownloadDirHint'.tr, + border: const OutlineInputBorder(), + ), + ), + const SizedBox(height: 12), + DropdownButtonFormField( + value: timeout, + decoration: InputDecoration( + labelText: 'aria2ConnectTimeout'.tr, + border: const OutlineInputBorder(), + ), + items: const [ + DropdownMenuItem(value: 3000, child: Text('3000ms')), + DropdownMenuItem(value: 5000, child: Text('5000ms')), + DropdownMenuItem(value: 8000, child: Text('8000ms')), + DropdownMenuItem(value: 12000, child: Text('12000ms')), + DropdownMenuItem(value: 20000, child: Text('20000ms')), + ], + onChanged: (value) { + if (value != null) { + timeout = value; + } + }, + ), + ], + ), + ), + actions: [ + TextButton(onPressed: () => Navigator.of(context).pop(false), child: Text('cancel'.tr)), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: Text('save'.tr), + ), + ], + ); + }, + ); + }, + ); + + if (result != true) { + return; + } + + await downloadSetting.saveAria2RpcUrl(rpcUrlController.text.trim()); + await downloadSetting.saveAria2Secret(secretController.text.trim()); + await downloadSetting.saveAria2DownloadDir(dirController.text.trim()); + await downloadSetting.saveAria2ConnectTimeout(timeout); + } + + Future _testAria2Connection() async { + try { + await aria2Service.testConnection(); + toast('aria2ConnectionSucceeded'.tr); + } catch (e) { + snack('aria2ConnectionFailed'.tr, e.toString(), isShort: true); + } + } } diff --git a/lib/src/service/archive_download_service.dart b/lib/src/service/archive_download_service.dart index 0655a0a77..ddad629d0 100644 --- a/lib/src/service/archive_download_service.dart +++ b/lib/src/service/archive_download_service.dart @@ -26,6 +26,7 @@ import 'package:jhentai/src/setting/download_setting.dart'; import 'package:jhentai/src/setting/network_setting.dart'; import 'package:jhentai/src/service/path_service.dart'; import 'package:jhentai/src/utils/archive_bot_response_parser.dart'; +import 'package:jhentai/src/utils/cookie_util.dart'; import 'package:jhentai/src/utils/speed_computer.dart'; import 'package:jhentai/src/utils/eh_spider_parser.dart'; import 'package:logger/logger.dart'; @@ -41,9 +42,11 @@ import '../model/gallery_image.dart'; import '../pages/download/grid/mixin/grid_download_page_service_mixin.dart'; import '../utils/archive_util.dart'; import '../utils/file_util.dart'; +import '../utils/toast_util.dart'; import 'jh_service.dart'; import 'log.dart'; import '../utils/snack_util.dart'; +import 'aria2_service.dart'; import 'gallery_download_service.dart'; ArchiveDownloadService archiveDownloadService = ArchiveDownloadService(); @@ -396,6 +399,157 @@ class ArchiveDownloadService extends GetxController with GridBasePageServiceMixi update(['$archiveStatusId::$gid']); } + Future pushArchiveToAria2(ArchiveDownloadedData archive, {bool reParse = false}) async { + if (!aria2Service.isReady) { + snack('aria2PushFailed'.tr, 'aria2NotConfiguredHint'.tr); + return; + } + + String? resolvedDownloadUrl; + String? referer; + + ArchiveDownloadInfo? archiveDownloadInfo = archiveDownloadInfos[archive.gid]; + if (!reParse && archiveDownloadInfo?.downloadUrl != null) { + resolvedDownloadUrl = archiveDownloadInfo!.downloadUrl; + referer = archiveDownloadInfo.downloadPageUrl; + } else { + ({String downloadUrl, String? referer})? result = await _resolveArchiveDownloadUrlForAria2(archive, reParse: reParse); + if (result == null) { + return; + } + resolvedDownloadUrl = result.downloadUrl; + referer = result.referer; + } + + if (resolvedDownloadUrl == null) { + snack('aria2PushFailed'.tr, 'archiveError'.tr); + return; + } + + List headers = []; + String cookies = CookieUtil.parse2String(ehRequest.cookies); + if (cookies.isNotEmpty) { + headers.add('Cookie: $cookies'); + } + if (referer != null && referer.isNotEmpty) { + headers.add('Referer: $referer'); + } + + try { + await aria2Service.addUri( + uri: resolvedDownloadUrl, + out: 'ArchiveV2 - ${archive.gid} - ${_computeArchiveTitle(archive.title)}.zip', + dir: downloadSetting.aria2DownloadDir.value.trim(), + headers: headers, + ); + toast('aria2PushSuccess'.trArgs([archive.title]), isCenter: false); + } on DioException catch (e) { + snack('aria2PushFailed'.tr, e.message ?? e.toString(), isShort: true); + } catch (e) { + snack('aria2PushFailed'.tr, e.toString(), isShort: true); + } + } + + Future<({String downloadUrl, String? referer})?> _resolveArchiveDownloadUrlForAria2( + ArchiveDownloadedData archive, { + bool reParse = false, + }) async { + try { + if (archive.parseSource == ArchiveParseSource.official.code) { + String? downloadPageUrl = archiveDownloadInfos[archive.gid]?.downloadPageUrl; + if (downloadPageUrl == null || downloadPageUrl.isEmpty) { + downloadPageUrl = await _resolveOfficialDownloadPageUrl(archive); + } + if (downloadPageUrl == null) { + return null; + } + + String downloadPath = await retry( + () => ehRequest.get( + url: downloadPageUrl!, + parser: EHSpiderParser.downloadArchivePage2DownloadUrl, + ), + retryIf: (e) => e is DioException && e.type != DioExceptionType.cancel, + maxAttempts: _maxRetryTimes, + ); + + String downloadUrl = _ensureStartQueryParameter('https://${Uri.parse(downloadPageUrl).host}$downloadPath'); + return (downloadUrl: downloadUrl, referer: downloadPageUrl); + } + + if (!archiveBotSetting.isReady) { + snack('aria2PushFailed'.tr, 'pauseDownloadByInvalidArchiveBotKey'.tr); + return null; + } + + ArchiveBotResponse response = await retry( + () => archiveBotRequest.requestResolve( + apiAddress: archiveBotSetting.apiAddress.value, + apiKey: archiveBotSetting.apiKey.value!, + gid: archive.gid, + token: archive.token, + reParse: reParse, + parser: ArchiveBotResponseParser.commonParse, + ), + retryIf: (e) => e is DioException && e.type != DioExceptionType.cancel, + maxAttempts: _maxRetryTimes, + ); + + if (!response.isSuccess) { + snack('aria2PushFailed'.tr, response.errorMessage, isShort: true); + return null; + } + + ArchiveResolveVO archiveResolveVO = ArchiveResolveVO.fromResponse(response.data); + return (downloadUrl: _ensureStartQueryParameter(archiveResolveVO.url), referer: null); + } on DioException catch (e) { + snack('aria2PushFailed'.tr, e.error?.toString() ?? e.message ?? e.toString(), isShort: true); + return null; + } on EHSiteException catch (e) { + snack('aria2PushFailed'.tr, e.message, isShort: true); + return null; + } catch (e) { + snack('aria2PushFailed'.tr, e.toString(), isShort: true); + return null; + } + } + + Future _resolveOfficialDownloadPageUrl(ArchiveDownloadedData archive) async { + for (int i = 0; i < 12; i++) { + ArchiveUnlockResult result = await retry( + () => ehRequest.requestUnlockArchive( + url: archive.archivePageUrl.replaceFirst('--', '-'), + isOriginal: archive.isOriginal, + parser: EHSpiderParser.unlockArchivePage2DownloadArchivePageUrl, + ), + retryIf: (e) => e is DioException && e.type != DioExceptionType.cancel, + maxAttempts: _maxRetryTimes, + ); + + if (!result.success) { + snack('aria2PushFailed'.tr, result.msg, isShort: true); + return null; + } + + if (result.url != null && result.url!.isNotEmpty) { + return result.url!; + } + + await Future.delayed(const Duration(milliseconds: 1000)); + } + + snack('aria2PushFailed'.tr, 'archiveError'.tr, isShort: true); + return null; + } + + String _ensureStartQueryParameter(String url) { + Uri uri = Uri.parse(url); + Map queryParameters = Map.from(uri.queryParameters); + queryParameters.remove('autostart'); + queryParameters.putIfAbsent('start', () => '1'); + return uri.replace(queryParameters: queryParameters).toString(); + } + Future batchUpdateArchiveInDatabase(List archives) async { await appDb.transaction(() async { for (ArchiveDownloadedData archive in archives) { diff --git a/lib/src/service/aria2_service.dart b/lib/src/service/aria2_service.dart new file mode 100644 index 000000000..370f164d8 --- /dev/null +++ b/lib/src/service/aria2_service.dart @@ -0,0 +1,117 @@ +import 'package:dio/dio.dart'; +import 'package:get/get_utils/get_utils.dart'; +import 'package:jhentai/src/setting/download_setting.dart'; + +import 'log.dart'; + +Aria2Service aria2Service = Aria2Service(); + +class Aria2Service { + bool get isReady => downloadSetting.enableAria2Push.value && downloadSetting.aria2RpcUrl.value.trim().isNotEmpty; + + String _normalizedRpcUrl() { + String url = downloadSetting.aria2RpcUrl.value.trim(); + if (url.isEmpty) { + return url; + } + if (!url.startsWith('http://') && !url.startsWith('https://')) { + url = 'http://$url'; + } + if (!url.endsWith('/jsonrpc')) { + if (url.endsWith('/')) { + url = '${url}jsonrpc'; + } else { + url = '$url/jsonrpc'; + } + } + return url; + } + + Future addUri({ + required String uri, + String? out, + String? dir, + List? headers, + }) async { + Dio dio = Dio(BaseOptions( + connectTimeout: Duration(milliseconds: downloadSetting.aria2ConnectTimeout.value), + receiveTimeout: Duration(milliseconds: downloadSetting.aria2ConnectTimeout.value), + )); + + List params = []; + if (downloadSetting.aria2Secret.value.trim().isNotEmpty) { + params.add('token:${downloadSetting.aria2Secret.value.trim()}'); + } + params.add([uri]); + + Map options = {}; + if (out != null && out.isNotEmpty) { + options['out'] = out; + } + if (dir != null && dir.isNotEmpty) { + options['dir'] = dir; + } + if (headers != null && headers.isNotEmpty) { + options['header'] = headers; + } + if (options.isNotEmpty) { + params.add(options); + } + + Response response = await dio.post( + _normalizedRpcUrl(), + data: { + 'jsonrpc': '2.0', + 'id': DateTime.now().millisecondsSinceEpoch.toString(), + 'method': 'aria2.addUri', + 'params': params, + }, + ); + + if (response.data is! Map) { + throw Exception('Invalid aria2 response'); + } + if (response.data['error'] != null) { + String error = response.data['error'] is Map ? (response.data['error']['message']?.toString() ?? 'Unknown aria2 error') : 'Unknown aria2 error'; + throw Exception(error); + } + + String? gid = response.data['result']?.toString(); + if (gid.isBlank == true) { + throw Exception('Empty aria2 task id'); + } + + log.info('Pushed to aria2, gid: $gid'); + return gid!; + } + + Future testConnection() async { + Dio dio = Dio(BaseOptions( + connectTimeout: Duration(milliseconds: downloadSetting.aria2ConnectTimeout.value), + receiveTimeout: Duration(milliseconds: downloadSetting.aria2ConnectTimeout.value), + )); + + List params = []; + if (downloadSetting.aria2Secret.value.trim().isNotEmpty) { + params.add('token:${downloadSetting.aria2Secret.value.trim()}'); + } + + Response response = await dio.post( + _normalizedRpcUrl(), + data: { + 'jsonrpc': '2.0', + 'id': DateTime.now().millisecondsSinceEpoch.toString(), + 'method': 'aria2.getVersion', + 'params': params, + }, + ); + + if (response.data is! Map) { + throw Exception('Invalid aria2 response'); + } + if (response.data['error'] != null) { + String error = response.data['error'] is Map ? (response.data['error']['message']?.toString() ?? 'Unknown aria2 error') : 'Unknown aria2 error'; + throw Exception(error); + } + } +} diff --git a/lib/src/setting/download_setting.dart b/lib/src/setting/download_setting.dart index 87516f95c..d0c66ad01 100644 --- a/lib/src/setting/download_setting.dart +++ b/lib/src/setting/download_setting.dart @@ -32,6 +32,11 @@ class DownloadSetting with JHLifeCircleBeanWithConfigStorage implements JHLifeCi RxBool manageArchiveDownloadConcurrency = true.obs; RxBool deleteArchiveFileAfterDownload = true.obs; RxBool restoreTasksAutomatically = false.obs; + RxBool enableAria2Push = false.obs; + RxString aria2RpcUrl = 'http://127.0.0.1:6800/jsonrpc'.obs; + RxString aria2Secret = ''.obs; + RxString aria2DownloadDir = ''.obs; + RxInt aria2ConnectTimeout = 8000.obs; @override ConfigEnum get configEnum => ConfigEnum.downloadSetting; @@ -63,6 +68,11 @@ class DownloadSetting with JHLifeCircleBeanWithConfigStorage implements JHLifeCi manageArchiveDownloadConcurrency.value = map['manageArchiveDownloadConcurrency'] ?? manageArchiveDownloadConcurrency.value; deleteArchiveFileAfterDownload.value = map['deleteArchiveFileAfterDownload'] ?? deleteArchiveFileAfterDownload.value; restoreTasksAutomatically.value = map['restoreTasksAutomatically'] ?? restoreTasksAutomatically.value; + enableAria2Push.value = map['enableAria2Push'] ?? enableAria2Push.value; + aria2RpcUrl.value = map['aria2RpcUrl'] ?? aria2RpcUrl.value; + aria2Secret.value = map['aria2Secret'] ?? aria2Secret.value; + aria2DownloadDir.value = map['aria2DownloadDir'] ?? aria2DownloadDir.value; + aria2ConnectTimeout.value = map['aria2ConnectTimeout'] ?? aria2ConnectTimeout.value; } @override @@ -83,6 +93,11 @@ class DownloadSetting with JHLifeCircleBeanWithConfigStorage implements JHLifeCi 'manageArchiveDownloadConcurrency': manageArchiveDownloadConcurrency.value, 'deleteArchiveFileAfterDownload': deleteArchiveFileAfterDownload.value, 'restoreTasksAutomatically': restoreTasksAutomatically.value, + 'enableAria2Push': enableAria2Push.value, + 'aria2RpcUrl': aria2RpcUrl.value, + 'aria2Secret': aria2Secret.value, + 'aria2DownloadDir': aria2DownloadDir.value, + 'aria2ConnectTimeout': aria2ConnectTimeout.value, }); } @@ -198,6 +213,36 @@ class DownloadSetting with JHLifeCircleBeanWithConfigStorage implements JHLifeCi await saveBeanConfig(); } + Future saveEnableAria2Push(bool value) async { + log.debug('saveEnableAria2Push:$value'); + enableAria2Push.value = value; + await saveBeanConfig(); + } + + Future saveAria2RpcUrl(String value) async { + log.debug('saveAria2RpcUrl:$value'); + aria2RpcUrl.value = value; + await saveBeanConfig(); + } + + Future saveAria2Secret(String value) async { + log.debug('saveAria2Secret:${value.isEmpty ? '' : ''}'); + aria2Secret.value = value; + await saveBeanConfig(); + } + + Future saveAria2DownloadDir(String value) async { + log.debug('saveAria2DownloadDir:$value'); + aria2DownloadDir.value = value; + await saveBeanConfig(); + } + + Future saveAria2ConnectTimeout(int value) async { + log.debug('saveAria2ConnectTimeout:$value'); + aria2ConnectTimeout.value = value; + await saveBeanConfig(); + } + Future _ensureDownloadDirExists() async { try { await Directory(downloadPath.value).create(recursive: true); From 2e6ebcb354f20457f0a767624280a29d34165dc4 Mon Sep 17 00:00:00 2001 From: Wen_He Date: Tue, 17 Mar 2026 18:46:23 +0800 Subject: [PATCH 02/16] feat: improve aria2 archive push UX and filename template --- lib/src/l18n/en_US.dart | 10 ++- lib/src/l18n/zh_CN.dart | 10 ++- lib/src/pages/details/details_page.dart | 11 ---- lib/src/pages/details/details_page_logic.dart | 33 ++-------- .../download/setting_download_page.dart | 63 +++++++++++++------ lib/src/service/archive_download_service.dart | 26 +++++++- lib/src/service/aria2_service.dart | 41 ++++++++---- lib/src/setting/download_setting.dart | 9 +++ lib/src/widget/eh_archive_dialog.dart | 14 ++++- 9 files changed, 142 insertions(+), 75 deletions(-) diff --git a/lib/src/l18n/en_US.dart b/lib/src/l18n/en_US.dart index 14cdd5499..af93e0203 100644 --- a/lib/src/l18n/en_US.dart +++ b/lib/src/l18n/en_US.dart @@ -224,7 +224,7 @@ class en_US { 'back': 'Back', 'toggleMenu': 'Toggle menu', 'share': 'Share', - 'save': 'Save to Pictures', + 'save': 'Save', /// setting page 'account': 'Account', @@ -726,6 +726,14 @@ class en_US { 'aria2Secret': 'RPC Secret', 'aria2DownloadDir': 'aria2 Download Directory', 'aria2DownloadDirHint': 'Optional, use aria2 default when empty', + 'aria2FilenameTemplate': 'Filename Template', + 'aria2FilenameTemplateHint': 'ArchiveV2 - {gid} - {title}.zip', + 'aria2FilenameTemplateHelp': 'Filename template help', + 'aria2FilenameTemplateOptionGid': '{gid}: Gallery ID', + 'aria2FilenameTemplateOptionTitle': '{title}: Title', + 'aria2FilenameTemplateOptionUploader': '{uploader}: Uploader', + 'aria2FilenameTemplateOptionCategory': '{category}: Category', + 'aria2FilenameTemplateExample': 'Example: ArchiveV2 - {gid} - {title}.zip', 'aria2ConnectTimeout': 'RPC Timeout', 'testConnection': 'Test Connection', 'aria2ConnectionSucceeded': 'aria2 connection succeeded', diff --git a/lib/src/l18n/zh_CN.dart b/lib/src/l18n/zh_CN.dart index e0874ea16..d7763b2bc 100644 --- a/lib/src/l18n/zh_CN.dart +++ b/lib/src/l18n/zh_CN.dart @@ -223,7 +223,7 @@ class zh_CN { 'back': '返回', 'toggleMenu': '展示/隐藏菜单', 'share': '分享', - 'save': '保存至图库', + 'save': '保存', /// setting page 'account': '账户', @@ -731,6 +731,14 @@ favnote:匹配收藏备注 'aria2Secret': 'RPC密钥', 'aria2DownloadDir': 'aria2下载目录', 'aria2DownloadDirHint': '可选,留空则使用aria2默认目录', + 'aria2FilenameTemplate': '文件名模板', + 'aria2FilenameTemplateHint': 'ArchiveV2 - {gid} - {title}.zip', + 'aria2FilenameTemplateHelp': '文件名模板说明', + 'aria2FilenameTemplateOptionGid': '{gid}:画廊ID', + 'aria2FilenameTemplateOptionTitle': '{title}:标题', + 'aria2FilenameTemplateOptionUploader': '{uploader}:上传者', + 'aria2FilenameTemplateOptionCategory': '{category}:分类', + 'aria2FilenameTemplateExample': '示例:ArchiveV2 - {gid} - {title}.zip', 'aria2ConnectTimeout': 'RPC超时', 'testConnection': '测试连接', 'aria2ConnectionSucceeded': 'aria2连接成功', diff --git a/lib/src/pages/details/details_page.dart b/lib/src/pages/details/details_page.dart index 63339e705..77b32e66d 100644 --- a/lib/src/pages/details/details_page.dart +++ b/lib/src/pages/details/details_page.dart @@ -157,14 +157,6 @@ class DetailsPage extends StatelessWidget with Scroll2TopPageMixin { children: [Text('block'.tr), const Icon(Icons.block)], ), ), - if (state.galleryDetails != null) - PopupMenuItem( - value: 6, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [Text('pushArchiveToAria2'.tr), const Icon(Icons.send_and_archive_outlined)], - ), - ), ]; }, onSelected: (value) { @@ -190,9 +182,6 @@ class DetailsPage extends StatelessWidget with Scroll2TopPageMixin { if (value == 5) { logic.blockGallery(); } - if (value == 6) { - logic.handlePushArchiveToAria2(); - } }, ); }, diff --git a/lib/src/pages/details/details_page_logic.dart b/lib/src/pages/details/details_page_logic.dart index f548c2baa..295e89b64 100644 --- a/lib/src/pages/details/details_page_logic.dart +++ b/lib/src/pages/details/details_page_logic.dart @@ -639,7 +639,7 @@ class DetailsPageLogic extends GetxController with LoginRequiredMixin, Scroll2To return; } - ({bool useBot, bool isOriginal, int size, String group})? result = await Get.dialog( + ({bool useBot, bool isOriginal, int size, String group, bool pushToAria2})? result = await Get.dialog( EHArchiveDialog( title: 'chooseArchive'.tr, archivePageUrl: state.galleryDetails!.archivePageUrl, @@ -652,6 +652,10 @@ class DetailsPageLogic extends GetxController with LoginRequiredMixin, Scroll2To } ArchiveDownloadedData archive = _buildArchiveTaskFromDialogResult(result); + if (result.pushToAria2) { + await archiveDownloadService.pushArchiveToAria2(archive); + return; + } archiveDownloadService.downloadArchive(archive); updateGlobalGalleryStatus(); @@ -702,32 +706,7 @@ class DetailsPageLogic extends GetxController with LoginRequiredMixin, Scroll2To } } - Future handlePushArchiveToAria2() async { - if (state.galleryDetails == null) { - return; - } - if (!userSetting.hasLoggedIn()) { - showLoginToast(); - return; - } - - ({bool useBot, bool isOriginal, int size, String group})? result = await Get.dialog( - EHArchiveDialog( - title: 'chooseArchive'.tr, - archivePageUrl: state.galleryDetails!.archivePageUrl, - currentGroup: downloadSetting.defaultArchiveGroup.value, - candidates: archiveDownloadService.allGroups, - ), - ); - if (result == null) { - return; - } - - ArchiveDownloadedData archive = _buildArchiveTaskFromDialogResult(result); - await archiveDownloadService.pushArchiveToAria2(archive); - } - - ArchiveDownloadedData _buildArchiveTaskFromDialogResult(({bool useBot, bool isOriginal, int size, String group}) result) { + ArchiveDownloadedData _buildArchiveTaskFromDialogResult(({bool useBot, bool isOriginal, int size, String group, bool pushToAria2}) result) { return ArchiveDownloadedData( gid: state.galleryDetails!.galleryUrl.gid, token: state.galleryDetails!.galleryUrl.token, diff --git a/lib/src/pages/setting/download/setting_download_page.dart b/lib/src/pages/setting/download/setting_download_page.dart index 0e0b16170..716f50263 100644 --- a/lib/src/pages/setting/download/setting_download_page.dart +++ b/lib/src/pages/setting/download/setting_download_page.dart @@ -18,12 +18,10 @@ import 'package:path/path.dart'; import '../../../routes/routes.dart'; import '../../../service/archive_download_service.dart'; -import '../../../service/aria2_service.dart'; import '../../../service/gallery_download_service.dart'; import '../../../service/log.dart'; import '../../../utils/permission_util.dart'; import '../../../utils/route_util.dart'; -import '../../../utils/snack_util.dart'; import '../../../widget/eh_download_dialog.dart'; class SettingDownloadPage extends StatefulWidget { @@ -223,21 +221,16 @@ class _SettingDownloadPageState extends State { Widget _buildAria2Setting(BuildContext context) { String rpcUrl = downloadSetting.aria2RpcUrl.value.trim().isEmpty ? 'aria2RpcUrlHint'.tr : downloadSetting.aria2RpcUrl.value; String downloadDir = downloadSetting.aria2DownloadDir.value.trim().isEmpty ? 'default'.tr : downloadSetting.aria2DownloadDir.value; + String filenameTemplate = downloadSetting.aria2FilenameTemplate.value.trim().isEmpty ? 'aria2FilenameTemplateHint'.tr : downloadSetting.aria2FilenameTemplate.value; return ListTile( title: Text('aria2Settings'.tr), - subtitle: Text('${'aria2RpcUrl'.tr}: ${rpcUrl.breakWord}\n${'aria2DownloadDir'.tr}: ${downloadDir.breakWord}'), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - onPressed: _testAria2Connection, - icon: const Icon(Icons.wifi_tethering), - tooltip: 'testConnection'.tr, - ), - const Icon(Icons.keyboard_arrow_right), - ], + subtitle: Text( + '${'aria2RpcUrl'.tr}: ${rpcUrl.breakWord}\n' + '${'aria2DownloadDir'.tr}: ${downloadDir.breakWord}\n' + '${'aria2FilenameTemplate'.tr}: ${filenameTemplate.breakWord}', ), + trailing: const Icon(Icons.keyboard_arrow_right), onTap: () => _showAria2SettingsDialog(context), ); } @@ -490,6 +483,7 @@ class _SettingDownloadPageState extends State { TextEditingController rpcUrlController = TextEditingController(text: downloadSetting.aria2RpcUrl.value); TextEditingController secretController = TextEditingController(text: downloadSetting.aria2Secret.value); TextEditingController dirController = TextEditingController(text: downloadSetting.aria2DownloadDir.value); + TextEditingController filenameTemplateController = TextEditingController(text: downloadSetting.aria2FilenameTemplate.value); int timeout = downloadSetting.aria2ConnectTimeout.value; bool obscureSecret = true; @@ -536,6 +530,20 @@ class _SettingDownloadPageState extends State { ), ), const SizedBox(height: 12), + TextField( + controller: filenameTemplateController, + decoration: InputDecoration( + labelText: 'aria2FilenameTemplate'.tr, + hintText: 'aria2FilenameTemplateHint'.tr, + border: const OutlineInputBorder(), + suffixIcon: IconButton( + onPressed: () => _showAria2FilenameTemplateHelp(context), + icon: const Icon(Icons.help_outline), + tooltip: 'aria2FilenameTemplateHelp'.tr, + ), + ), + ), + const SizedBox(height: 12), DropdownButtonFormField( value: timeout, decoration: InputDecoration( @@ -578,15 +586,30 @@ class _SettingDownloadPageState extends State { await downloadSetting.saveAria2RpcUrl(rpcUrlController.text.trim()); await downloadSetting.saveAria2Secret(secretController.text.trim()); await downloadSetting.saveAria2DownloadDir(dirController.text.trim()); + await downloadSetting.saveAria2FilenameTemplate(filenameTemplateController.text.trim().isEmpty ? 'ArchiveV2 - {gid} - {title}.zip' : filenameTemplateController.text.trim()); await downloadSetting.saveAria2ConnectTimeout(timeout); } - Future _testAria2Connection() async { - try { - await aria2Service.testConnection(); - toast('aria2ConnectionSucceeded'.tr); - } catch (e) { - snack('aria2ConnectionFailed'.tr, e.toString(), isShort: true); - } + Future _showAria2FilenameTemplateHelp(BuildContext context) async { + await showDialog( + context: context, + builder: (_) => AlertDialog( + title: Text('aria2FilenameTemplateHelp'.tr), + content: Text( + '${'aria2FilenameTemplateOptionGid'.tr}\n' + '${'aria2FilenameTemplateOptionTitle'.tr}\n' + '${'aria2FilenameTemplateOptionUploader'.tr}\n' + '${'aria2FilenameTemplateOptionCategory'.tr}\n\n' + '${'aria2FilenameTemplateExample'.tr}', + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text('ok'.tr), + ), + ], + ), + ); } + } diff --git a/lib/src/service/archive_download_service.dart b/lib/src/service/archive_download_service.dart index ddad629d0..262fca32e 100644 --- a/lib/src/service/archive_download_service.dart +++ b/lib/src/service/archive_download_service.dart @@ -438,7 +438,7 @@ class ArchiveDownloadService extends GetxController with GridBasePageServiceMixi try { await aria2Service.addUri( uri: resolvedDownloadUrl, - out: 'ArchiveV2 - ${archive.gid} - ${_computeArchiveTitle(archive.title)}.zip', + out: _buildAria2OutName(archive), dir: downloadSetting.aria2DownloadDir.value.trim(), headers: headers, ); @@ -711,6 +711,30 @@ class ArchiveDownloadService extends GetxController with GridBasePageServiceMixi return title; } + String _buildAria2OutName(ArchiveDownloadedData archive) { + String template = downloadSetting.aria2FilenameTemplate.value.trim(); + if (template.isEmpty) { + template = 'ArchiveV2 - {gid} - {title}.zip'; + } + + String out = template + .replaceAll('{gid}', archive.gid.toString()) + .replaceAll('{title}', _computeArchiveTitle(archive.title)) + .replaceAll('{uploader}', _computeArchiveTitle(archive.uploader)) + .replaceAll('{category}', _computeArchiveTitle(archive.category)); + + out = out.replaceAll(RegExp(r'[/|?,:*"<>\\]'), ' ').replaceAll(RegExp(r'\s+'), ' ').trim(); + + if (out.isEmpty) { + out = 'ArchiveV2 - ${archive.gid} - ${_computeArchiveTitle(archive.title)}'; + } + if (!out.toLowerCase().endsWith('.zip')) { + out = '$out.zip'; + } + + return out; + } + String computePackingFileDownloadPath(ArchiveDownloadedData archive) { String title = _computeArchiveTitle(archive.title); diff --git a/lib/src/service/aria2_service.dart b/lib/src/service/aria2_service.dart index 370f164d8..ec7da5234 100644 --- a/lib/src/service/aria2_service.dart +++ b/lib/src/service/aria2_service.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:dio/dio.dart'; import 'package:get/get_utils/get_utils.dart'; import 'package:jhentai/src/setting/download_setting.dart'; @@ -27,6 +29,25 @@ class Aria2Service { return url; } + Map _parseJsonRpcResponse(dynamic data) { + if (data is Map) { + return data; + } + if (data is Map) { + return Map.from(data); + } + if (data is String && data.isNotEmpty) { + dynamic decoded = jsonDecode(data); + if (decoded is Map) { + return decoded; + } + if (decoded is Map) { + return Map.from(decoded); + } + } + throw Exception('Invalid aria2 response'); + } + Future addUri({ required String uri, String? out, @@ -68,15 +89,14 @@ class Aria2Service { }, ); - if (response.data is! Map) { - throw Exception('Invalid aria2 response'); - } - if (response.data['error'] != null) { - String error = response.data['error'] is Map ? (response.data['error']['message']?.toString() ?? 'Unknown aria2 error') : 'Unknown aria2 error'; + Map rpcResult = _parseJsonRpcResponse(response.data); + + if (rpcResult['error'] != null) { + String error = rpcResult['error'] is Map ? (rpcResult['error']['message']?.toString() ?? 'Unknown aria2 error') : 'Unknown aria2 error'; throw Exception(error); } - String? gid = response.data['result']?.toString(); + String? gid = rpcResult['result']?.toString(); if (gid.isBlank == true) { throw Exception('Empty aria2 task id'); } @@ -106,11 +126,10 @@ class Aria2Service { }, ); - if (response.data is! Map) { - throw Exception('Invalid aria2 response'); - } - if (response.data['error'] != null) { - String error = response.data['error'] is Map ? (response.data['error']['message']?.toString() ?? 'Unknown aria2 error') : 'Unknown aria2 error'; + Map rpcResult = _parseJsonRpcResponse(response.data); + + if (rpcResult['error'] != null) { + String error = rpcResult['error'] is Map ? (rpcResult['error']['message']?.toString() ?? 'Unknown aria2 error') : 'Unknown aria2 error'; throw Exception(error); } } diff --git a/lib/src/setting/download_setting.dart b/lib/src/setting/download_setting.dart index d0c66ad01..f5ceec2b8 100644 --- a/lib/src/setting/download_setting.dart +++ b/lib/src/setting/download_setting.dart @@ -36,6 +36,7 @@ class DownloadSetting with JHLifeCircleBeanWithConfigStorage implements JHLifeCi RxString aria2RpcUrl = 'http://127.0.0.1:6800/jsonrpc'.obs; RxString aria2Secret = ''.obs; RxString aria2DownloadDir = ''.obs; + RxString aria2FilenameTemplate = 'ArchiveV2 - {gid} - {title}.zip'.obs; RxInt aria2ConnectTimeout = 8000.obs; @override @@ -72,6 +73,7 @@ class DownloadSetting with JHLifeCircleBeanWithConfigStorage implements JHLifeCi aria2RpcUrl.value = map['aria2RpcUrl'] ?? aria2RpcUrl.value; aria2Secret.value = map['aria2Secret'] ?? aria2Secret.value; aria2DownloadDir.value = map['aria2DownloadDir'] ?? aria2DownloadDir.value; + aria2FilenameTemplate.value = map['aria2FilenameTemplate'] ?? aria2FilenameTemplate.value; aria2ConnectTimeout.value = map['aria2ConnectTimeout'] ?? aria2ConnectTimeout.value; } @@ -97,6 +99,7 @@ class DownloadSetting with JHLifeCircleBeanWithConfigStorage implements JHLifeCi 'aria2RpcUrl': aria2RpcUrl.value, 'aria2Secret': aria2Secret.value, 'aria2DownloadDir': aria2DownloadDir.value, + 'aria2FilenameTemplate': aria2FilenameTemplate.value, 'aria2ConnectTimeout': aria2ConnectTimeout.value, }); } @@ -237,6 +240,12 @@ class DownloadSetting with JHLifeCircleBeanWithConfigStorage implements JHLifeCi await saveBeanConfig(); } + Future saveAria2FilenameTemplate(String value) async { + log.debug('saveAria2FilenameTemplate:$value'); + aria2FilenameTemplate.value = value; + await saveBeanConfig(); + } + Future saveAria2ConnectTimeout(int value) async { log.debug('saveAria2ConnectTimeout:$value'); aria2ConnectTimeout.value = value; diff --git a/lib/src/widget/eh_archive_dialog.dart b/lib/src/widget/eh_archive_dialog.dart index 590564caf..4077e60ec 100644 --- a/lib/src/widget/eh_archive_dialog.dart +++ b/lib/src/widget/eh_archive_dialog.dart @@ -40,6 +40,7 @@ class _EHArchiveDialogState extends State { late List candidates; late GalleryArchive archive; LoadingState loadingState = LoadingState.idle; + bool pushToAria2 = false; @override void initState() { @@ -72,6 +73,13 @@ class _EHArchiveDialogState extends State { mainAxisSize: MainAxisSize.min, children: [ EHGroupNameSelector(candidates: candidates, currentGroup: group, listener: (g) => group = g), + CheckboxListTile( + value: pushToAria2, + onChanged: (value) => setState(() => pushToAria2 = value ?? false), + title: Text('pushArchiveToAria2'.tr), + contentPadding: EdgeInsets.zero, + controlAffinity: ListTileControlAffinity.leading, + ), if (archive.creditCount != null && archive.gpCount != null) EHAsset(gpCount: archive.gpCount!, creditCount: archive.creditCount!).marginOnly(top: 12), Expanded(child: _buildButtons().marginOnly(top: 12)), ], @@ -88,7 +96,7 @@ class _EHArchiveDialogState extends State { text: 'resample'.tr, callback: _canAffordDownload(isOriginal: false) ? () => backRoute( - result: (useBot: false, isOriginal: false, size: _computeSizeInBytes(isOriginal: false), group: group), + result: (useBot: false, isOriginal: false, size: _computeSizeInBytes(isOriginal: false), group: group, pushToAria2: pushToAria2), ) : null, ), @@ -98,7 +106,7 @@ class _EHArchiveDialogState extends State { text: 'original'.tr, callback: _canAffordDownload(isOriginal: true) ? () => backRoute( - result: (useBot: false, isOriginal: true, size: _computeSizeInBytes(isOriginal: true), group: group), + result: (useBot: false, isOriginal: true, size: _computeSizeInBytes(isOriginal: true), group: group, pushToAria2: pushToAria2), ) : null, ), @@ -108,7 +116,7 @@ class _EHArchiveDialogState extends State { size: archive.originalSize, icon: const Icon(Icons.smart_toy_outlined), callback: () => backRoute( - result: (useBot: true, isOriginal: true, size: _computeSizeInBytes(isOriginal: true), group: group), + result: (useBot: true, isOriginal: true, size: _computeSizeInBytes(isOriginal: true), group: group, pushToAria2: pushToAria2), ), ), ], From 0a850632ff7a4b660ff5ebbd56128fd1f0ec01e1 Mon Sep 17 00:00:00 2001 From: Wen_He Date: Tue, 17 Mar 2026 19:06:29 +0800 Subject: [PATCH 03/16] fix(l10n): keep global save text and add aria2 save label --- lib/src/l18n/en_US.dart | 3 ++- lib/src/l18n/zh_CN.dart | 3 ++- lib/src/pages/setting/download/setting_download_page.dart | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/src/l18n/en_US.dart b/lib/src/l18n/en_US.dart index af93e0203..9eeead22b 100644 --- a/lib/src/l18n/en_US.dart +++ b/lib/src/l18n/en_US.dart @@ -224,7 +224,7 @@ class en_US { 'back': 'Back', 'toggleMenu': 'Toggle menu', 'share': 'Share', - 'save': 'Save', + 'save': 'Save to Pictures', /// setting page 'account': 'Account', @@ -742,6 +742,7 @@ class en_US { 'aria2PushSuccess': 'Pushed to aria2: %s', 'aria2PushFailed': 'Push to aria2 failed', 'aria2NotConfiguredHint': 'Please enable and configure aria2 in Download Settings', + 'aria2SaveSettings': 'Save', /// archive bot settings 'archiveBotSettings': 'Archive Bot Settings', diff --git a/lib/src/l18n/zh_CN.dart b/lib/src/l18n/zh_CN.dart index d7763b2bc..2126e6169 100644 --- a/lib/src/l18n/zh_CN.dart +++ b/lib/src/l18n/zh_CN.dart @@ -223,7 +223,7 @@ class zh_CN { 'back': '返回', 'toggleMenu': '展示/隐藏菜单', 'share': '分享', - 'save': '保存', + 'save': '保存至图库', /// setting page 'account': '账户', @@ -747,6 +747,7 @@ favnote:匹配收藏备注 'aria2PushSuccess': '已推送到aria2:%s', 'aria2PushFailed': '推送到aria2失败', 'aria2NotConfiguredHint': '请先在下载设置中启用并配置aria2', + 'aria2SaveSettings': '保存', /// archive bot settings 'archiveBotSettings': '归档机器人设置', diff --git a/lib/src/pages/setting/download/setting_download_page.dart b/lib/src/pages/setting/download/setting_download_page.dart index 716f50263..57dddf307 100644 --- a/lib/src/pages/setting/download/setting_download_page.dart +++ b/lib/src/pages/setting/download/setting_download_page.dart @@ -570,7 +570,7 @@ class _SettingDownloadPageState extends State { TextButton(onPressed: () => Navigator.of(context).pop(false), child: Text('cancel'.tr)), TextButton( onPressed: () => Navigator.of(context).pop(true), - child: Text('save'.tr), + child: Text('aria2SaveSettings'.tr), ), ], ); From b3b20f3566992f7cf05e1cf4f63099d7099ea1eb Mon Sep 17 00:00:00 2001 From: Wen_He Date: Tue, 17 Mar 2026 19:37:47 +0800 Subject: [PATCH 04/16] fix(aria2): handle nullable uploader in filename template --- lib/src/service/archive_download_service.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/service/archive_download_service.dart b/lib/src/service/archive_download_service.dart index 262fca32e..2dfc280e6 100644 --- a/lib/src/service/archive_download_service.dart +++ b/lib/src/service/archive_download_service.dart @@ -720,7 +720,7 @@ class ArchiveDownloadService extends GetxController with GridBasePageServiceMixi String out = template .replaceAll('{gid}', archive.gid.toString()) .replaceAll('{title}', _computeArchiveTitle(archive.title)) - .replaceAll('{uploader}', _computeArchiveTitle(archive.uploader)) + .replaceAll('{uploader}', _computeArchiveTitle(archive.uploader ?? '')) .replaceAll('{category}', _computeArchiveTitle(archive.category)); out = out.replaceAll(RegExp(r'[/|?,:*"<>\\]'), ' ').replaceAll(RegExp(r'\s+'), ' ').trim(); From 6751e09d2a45fcb48c511fe259ad0bf00321be04 Mon Sep 17 00:00:00 2001 From: Wen_He Date: Tue, 17 Mar 2026 19:56:15 +0800 Subject: [PATCH 05/16] style(archive): compact aria2 push option row --- lib/src/widget/eh_archive_dialog.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/src/widget/eh_archive_dialog.dart b/lib/src/widget/eh_archive_dialog.dart index 4077e60ec..c818bd793 100644 --- a/lib/src/widget/eh_archive_dialog.dart +++ b/lib/src/widget/eh_archive_dialog.dart @@ -76,9 +76,14 @@ class _EHArchiveDialogState extends State { CheckboxListTile( value: pushToAria2, onChanged: (value) => setState(() => pushToAria2 = value ?? false), - title: Text('pushArchiveToAria2'.tr), + title: Text( + 'pushArchiveToAria2'.tr, + style: TextStyle(fontSize: UIConfig.archiveDialogDownloadTextSize + 1), + ), contentPadding: EdgeInsets.zero, controlAffinity: ListTileControlAffinity.leading, + dense: true, + visualDensity: const VisualDensity(horizontal: 0, vertical: -3), ), if (archive.creditCount != null && archive.gpCount != null) EHAsset(gpCount: archive.gpCount!, creditCount: archive.creditCount!).marginOnly(top: 12), Expanded(child: _buildButtons().marginOnly(top: 12)), From 8d6317bcf36c6a6e1c3d6b776a485339cc3c9b21 Mon Sep 17 00:00:00 2001 From: Wen_He Date: Tue, 17 Mar 2026 20:15:41 +0800 Subject: [PATCH 06/16] style(archive): tighten top spacing for aria2 option --- lib/src/widget/eh_archive_dialog.dart | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/lib/src/widget/eh_archive_dialog.dart b/lib/src/widget/eh_archive_dialog.dart index c818bd793..c227320f3 100644 --- a/lib/src/widget/eh_archive_dialog.dart +++ b/lib/src/widget/eh_archive_dialog.dart @@ -73,17 +73,21 @@ class _EHArchiveDialogState extends State { mainAxisSize: MainAxisSize.min, children: [ EHGroupNameSelector(candidates: candidates, currentGroup: group, listener: (g) => group = g), - CheckboxListTile( - value: pushToAria2, - onChanged: (value) => setState(() => pushToAria2 = value ?? false), - title: Text( - 'pushArchiveToAria2'.tr, - style: TextStyle(fontSize: UIConfig.archiveDialogDownloadTextSize + 1), + Transform.translate( + offset: const Offset(0, -8), + child: CheckboxListTile( + value: pushToAria2, + onChanged: (value) => setState(() => pushToAria2 = value ?? false), + title: Text( + 'pushArchiveToAria2'.tr, + style: TextStyle(fontSize: UIConfig.archiveDialogDownloadTextSize + 1), + ), + contentPadding: EdgeInsets.zero, + controlAffinity: ListTileControlAffinity.leading, + dense: true, + visualDensity: const VisualDensity(horizontal: 0, vertical: -4), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, ), - contentPadding: EdgeInsets.zero, - controlAffinity: ListTileControlAffinity.leading, - dense: true, - visualDensity: const VisualDensity(horizontal: 0, vertical: -3), ), if (archive.creditCount != null && archive.gpCount != null) EHAsset(gpCount: archive.gpCount!, creditCount: archive.creditCount!).marginOnly(top: 12), Expanded(child: _buildButtons().marginOnly(top: 12)), From 679a87805cf2e6eb275bde940882f2c50d2594ce Mon Sep 17 00:00:00 2001 From: Wen_He Date: Tue, 17 Mar 2026 20:36:13 +0800 Subject: [PATCH 07/16] feat(aria2): add dedicated settings page and default push option --- lib/src/l18n/en_US.dart | 2 + lib/src/l18n/zh_CN.dart | 2 + .../archive_download_page_logic_mixin.dart | 16 +- .../download/aria2/aria2_settings_page.dart | 167 ++++++++++++++++++ .../download/setting_download_page.dart | 139 +-------------- lib/src/routes/routes.dart | 8 + lib/src/setting/download_setting.dart | 9 + lib/src/widget/eh_archive_dialog.dart | 31 ++-- 8 files changed, 217 insertions(+), 157 deletions(-) create mode 100644 lib/src/pages/setting/download/aria2/aria2_settings_page.dart diff --git a/lib/src/l18n/en_US.dart b/lib/src/l18n/en_US.dart index 9eeead22b..db320c8bc 100644 --- a/lib/src/l18n/en_US.dart +++ b/lib/src/l18n/en_US.dart @@ -734,6 +734,8 @@ class en_US { 'aria2FilenameTemplateOptionUploader': '{uploader}: Uploader', 'aria2FilenameTemplateOptionCategory': '{category}: Category', 'aria2FilenameTemplateExample': 'Example: ArchiveV2 - {gid} - {title}.zip', + 'aria2DefaultPushSelected': 'Default Select Push to aria2', + 'aria2DefaultPushSelectedHint': 'Pre-select "Push Archive to aria2" in archive dialog', 'aria2ConnectTimeout': 'RPC Timeout', 'testConnection': 'Test Connection', 'aria2ConnectionSucceeded': 'aria2 connection succeeded', diff --git a/lib/src/l18n/zh_CN.dart b/lib/src/l18n/zh_CN.dart index 2126e6169..9d3bb1645 100644 --- a/lib/src/l18n/zh_CN.dart +++ b/lib/src/l18n/zh_CN.dart @@ -739,6 +739,8 @@ favnote:匹配收藏备注 'aria2FilenameTemplateOptionUploader': '{uploader}:上传者', 'aria2FilenameTemplateOptionCategory': '{category}:分类', 'aria2FilenameTemplateExample': '示例:ArchiveV2 - {gid} - {title}.zip', + 'aria2DefaultPushSelected': '默认选中推送至aria2', + 'aria2DefaultPushSelectedHint': '在归档弹窗中默认勾选“推送归档至aria2”', 'aria2ConnectTimeout': 'RPC超时', 'testConnection': '测试连接', 'aria2ConnectionSucceeded': 'aria2连接成功', diff --git a/lib/src/pages/download/mixin/archive/archive_download_page_logic_mixin.dart b/lib/src/pages/download/mixin/archive/archive_download_page_logic_mixin.dart index d3f5c4274..9e2035fbc 100644 --- a/lib/src/pages/download/mixin/archive/archive_download_page_logic_mixin.dart +++ b/lib/src/pages/download/mixin/archive/archive_download_page_logic_mixin.dart @@ -5,6 +5,7 @@ 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/update_global_gallery_status_logic_mixin.dart'; import 'package:jhentai/src/setting/archive_bot_setting.dart'; +import 'package:jhentai/src/setting/download_setting.dart'; import 'package:jhentai/src/widget/eh_archive_parse_source_select_dialog.dart'; import '../../../../database/database.dart'; @@ -228,13 +229,14 @@ mixin ArchiveDownloadPageLogicMixin on GetxController changeParseSource(archive.gid, ArchiveParseSource.bot); }, ), - CupertinoActionSheetAction( - child: Text('pushArchiveToAria2'.tr), - onPressed: () { - backRoute(); - archiveDownloadService.pushArchiveToAria2(archive); - }, - ), + if (downloadSetting.enableAria2Push.value) + CupertinoActionSheetAction( + child: Text('pushArchiveToAria2'.tr), + onPressed: () { + backRoute(); + archiveDownloadService.pushArchiveToAria2(archive); + }, + ), CupertinoActionSheetAction( child: Text('changeGroup'.tr), onPressed: () { diff --git a/lib/src/pages/setting/download/aria2/aria2_settings_page.dart b/lib/src/pages/setting/download/aria2/aria2_settings_page.dart new file mode 100644 index 000000000..3356dd24f --- /dev/null +++ b/lib/src/pages/setting/download/aria2/aria2_settings_page.dart @@ -0,0 +1,167 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:jhentai/src/setting/download_setting.dart'; +import 'package:jhentai/src/utils/toast_util.dart'; + +class Aria2SettingsPage extends StatefulWidget { + const Aria2SettingsPage({Key? key}) : super(key: key); + + @override + State createState() => _Aria2SettingsPageState(); +} + +class _Aria2SettingsPageState extends State { + late final TextEditingController rpcUrlController; + late final TextEditingController secretController; + late final TextEditingController dirController; + late final TextEditingController filenameTemplateController; + + int timeout = downloadSetting.aria2ConnectTimeout.value; + bool defaultPushSelected = downloadSetting.aria2DefaultPushSelected.value; + bool obscureSecret = true; + + @override + void initState() { + super.initState(); + rpcUrlController = TextEditingController(text: downloadSetting.aria2RpcUrl.value); + secretController = TextEditingController(text: downloadSetting.aria2Secret.value); + dirController = TextEditingController(text: downloadSetting.aria2DownloadDir.value); + filenameTemplateController = TextEditingController(text: downloadSetting.aria2FilenameTemplate.value); + } + + @override + void dispose() { + rpcUrlController.dispose(); + secretController.dispose(); + dirController.dispose(); + filenameTemplateController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + centerTitle: true, + title: Text('aria2Settings'.tr), + actions: [ + TextButton( + onPressed: _save, + child: Text('aria2SaveSettings'.tr), + ), + ], + ), + body: ListView( + padding: const EdgeInsets.all(16), + children: [ + TextField( + controller: rpcUrlController, + decoration: InputDecoration( + labelText: 'aria2RpcUrl'.tr, + hintText: 'aria2RpcUrlHint'.tr, + border: const OutlineInputBorder(), + ), + ), + SwitchListTile( + contentPadding: EdgeInsets.zero, + title: Text('aria2DefaultPushSelected'.tr), + subtitle: Text('aria2DefaultPushSelectedHint'.tr), + value: defaultPushSelected, + onChanged: (value) => setState(() => defaultPushSelected = value), + ), + const SizedBox(height: 12), + TextField( + controller: secretController, + obscureText: obscureSecret, + decoration: InputDecoration( + labelText: 'aria2Secret'.tr, + border: const OutlineInputBorder(), + suffixIcon: IconButton( + onPressed: () => setState(() => obscureSecret = !obscureSecret), + icon: Icon(obscureSecret ? Icons.visibility_off : Icons.visibility), + ), + ), + ), + const SizedBox(height: 12), + TextField( + controller: dirController, + decoration: InputDecoration( + labelText: 'aria2DownloadDir'.tr, + hintText: 'aria2DownloadDirHint'.tr, + border: const OutlineInputBorder(), + ), + ), + const SizedBox(height: 12), + TextField( + controller: filenameTemplateController, + decoration: InputDecoration( + labelText: 'aria2FilenameTemplate'.tr, + hintText: 'aria2FilenameTemplateHint'.tr, + border: const OutlineInputBorder(), + suffixIcon: IconButton( + onPressed: _showAria2FilenameTemplateHelp, + icon: const Icon(Icons.help_outline), + tooltip: 'aria2FilenameTemplateHelp'.tr, + ), + ), + ), + const SizedBox(height: 12), + DropdownButtonFormField( + value: timeout, + decoration: InputDecoration( + labelText: 'aria2ConnectTimeout'.tr, + border: const OutlineInputBorder(), + ), + items: const [ + DropdownMenuItem(value: 3000, child: Text('3000ms')), + DropdownMenuItem(value: 5000, child: Text('5000ms')), + DropdownMenuItem(value: 8000, child: Text('8000ms')), + DropdownMenuItem(value: 12000, child: Text('12000ms')), + DropdownMenuItem(value: 20000, child: Text('20000ms')), + ], + onChanged: (value) { + if (value != null) { + timeout = value; + } + }, + ), + ], + ), + ); + } + + Future _save() async { + await downloadSetting.saveAria2RpcUrl(rpcUrlController.text.trim()); + await downloadSetting.saveAria2Secret(secretController.text.trim()); + await downloadSetting.saveAria2DownloadDir(dirController.text.trim()); + await downloadSetting.saveAria2FilenameTemplate( + filenameTemplateController.text.trim().isEmpty ? 'ArchiveV2 - {gid} - {title}.zip' : filenameTemplateController.text.trim(), + ); + await downloadSetting.saveAria2DefaultPushSelected(defaultPushSelected); + await downloadSetting.saveAria2ConnectTimeout(timeout); + toast('saveSuccess'.tr); + Get.back(); + } + + Future _showAria2FilenameTemplateHelp() async { + await showDialog( + context: context, + builder: (_) => AlertDialog( + title: Text('aria2FilenameTemplateHelp'.tr), + content: Text( + '${'aria2FilenameTemplateOptionGid'.tr}\n' + '${'aria2FilenameTemplateOptionTitle'.tr}\n' + '${'aria2FilenameTemplateOptionUploader'.tr}\n' + '${'aria2FilenameTemplateOptionCategory'.tr}\n\n' + '${'aria2FilenameTemplateExample'.tr}', + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text('ok'.tr), + ), + ], + ), + ); + } +} diff --git a/lib/src/pages/setting/download/setting_download_page.dart b/lib/src/pages/setting/download/setting_download_page.dart index 57dddf307..a99aab57f 100644 --- a/lib/src/pages/setting/download/setting_download_page.dart +++ b/lib/src/pages/setting/download/setting_download_page.dart @@ -62,7 +62,7 @@ class _SettingDownloadPageState extends State { _buildDefaultArchiveGroup(context), _buildArchiveBotSettings(), _buildEnableAria2Push(), - _buildAria2Setting(context), + _buildAria2Setting(), _buildDownloadConcurrency(), _buildSpeedLimit(context), _buildDownloadAllGallerysOfSamePriority(), @@ -218,7 +218,7 @@ class _SettingDownloadPageState extends State { ); } - Widget _buildAria2Setting(BuildContext context) { + Widget _buildAria2Setting() { String rpcUrl = downloadSetting.aria2RpcUrl.value.trim().isEmpty ? 'aria2RpcUrlHint'.tr : downloadSetting.aria2RpcUrl.value; String downloadDir = downloadSetting.aria2DownloadDir.value.trim().isEmpty ? 'default'.tr : downloadSetting.aria2DownloadDir.value; String filenameTemplate = downloadSetting.aria2FilenameTemplate.value.trim().isEmpty ? 'aria2FilenameTemplateHint'.tr : downloadSetting.aria2FilenameTemplate.value; @@ -231,7 +231,7 @@ class _SettingDownloadPageState extends State { '${'aria2FilenameTemplate'.tr}: ${filenameTemplate.breakWord}', ), trailing: const Icon(Icons.keyboard_arrow_right), - onTap: () => _showAria2SettingsDialog(context), + onTap: () => toRoute(Routes.aria2Settings), ); } @@ -479,137 +479,4 @@ class _SettingDownloadPageState extends State { ); } - Future _showAria2SettingsDialog(BuildContext context) async { - TextEditingController rpcUrlController = TextEditingController(text: downloadSetting.aria2RpcUrl.value); - TextEditingController secretController = TextEditingController(text: downloadSetting.aria2Secret.value); - TextEditingController dirController = TextEditingController(text: downloadSetting.aria2DownloadDir.value); - TextEditingController filenameTemplateController = TextEditingController(text: downloadSetting.aria2FilenameTemplate.value); - int timeout = downloadSetting.aria2ConnectTimeout.value; - bool obscureSecret = true; - - bool? result = await showDialog( - context: context, - builder: (_) { - return StatefulBuilder( - builder: (context, setState) { - return AlertDialog( - title: Text('aria2Settings'.tr), - content: SizedBox( - width: 520, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextField( - controller: rpcUrlController, - decoration: InputDecoration( - labelText: 'aria2RpcUrl'.tr, - hintText: 'aria2RpcUrlHint'.tr, - border: const OutlineInputBorder(), - ), - ), - const SizedBox(height: 12), - TextField( - controller: secretController, - obscureText: obscureSecret, - decoration: InputDecoration( - labelText: 'aria2Secret'.tr, - border: const OutlineInputBorder(), - suffixIcon: IconButton( - onPressed: () => setState(() => obscureSecret = !obscureSecret), - icon: Icon(obscureSecret ? Icons.visibility_off : Icons.visibility), - ), - ), - ), - const SizedBox(height: 12), - TextField( - controller: dirController, - decoration: InputDecoration( - labelText: 'aria2DownloadDir'.tr, - hintText: 'aria2DownloadDirHint'.tr, - border: const OutlineInputBorder(), - ), - ), - const SizedBox(height: 12), - TextField( - controller: filenameTemplateController, - decoration: InputDecoration( - labelText: 'aria2FilenameTemplate'.tr, - hintText: 'aria2FilenameTemplateHint'.tr, - border: const OutlineInputBorder(), - suffixIcon: IconButton( - onPressed: () => _showAria2FilenameTemplateHelp(context), - icon: const Icon(Icons.help_outline), - tooltip: 'aria2FilenameTemplateHelp'.tr, - ), - ), - ), - const SizedBox(height: 12), - DropdownButtonFormField( - value: timeout, - decoration: InputDecoration( - labelText: 'aria2ConnectTimeout'.tr, - border: const OutlineInputBorder(), - ), - items: const [ - DropdownMenuItem(value: 3000, child: Text('3000ms')), - DropdownMenuItem(value: 5000, child: Text('5000ms')), - DropdownMenuItem(value: 8000, child: Text('8000ms')), - DropdownMenuItem(value: 12000, child: Text('12000ms')), - DropdownMenuItem(value: 20000, child: Text('20000ms')), - ], - onChanged: (value) { - if (value != null) { - timeout = value; - } - }, - ), - ], - ), - ), - actions: [ - TextButton(onPressed: () => Navigator.of(context).pop(false), child: Text('cancel'.tr)), - TextButton( - onPressed: () => Navigator.of(context).pop(true), - child: Text('aria2SaveSettings'.tr), - ), - ], - ); - }, - ); - }, - ); - - if (result != true) { - return; - } - - await downloadSetting.saveAria2RpcUrl(rpcUrlController.text.trim()); - await downloadSetting.saveAria2Secret(secretController.text.trim()); - await downloadSetting.saveAria2DownloadDir(dirController.text.trim()); - await downloadSetting.saveAria2FilenameTemplate(filenameTemplateController.text.trim().isEmpty ? 'ArchiveV2 - {gid} - {title}.zip' : filenameTemplateController.text.trim()); - await downloadSetting.saveAria2ConnectTimeout(timeout); - } - - Future _showAria2FilenameTemplateHelp(BuildContext context) async { - await showDialog( - context: context, - builder: (_) => AlertDialog( - title: Text('aria2FilenameTemplateHelp'.tr), - content: Text( - '${'aria2FilenameTemplateOptionGid'.tr}\n' - '${'aria2FilenameTemplateOptionTitle'.tr}\n' - '${'aria2FilenameTemplateOptionUploader'.tr}\n' - '${'aria2FilenameTemplateOptionCategory'.tr}\n\n' - '${'aria2FilenameTemplateExample'.tr}', - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text('ok'.tr), - ), - ], - ), - ); - } - } diff --git a/lib/src/routes/routes.dart b/lib/src/routes/routes.dart index e5a04e6f8..b92267f92 100644 --- a/lib/src/routes/routes.dart +++ b/lib/src/routes/routes.dart @@ -50,6 +50,7 @@ import '../pages/setting/account/setting_account_page.dart'; import '../pages/setting/advanced/loglist/log/log_page.dart'; import '../pages/setting/advanced/loglist/log_list_page.dart'; import '../pages/setting/advanced/super_resolution/setting_super_resolution_page.dart'; +import '../pages/setting/download/aria2/aria2_settings_page.dart'; import '../pages/setting/download/archive_bot/archive_bot_settings_page.dart'; import '../pages/setting/preference/block_rule/add_block_rule/configure_blocking_rule_page.dart'; import '../pages/setting/style/page_list_style/setting_page_list_style_page.dart'; @@ -121,6 +122,7 @@ class Routes { static const String extraGalleryScanPath = "/setting_download/extraGalleryScanPath"; static const String archiveBotSettings = '/setting/download/archive_bot'; + static const String aria2Settings = '/setting/download/aria2'; static const String superResolution = "/setting_advanced/superResolution"; static const String logList = "/setting_advanced/logList"; @@ -404,6 +406,12 @@ class Routes { transition: defaultTransition, offAllBefore: false, ), + EHPage( + name: aria2Settings, + page: () => const Aria2SettingsPage().withEscOrFifthButton2BackRightRoute(), + transition: defaultTransition, + offAllBefore: false, + ), EHPage( name: superResolution, page: () => const SettingSuperResolutionPage().withEscOrFifthButton2BackRightRoute(), diff --git a/lib/src/setting/download_setting.dart b/lib/src/setting/download_setting.dart index f5ceec2b8..ef6b45e7b 100644 --- a/lib/src/setting/download_setting.dart +++ b/lib/src/setting/download_setting.dart @@ -37,6 +37,7 @@ class DownloadSetting with JHLifeCircleBeanWithConfigStorage implements JHLifeCi RxString aria2Secret = ''.obs; RxString aria2DownloadDir = ''.obs; RxString aria2FilenameTemplate = 'ArchiveV2 - {gid} - {title}.zip'.obs; + RxBool aria2DefaultPushSelected = false.obs; RxInt aria2ConnectTimeout = 8000.obs; @override @@ -74,6 +75,7 @@ class DownloadSetting with JHLifeCircleBeanWithConfigStorage implements JHLifeCi aria2Secret.value = map['aria2Secret'] ?? aria2Secret.value; aria2DownloadDir.value = map['aria2DownloadDir'] ?? aria2DownloadDir.value; aria2FilenameTemplate.value = map['aria2FilenameTemplate'] ?? aria2FilenameTemplate.value; + aria2DefaultPushSelected.value = map['aria2DefaultPushSelected'] ?? aria2DefaultPushSelected.value; aria2ConnectTimeout.value = map['aria2ConnectTimeout'] ?? aria2ConnectTimeout.value; } @@ -100,6 +102,7 @@ class DownloadSetting with JHLifeCircleBeanWithConfigStorage implements JHLifeCi 'aria2Secret': aria2Secret.value, 'aria2DownloadDir': aria2DownloadDir.value, 'aria2FilenameTemplate': aria2FilenameTemplate.value, + 'aria2DefaultPushSelected': aria2DefaultPushSelected.value, 'aria2ConnectTimeout': aria2ConnectTimeout.value, }); } @@ -246,6 +249,12 @@ class DownloadSetting with JHLifeCircleBeanWithConfigStorage implements JHLifeCi await saveBeanConfig(); } + Future saveAria2DefaultPushSelected(bool value) async { + log.debug('saveAria2DefaultPushSelected:$value'); + aria2DefaultPushSelected.value = value; + await saveBeanConfig(); + } + Future saveAria2ConnectTimeout(int value) async { log.debug('saveAria2ConnectTimeout:$value'); aria2ConnectTimeout.value = value; diff --git a/lib/src/widget/eh_archive_dialog.dart b/lib/src/widget/eh_archive_dialog.dart index c227320f3..3de97bffe 100644 --- a/lib/src/widget/eh_archive_dialog.dart +++ b/lib/src/widget/eh_archive_dialog.dart @@ -6,6 +6,7 @@ import 'package:jhentai/src/extension/dio_exception_extension.dart'; import 'package:jhentai/src/extension/widget_extension.dart'; import 'package:jhentai/src/model/gallery_archive.dart'; import 'package:jhentai/src/setting/archive_bot_setting.dart'; +import 'package:jhentai/src/setting/download_setting.dart'; import 'package:jhentai/src/widget/eh_asset.dart'; import 'package:jhentai/src/widget/eh_group_name_selector.dart'; import 'package:jhentai/src/widget/loading_state_indicator.dart'; @@ -47,6 +48,7 @@ class _EHArchiveDialogState extends State { super.initState(); group = widget.currentGroup ?? widget.candidates.firstOrNull ?? 'default'.tr; + pushToAria2 = downloadSetting.aria2DefaultPushSelected.value; candidates = List.of(widget.candidates); candidates.remove(group); candidates.insert(0, group); @@ -73,22 +75,23 @@ class _EHArchiveDialogState extends State { mainAxisSize: MainAxisSize.min, children: [ EHGroupNameSelector(candidates: candidates, currentGroup: group, listener: (g) => group = g), - Transform.translate( - offset: const Offset(0, -8), - child: CheckboxListTile( - value: pushToAria2, - onChanged: (value) => setState(() => pushToAria2 = value ?? false), - title: Text( - 'pushArchiveToAria2'.tr, - style: TextStyle(fontSize: UIConfig.archiveDialogDownloadTextSize + 1), + if (downloadSetting.enableAria2Push.value) + Transform.translate( + offset: const Offset(0, -8), + child: CheckboxListTile( + value: pushToAria2, + onChanged: (value) => setState(() => pushToAria2 = value ?? false), + title: Text( + 'pushArchiveToAria2'.tr, + style: TextStyle(fontSize: UIConfig.archiveDialogDownloadTextSize + 1), + ), + contentPadding: EdgeInsets.zero, + controlAffinity: ListTileControlAffinity.leading, + dense: true, + visualDensity: const VisualDensity(horizontal: 0, vertical: -4), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, ), - contentPadding: EdgeInsets.zero, - controlAffinity: ListTileControlAffinity.leading, - dense: true, - visualDensity: const VisualDensity(horizontal: 0, vertical: -4), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, ), - ), if (archive.creditCount != null && archive.gpCount != null) EHAsset(gpCount: archive.gpCount!, creditCount: archive.creditCount!).marginOnly(top: 12), Expanded(child: _buildButtons().marginOnly(top: 12)), ], From 90c3c81912a9019fb3b1cffe6dd92733ce902e75 Mon Sep 17 00:00:00 2001 From: Wen_He Date: Tue, 17 Mar 2026 21:00:24 +0800 Subject: [PATCH 08/16] refactor(aria2): move switches into settings page and enable autosave --- lib/src/l18n/en_US.dart | 1 - lib/src/l18n/zh_CN.dart | 1 - .../download/aria2/aria2_settings_page.dart | 55 +++++++++---------- .../download/setting_download_page.dart | 10 ---- lib/src/widget/eh_archive_dialog.dart | 6 +- 5 files changed, 30 insertions(+), 43 deletions(-) diff --git a/lib/src/l18n/en_US.dart b/lib/src/l18n/en_US.dart index db320c8bc..4c20d1490 100644 --- a/lib/src/l18n/en_US.dart +++ b/lib/src/l18n/en_US.dart @@ -744,7 +744,6 @@ class en_US { 'aria2PushSuccess': 'Pushed to aria2: %s', 'aria2PushFailed': 'Push to aria2 failed', 'aria2NotConfiguredHint': 'Please enable and configure aria2 in Download Settings', - 'aria2SaveSettings': 'Save', /// archive bot settings 'archiveBotSettings': 'Archive Bot Settings', diff --git a/lib/src/l18n/zh_CN.dart b/lib/src/l18n/zh_CN.dart index 9d3bb1645..7f863699f 100644 --- a/lib/src/l18n/zh_CN.dart +++ b/lib/src/l18n/zh_CN.dart @@ -749,7 +749,6 @@ favnote:匹配收藏备注 'aria2PushSuccess': '已推送到aria2:%s', 'aria2PushFailed': '推送到aria2失败', 'aria2NotConfiguredHint': '请先在下载设置中启用并配置aria2', - 'aria2SaveSettings': '保存', /// archive bot settings 'archiveBotSettings': '归档机器人设置', diff --git a/lib/src/pages/setting/download/aria2/aria2_settings_page.dart b/lib/src/pages/setting/download/aria2/aria2_settings_page.dart index 3356dd24f..3579b1c88 100644 --- a/lib/src/pages/setting/download/aria2/aria2_settings_page.dart +++ b/lib/src/pages/setting/download/aria2/aria2_settings_page.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:jhentai/src/setting/download_setting.dart'; -import 'package:jhentai/src/utils/toast_util.dart'; class Aria2SettingsPage extends StatefulWidget { const Aria2SettingsPage({Key? key}) : super(key: key); @@ -16,6 +15,7 @@ class _Aria2SettingsPageState extends State { late final TextEditingController dirController; late final TextEditingController filenameTemplateController; + bool enableAria2Push = downloadSetting.enableAria2Push.value; int timeout = downloadSetting.aria2ConnectTimeout.value; bool defaultPushSelected = downloadSetting.aria2DefaultPushSelected.value; bool obscureSecret = true; @@ -44,35 +44,44 @@ class _Aria2SettingsPageState extends State { appBar: AppBar( centerTitle: true, title: Text('aria2Settings'.tr), - actions: [ - TextButton( - onPressed: _save, - child: Text('aria2SaveSettings'.tr), - ), - ], ), body: ListView( padding: const EdgeInsets.all(16), children: [ + SwitchListTile( + contentPadding: EdgeInsets.zero, + title: Text('enableAria2Push'.tr), + subtitle: Text('enableAria2PushHint'.tr), + value: enableAria2Push, + onChanged: (value) async { + setState(() => enableAria2Push = value); + await downloadSetting.saveEnableAria2Push(value); + }, + ), + SwitchListTile( + contentPadding: EdgeInsets.zero, + title: Text('aria2DefaultPushSelected'.tr), + subtitle: Text('aria2DefaultPushSelectedHint'.tr), + value: defaultPushSelected, + onChanged: (value) async { + setState(() => defaultPushSelected = value); + await downloadSetting.saveAria2DefaultPushSelected(value); + }, + ), TextField( controller: rpcUrlController, + onChanged: (value) => downloadSetting.saveAria2RpcUrl(value.trim()), decoration: InputDecoration( labelText: 'aria2RpcUrl'.tr, hintText: 'aria2RpcUrlHint'.tr, border: const OutlineInputBorder(), ), ), - SwitchListTile( - contentPadding: EdgeInsets.zero, - title: Text('aria2DefaultPushSelected'.tr), - subtitle: Text('aria2DefaultPushSelectedHint'.tr), - value: defaultPushSelected, - onChanged: (value) => setState(() => defaultPushSelected = value), - ), const SizedBox(height: 12), TextField( controller: secretController, obscureText: obscureSecret, + onChanged: (value) => downloadSetting.saveAria2Secret(value.trim()), decoration: InputDecoration( labelText: 'aria2Secret'.tr, border: const OutlineInputBorder(), @@ -85,6 +94,7 @@ class _Aria2SettingsPageState extends State { const SizedBox(height: 12), TextField( controller: dirController, + onChanged: (value) => downloadSetting.saveAria2DownloadDir(value.trim()), decoration: InputDecoration( labelText: 'aria2DownloadDir'.tr, hintText: 'aria2DownloadDirHint'.tr, @@ -94,6 +104,7 @@ class _Aria2SettingsPageState extends State { const SizedBox(height: 12), TextField( controller: filenameTemplateController, + onChanged: (value) => downloadSetting.saveAria2FilenameTemplate(value.trim()), decoration: InputDecoration( labelText: 'aria2FilenameTemplate'.tr, hintText: 'aria2FilenameTemplateHint'.tr, @@ -121,7 +132,8 @@ class _Aria2SettingsPageState extends State { ], onChanged: (value) { if (value != null) { - timeout = value; + setState(() => timeout = value); + downloadSetting.saveAria2ConnectTimeout(value); } }, ), @@ -130,19 +142,6 @@ class _Aria2SettingsPageState extends State { ); } - Future _save() async { - await downloadSetting.saveAria2RpcUrl(rpcUrlController.text.trim()); - await downloadSetting.saveAria2Secret(secretController.text.trim()); - await downloadSetting.saveAria2DownloadDir(dirController.text.trim()); - await downloadSetting.saveAria2FilenameTemplate( - filenameTemplateController.text.trim().isEmpty ? 'ArchiveV2 - {gid} - {title}.zip' : filenameTemplateController.text.trim(), - ); - await downloadSetting.saveAria2DefaultPushSelected(defaultPushSelected); - await downloadSetting.saveAria2ConnectTimeout(timeout); - toast('saveSuccess'.tr); - Get.back(); - } - Future _showAria2FilenameTemplateHelp() async { await showDialog( context: context, diff --git a/lib/src/pages/setting/download/setting_download_page.dart b/lib/src/pages/setting/download/setting_download_page.dart index a99aab57f..44884689e 100644 --- a/lib/src/pages/setting/download/setting_download_page.dart +++ b/lib/src/pages/setting/download/setting_download_page.dart @@ -61,7 +61,6 @@ class _SettingDownloadPageState extends State { _buildDefaultGalleryGroup(context), _buildDefaultArchiveGroup(context), _buildArchiveBotSettings(), - _buildEnableAria2Push(), _buildAria2Setting(), _buildDownloadConcurrency(), _buildSpeedLimit(context), @@ -209,15 +208,6 @@ class _SettingDownloadPageState extends State { ); } - Widget _buildEnableAria2Push() { - return SwitchListTile( - title: Text('enableAria2Push'.tr), - subtitle: Text('enableAria2PushHint'.tr), - value: downloadSetting.enableAria2Push.value, - onChanged: downloadSetting.saveEnableAria2Push, - ); - } - Widget _buildAria2Setting() { String rpcUrl = downloadSetting.aria2RpcUrl.value.trim().isEmpty ? 'aria2RpcUrlHint'.tr : downloadSetting.aria2RpcUrl.value; String downloadDir = downloadSetting.aria2DownloadDir.value.trim().isEmpty ? 'default'.tr : downloadSetting.aria2DownloadDir.value; diff --git a/lib/src/widget/eh_archive_dialog.dart b/lib/src/widget/eh_archive_dialog.dart index 3de97bffe..b54d5aa9e 100644 --- a/lib/src/widget/eh_archive_dialog.dart +++ b/lib/src/widget/eh_archive_dialog.dart @@ -64,8 +64,8 @@ class _EHArchiveDialogState extends State { child: LoadingStateIndicator( loadingState: loadingState, errorTapCallback: _getArchiveInfo, - successWidgetBuilder: _buildBody, - ), + Builder: _buildBody, + ), successWidget ), ); } @@ -77,7 +77,7 @@ class _EHArchiveDialogState extends State { EHGroupNameSelector(candidates: candidates, currentGroup: group, listener: (g) => group = g), if (downloadSetting.enableAria2Push.value) Transform.translate( - offset: const Offset(0, -8), + offset: const Offset(0, -16), child: CheckboxListTile( value: pushToAria2, onChanged: (value) => setState(() => pushToAria2 = value ?? false), From 628ebee0319ac9d0b183e389e434885f0d8012fe Mon Sep 17 00:00:00 2001 From: Wen_He Date: Tue, 17 Mar 2026 22:08:47 +0800 Subject: [PATCH 09/16] fix(archive): use successWidgetBuilder in archive dialog --- lib/src/widget/eh_archive_dialog.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/widget/eh_archive_dialog.dart b/lib/src/widget/eh_archive_dialog.dart index b54d5aa9e..7ebf17bac 100644 --- a/lib/src/widget/eh_archive_dialog.dart +++ b/lib/src/widget/eh_archive_dialog.dart @@ -64,8 +64,8 @@ class _EHArchiveDialogState extends State { child: LoadingStateIndicator( loadingState: loadingState, errorTapCallback: _getArchiveInfo, - Builder: _buildBody, - ), successWidget + successWidgetBuilder: _buildBody, + ), ), ); } From ab533cb10e0939fc774116903ef9ee9569d86cd6 Mon Sep 17 00:00:00 2001 From: Wen_He Date: Tue, 17 Mar 2026 22:51:14 +0800 Subject: [PATCH 10/16] feat: align aria2 push behavior and add connection test entry --- .../download/aria2/aria2_settings_page.dart | 56 +++++++++++++++++++ lib/src/service/archive_download_service.dart | 34 ++--------- 2 files changed, 62 insertions(+), 28 deletions(-) diff --git a/lib/src/pages/setting/download/aria2/aria2_settings_page.dart b/lib/src/pages/setting/download/aria2/aria2_settings_page.dart index 3579b1c88..d936702fa 100644 --- a/lib/src/pages/setting/download/aria2/aria2_settings_page.dart +++ b/lib/src/pages/setting/download/aria2/aria2_settings_page.dart @@ -1,6 +1,11 @@ +import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:jhentai/src/service/aria2_service.dart'; import 'package:jhentai/src/setting/download_setting.dart'; +import 'package:jhentai/src/utils/snack_util.dart'; +import 'package:jhentai/src/utils/toast_util.dart'; +import 'package:jhentai/src/widget/loading_state_indicator.dart'; class Aria2SettingsPage extends StatefulWidget { const Aria2SettingsPage({Key? key}) : super(key: key); @@ -19,6 +24,7 @@ class _Aria2SettingsPageState extends State { int timeout = downloadSetting.aria2ConnectTimeout.value; bool defaultPushSelected = downloadSetting.aria2DefaultPushSelected.value; bool obscureSecret = true; + LoadingState testConnectionState = LoadingState.idle; @override void initState() { @@ -137,11 +143,61 @@ class _Aria2SettingsPageState extends State { } }, ), + const SizedBox(height: 12), + ListTile( + contentPadding: EdgeInsets.zero, + title: Text('testConnection'.tr), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + LoadingStateIndicator( + loadingState: testConnectionState, + useCupertinoIndicator: true, + idleWidgetBuilder: () => const Icon(Icons.keyboard_arrow_right), + successWidgetBuilder: () => const Icon(Icons.check_circle_outline), + errorWidgetBuilder: () => const Icon(Icons.error_outline), + ), + ], + ), + onTap: _testConnection, + ), ], ), ); } + Future _testConnection() async { + if (testConnectionState == LoadingState.loading) { + return; + } + + setState(() => testConnectionState = LoadingState.loading); + await downloadSetting.saveAria2RpcUrl(rpcUrlController.text.trim()); + await downloadSetting.saveAria2Secret(secretController.text.trim()); + await downloadSetting.saveAria2ConnectTimeout(timeout); + + try { + await aria2Service.testConnection(); + if (!mounted) { + return; + } + setState(() => testConnectionState = LoadingState.success); + toast('aria2ConnectionSucceeded'.tr, isCenter: false); + } on DioException catch (e) { + if (!mounted) { + return; + } + setState(() => testConnectionState = LoadingState.error); + snack('aria2ConnectionFailed'.tr, e.message ?? e.toString(), isShort: true); + } catch (e) { + if (!mounted) { + return; + } + setState(() => testConnectionState = LoadingState.error); + snack('aria2ConnectionFailed'.tr, e.toString(), isShort: true); + } + } + Future _showAria2FilenameTemplateHelp() async { await showDialog( context: context, diff --git a/lib/src/service/archive_download_service.dart b/lib/src/service/archive_download_service.dart index 2dfc280e6..992faccaa 100644 --- a/lib/src/service/archive_download_service.dart +++ b/lib/src/service/archive_download_service.dart @@ -26,7 +26,6 @@ import 'package:jhentai/src/setting/download_setting.dart'; import 'package:jhentai/src/setting/network_setting.dart'; import 'package:jhentai/src/service/path_service.dart'; import 'package:jhentai/src/utils/archive_bot_response_parser.dart'; -import 'package:jhentai/src/utils/cookie_util.dart'; import 'package:jhentai/src/utils/speed_computer.dart'; import 'package:jhentai/src/utils/eh_spider_parser.dart'; import 'package:logger/logger.dart'; @@ -406,19 +405,16 @@ class ArchiveDownloadService extends GetxController with GridBasePageServiceMixi } String? resolvedDownloadUrl; - String? referer; ArchiveDownloadInfo? archiveDownloadInfo = archiveDownloadInfos[archive.gid]; if (!reParse && archiveDownloadInfo?.downloadUrl != null) { resolvedDownloadUrl = archiveDownloadInfo!.downloadUrl; - referer = archiveDownloadInfo.downloadPageUrl; } else { - ({String downloadUrl, String? referer})? result = await _resolveArchiveDownloadUrlForAria2(archive, reParse: reParse); + String? result = await _resolveArchiveDownloadUrlForAria2(archive, reParse: reParse); if (result == null) { return; } - resolvedDownloadUrl = result.downloadUrl; - referer = result.referer; + resolvedDownloadUrl = result; } if (resolvedDownloadUrl == null) { @@ -426,21 +422,11 @@ class ArchiveDownloadService extends GetxController with GridBasePageServiceMixi return; } - List headers = []; - String cookies = CookieUtil.parse2String(ehRequest.cookies); - if (cookies.isNotEmpty) { - headers.add('Cookie: $cookies'); - } - if (referer != null && referer.isNotEmpty) { - headers.add('Referer: $referer'); - } - try { await aria2Service.addUri( uri: resolvedDownloadUrl, out: _buildAria2OutName(archive), dir: downloadSetting.aria2DownloadDir.value.trim(), - headers: headers, ); toast('aria2PushSuccess'.trArgs([archive.title]), isCenter: false); } on DioException catch (e) { @@ -450,7 +436,7 @@ class ArchiveDownloadService extends GetxController with GridBasePageServiceMixi } } - Future<({String downloadUrl, String? referer})?> _resolveArchiveDownloadUrlForAria2( + Future _resolveArchiveDownloadUrlForAria2( ArchiveDownloadedData archive, { bool reParse = false, }) async { @@ -473,8 +459,8 @@ class ArchiveDownloadService extends GetxController with GridBasePageServiceMixi maxAttempts: _maxRetryTimes, ); - String downloadUrl = _ensureStartQueryParameter('https://${Uri.parse(downloadPageUrl).host}$downloadPath'); - return (downloadUrl: downloadUrl, referer: downloadPageUrl); + String downloadUrl = 'https://${Uri.parse(downloadPageUrl).host}$downloadPath'; + return downloadUrl; } if (!archiveBotSetting.isReady) { @@ -501,7 +487,7 @@ class ArchiveDownloadService extends GetxController with GridBasePageServiceMixi } ArchiveResolveVO archiveResolveVO = ArchiveResolveVO.fromResponse(response.data); - return (downloadUrl: _ensureStartQueryParameter(archiveResolveVO.url), referer: null); + return archiveResolveVO.url; } on DioException catch (e) { snack('aria2PushFailed'.tr, e.error?.toString() ?? e.message ?? e.toString(), isShort: true); return null; @@ -542,14 +528,6 @@ class ArchiveDownloadService extends GetxController with GridBasePageServiceMixi return null; } - String _ensureStartQueryParameter(String url) { - Uri uri = Uri.parse(url); - Map queryParameters = Map.from(uri.queryParameters); - queryParameters.remove('autostart'); - queryParameters.putIfAbsent('start', () => '1'); - return uri.replace(queryParameters: queryParameters).toString(); - } - Future batchUpdateArchiveInDatabase(List archives) async { await appDb.transaction(() async { for (ArchiveDownloadedData archive in archives) { From 6dc33b13663d974706767691eee113ac461e75ef Mon Sep 17 00:00:00 2001 From: Wen_He Date: Tue, 17 Mar 2026 23:26:14 +0800 Subject: [PATCH 11/16] refactor(ui): debounce aria2 settings save and remove layout hacks --- .../download/aria2/aria2_settings_page.dart | 32 +++++++++---------- lib/src/widget/eh_archive_dialog.dart | 6 ++-- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/lib/src/pages/setting/download/aria2/aria2_settings_page.dart b/lib/src/pages/setting/download/aria2/aria2_settings_page.dart index d936702fa..a35177108 100644 --- a/lib/src/pages/setting/download/aria2/aria2_settings_page.dart +++ b/lib/src/pages/setting/download/aria2/aria2_settings_page.dart @@ -1,11 +1,13 @@ import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:jhentai/src/extension/widget_extension.dart'; import 'package:jhentai/src/service/aria2_service.dart'; import 'package:jhentai/src/setting/download_setting.dart'; import 'package:jhentai/src/utils/snack_util.dart'; import 'package:jhentai/src/utils/toast_util.dart'; import 'package:jhentai/src/widget/loading_state_indicator.dart'; +import 'package:throttling/throttling.dart'; class Aria2SettingsPage extends StatefulWidget { const Aria2SettingsPage({Key? key}) : super(key: key); @@ -25,6 +27,7 @@ class _Aria2SettingsPageState extends State { bool defaultPushSelected = downloadSetting.aria2DefaultPushSelected.value; bool obscureSecret = true; LoadingState testConnectionState = LoadingState.idle; + late final Debouncing saveDebouncing; @override void initState() { @@ -33,6 +36,7 @@ class _Aria2SettingsPageState extends State { secretController = TextEditingController(text: downloadSetting.aria2Secret.value); dirController = TextEditingController(text: downloadSetting.aria2DownloadDir.value); filenameTemplateController = TextEditingController(text: downloadSetting.aria2FilenameTemplate.value); + saveDebouncing = Debouncing(duration: const Duration(milliseconds: 400)); } @override @@ -41,6 +45,7 @@ class _Aria2SettingsPageState extends State { secretController.dispose(); dirController.dispose(); filenameTemplateController.dispose(); + saveDebouncing.close(); super.dispose(); } @@ -76,7 +81,7 @@ class _Aria2SettingsPageState extends State { ), TextField( controller: rpcUrlController, - onChanged: (value) => downloadSetting.saveAria2RpcUrl(value.trim()), + onChanged: (value) => saveDebouncing.debounce(() => downloadSetting.saveAria2RpcUrl(value.trim())), decoration: InputDecoration( labelText: 'aria2RpcUrl'.tr, hintText: 'aria2RpcUrlHint'.tr, @@ -87,7 +92,7 @@ class _Aria2SettingsPageState extends State { TextField( controller: secretController, obscureText: obscureSecret, - onChanged: (value) => downloadSetting.saveAria2Secret(value.trim()), + onChanged: (value) => saveDebouncing.debounce(() => downloadSetting.saveAria2Secret(value.trim())), decoration: InputDecoration( labelText: 'aria2Secret'.tr, border: const OutlineInputBorder(), @@ -100,7 +105,7 @@ class _Aria2SettingsPageState extends State { const SizedBox(height: 12), TextField( controller: dirController, - onChanged: (value) => downloadSetting.saveAria2DownloadDir(value.trim()), + onChanged: (value) => saveDebouncing.debounce(() => downloadSetting.saveAria2DownloadDir(value.trim())), decoration: InputDecoration( labelText: 'aria2DownloadDir'.tr, hintText: 'aria2DownloadDirHint'.tr, @@ -110,7 +115,7 @@ class _Aria2SettingsPageState extends State { const SizedBox(height: 12), TextField( controller: filenameTemplateController, - onChanged: (value) => downloadSetting.saveAria2FilenameTemplate(value.trim()), + onChanged: (value) => saveDebouncing.debounce(() => downloadSetting.saveAria2FilenameTemplate(value.trim())), decoration: InputDecoration( labelText: 'aria2FilenameTemplate'.tr, hintText: 'aria2FilenameTemplateHint'.tr, @@ -147,22 +152,17 @@ class _Aria2SettingsPageState extends State { ListTile( contentPadding: EdgeInsets.zero, title: Text('testConnection'.tr), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - LoadingStateIndicator( - loadingState: testConnectionState, - useCupertinoIndicator: true, - idleWidgetBuilder: () => const Icon(Icons.keyboard_arrow_right), - successWidgetBuilder: () => const Icon(Icons.check_circle_outline), - errorWidgetBuilder: () => const Icon(Icons.error_outline), - ), - ], + trailing: LoadingStateIndicator( + loadingState: testConnectionState, + useCupertinoIndicator: true, + idleWidgetBuilder: () => const Icon(Icons.keyboard_arrow_right), + successWidgetBuilder: () => const Icon(Icons.check_circle_outline), + errorWidgetBuilder: () => const Icon(Icons.error_outline), ), onTap: _testConnection, ), ], - ), + ).withListTileTheme(context), ); } diff --git a/lib/src/widget/eh_archive_dialog.dart b/lib/src/widget/eh_archive_dialog.dart index 7ebf17bac..cf444cc99 100644 --- a/lib/src/widget/eh_archive_dialog.dart +++ b/lib/src/widget/eh_archive_dialog.dart @@ -76,8 +76,8 @@ class _EHArchiveDialogState extends State { children: [ EHGroupNameSelector(candidates: candidates, currentGroup: group, listener: (g) => group = g), if (downloadSetting.enableAria2Push.value) - Transform.translate( - offset: const Offset(0, -16), + Padding( + padding: const EdgeInsets.only(top: 4), child: CheckboxListTile( value: pushToAria2, onChanged: (value) => setState(() => pushToAria2 = value ?? false), @@ -88,7 +88,7 @@ class _EHArchiveDialogState extends State { contentPadding: EdgeInsets.zero, controlAffinity: ListTileControlAffinity.leading, dense: true, - visualDensity: const VisualDensity(horizontal: 0, vertical: -4), + visualDensity: VisualDensity.compact, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, ), ), From dfdc26c63a9285a98fe218643dd42e0fa14ae566 Mon Sep 17 00:00:00 2001 From: Wen_He Date: Tue, 17 Mar 2026 23:43:45 +0800 Subject: [PATCH 12/16] fix(ui): stabilize aria2 test tile and compact archive push option --- .../download/aria2/aria2_settings_page.dart | 21 ++++++---- lib/src/widget/eh_archive_dialog.dart | 39 +++++++++++-------- 2 files changed, 37 insertions(+), 23 deletions(-) diff --git a/lib/src/pages/setting/download/aria2/aria2_settings_page.dart b/lib/src/pages/setting/download/aria2/aria2_settings_page.dart index a35177108..94bb97dd4 100644 --- a/lib/src/pages/setting/download/aria2/aria2_settings_page.dart +++ b/lib/src/pages/setting/download/aria2/aria2_settings_page.dart @@ -152,13 +152,7 @@ class _Aria2SettingsPageState extends State { ListTile( contentPadding: EdgeInsets.zero, title: Text('testConnection'.tr), - trailing: LoadingStateIndicator( - loadingState: testConnectionState, - useCupertinoIndicator: true, - idleWidgetBuilder: () => const Icon(Icons.keyboard_arrow_right), - successWidgetBuilder: () => const Icon(Icons.check_circle_outline), - errorWidgetBuilder: () => const Icon(Icons.error_outline), - ), + trailing: _buildTestConnectionTrailing(), onTap: _testConnection, ), ], @@ -198,6 +192,19 @@ class _Aria2SettingsPageState extends State { } } + Widget _buildTestConnectionTrailing() { + if (testConnectionState == LoadingState.loading) { + return const CupertinoActivityIndicator(); + } + if (testConnectionState == LoadingState.success) { + return const Icon(Icons.check_circle_outline); + } + if (testConnectionState == LoadingState.error) { + return const Icon(Icons.error_outline); + } + return const Icon(Icons.keyboard_arrow_right); + } + Future _showAria2FilenameTemplateHelp() async { await showDialog( context: context, diff --git a/lib/src/widget/eh_archive_dialog.dart b/lib/src/widget/eh_archive_dialog.dart index cf444cc99..082f3fdcd 100644 --- a/lib/src/widget/eh_archive_dialog.dart +++ b/lib/src/widget/eh_archive_dialog.dart @@ -76,28 +76,35 @@ class _EHArchiveDialogState extends State { children: [ EHGroupNameSelector(candidates: candidates, currentGroup: group, listener: (g) => group = g), if (downloadSetting.enableAria2Push.value) - Padding( - padding: const EdgeInsets.only(top: 4), - child: CheckboxListTile( - value: pushToAria2, - onChanged: (value) => setState(() => pushToAria2 = value ?? false), - title: Text( - 'pushArchiveToAria2'.tr, - style: TextStyle(fontSize: UIConfig.archiveDialogDownloadTextSize + 1), - ), - contentPadding: EdgeInsets.zero, - controlAffinity: ListTileControlAffinity.leading, - dense: true, - visualDensity: VisualDensity.compact, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - ), + _buildPushToAria2Option(), if (archive.creditCount != null && archive.gpCount != null) EHAsset(gpCount: archive.gpCount!, creditCount: archive.creditCount!).marginOnly(top: 12), Expanded(child: _buildButtons().marginOnly(top: 12)), ], ); } + Widget _buildPushToAria2Option() { + return InkWell( + onTap: () => setState(() => pushToAria2 = !pushToAria2), + child: Row( + children: [ + Checkbox( + value: pushToAria2, + onChanged: (value) => setState(() => pushToAria2 = value ?? false), + visualDensity: VisualDensity.compact, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + Expanded( + child: Text( + 'pushArchiveToAria2'.tr, + style: TextStyle(fontSize: UIConfig.archiveDialogDownloadTextSize + 1), + ), + ), + ], + ), + ); + } + Widget _buildButtons() { return Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, From b3ee93bef11539963fb59e5fac2661f8a360938c Mon Sep 17 00:00:00 2001 From: Wen_He Date: Tue, 17 Mar 2026 23:51:54 +0800 Subject: [PATCH 13/16] fix(aria2): import cupertino for test connection indicator --- lib/src/pages/setting/download/aria2/aria2_settings_page.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/pages/setting/download/aria2/aria2_settings_page.dart b/lib/src/pages/setting/download/aria2/aria2_settings_page.dart index 94bb97dd4..8088eacdd 100644 --- a/lib/src/pages/setting/download/aria2/aria2_settings_page.dart +++ b/lib/src/pages/setting/download/aria2/aria2_settings_page.dart @@ -1,4 +1,5 @@ import 'package:dio/dio.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:jhentai/src/extension/widget_extension.dart'; From b784d1fe829b7774cdba62e2ddf50f93632ad5df Mon Sep 17 00:00:00 2001 From: Wen_He Date: Wed, 18 Mar 2026 00:08:47 +0800 Subject: [PATCH 14/16] refactor(aria2): remove unused headers option from addUri --- lib/src/service/aria2_service.dart | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/src/service/aria2_service.dart b/lib/src/service/aria2_service.dart index ec7da5234..127ad4735 100644 --- a/lib/src/service/aria2_service.dart +++ b/lib/src/service/aria2_service.dart @@ -52,7 +52,6 @@ class Aria2Service { required String uri, String? out, String? dir, - List? headers, }) async { Dio dio = Dio(BaseOptions( connectTimeout: Duration(milliseconds: downloadSetting.aria2ConnectTimeout.value), @@ -72,9 +71,6 @@ class Aria2Service { if (dir != null && dir.isNotEmpty) { options['dir'] = dir; } - if (headers != null && headers.isNotEmpty) { - options['header'] = headers; - } if (options.isNotEmpty) { params.add(options); } From ff266154d478063aae301dd75354ed71c6c745fb Mon Sep 17 00:00:00 2001 From: Wen_He Date: Wed, 18 Mar 2026 00:53:51 +0800 Subject: [PATCH 15/16] refactor(archive): align aria2 resolve flow with existing patterns --- .../setting/download/setting_download_page.dart | 1 - lib/src/service/archive_download_service.dart | 16 ++++++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/lib/src/pages/setting/download/setting_download_page.dart b/lib/src/pages/setting/download/setting_download_page.dart index 44884689e..c80aa301d 100644 --- a/lib/src/pages/setting/download/setting_download_page.dart +++ b/lib/src/pages/setting/download/setting_download_page.dart @@ -468,5 +468,4 @@ class _SettingDownloadPageState extends State { isShort: false, ); } - } diff --git a/lib/src/service/archive_download_service.dart b/lib/src/service/archive_download_service.dart index 992faccaa..6b1cf7a6d 100644 --- a/lib/src/service/archive_download_service.dart +++ b/lib/src/service/archive_download_service.dart @@ -55,6 +55,8 @@ class ArchiveDownloadService extends GetxController with GridBasePageServiceMixi static const String archiveSpeedComputerId = 'archiveSpeedComputerId'; static const int _maxRetryTimes = 3; + static const int _maxAria2ResolvePollingTimes = 12; + static const Duration _aria2ResolvePollingInterval = Duration(milliseconds: 1000); static const String metadataFileName = 'ametadata'; static const int _maxTitleLength = 80; static const int _maxIsolateCountsTotal = 10; @@ -440,6 +442,8 @@ class ArchiveDownloadService extends GetxController with GridBasePageServiceMixi ArchiveDownloadedData archive, { bool reParse = false, }) async { + CancelToken? cancelToken = archiveDownloadInfos[archive.gid]?.cancelToken; + try { if (archive.parseSource == ArchiveParseSource.official.code) { String? downloadPageUrl = archiveDownloadInfos[archive.gid]?.downloadPageUrl; @@ -453,6 +457,7 @@ class ArchiveDownloadService extends GetxController with GridBasePageServiceMixi String downloadPath = await retry( () => ehRequest.get( url: downloadPageUrl!, + cancelToken: cancelToken, parser: EHSpiderParser.downloadArchivePage2DownloadUrl, ), retryIf: (e) => e is DioException && e.type != DioExceptionType.cancel, @@ -475,6 +480,7 @@ class ArchiveDownloadService extends GetxController with GridBasePageServiceMixi gid: archive.gid, token: archive.token, reParse: reParse, + cancelToken: cancelToken, parser: ArchiveBotResponseParser.commonParse, ), retryIf: (e) => e is DioException && e.type != DioExceptionType.cancel, @@ -489,6 +495,9 @@ class ArchiveDownloadService extends GetxController with GridBasePageServiceMixi ArchiveResolveVO archiveResolveVO = ArchiveResolveVO.fromResponse(response.data); return archiveResolveVO.url; } on DioException catch (e) { + if (e.type == DioExceptionType.cancel) { + return null; + } snack('aria2PushFailed'.tr, e.error?.toString() ?? e.message ?? e.toString(), isShort: true); return null; } on EHSiteException catch (e) { @@ -501,11 +510,14 @@ class ArchiveDownloadService extends GetxController with GridBasePageServiceMixi } Future _resolveOfficialDownloadPageUrl(ArchiveDownloadedData archive) async { - for (int i = 0; i < 12; i++) { + CancelToken? cancelToken = archiveDownloadInfos[archive.gid]?.cancelToken; + + for (int i = 0; i < _maxAria2ResolvePollingTimes; i++) { ArchiveUnlockResult result = await retry( () => ehRequest.requestUnlockArchive( url: archive.archivePageUrl.replaceFirst('--', '-'), isOriginal: archive.isOriginal, + cancelToken: cancelToken, parser: EHSpiderParser.unlockArchivePage2DownloadArchivePageUrl, ), retryIf: (e) => e is DioException && e.type != DioExceptionType.cancel, @@ -521,7 +533,7 @@ class ArchiveDownloadService extends GetxController with GridBasePageServiceMixi return result.url!; } - await Future.delayed(const Duration(milliseconds: 1000)); + await Future.delayed(_aria2ResolvePollingInterval); } snack('aria2PushFailed'.tr, 'archiveError'.tr, isShort: true); From f4025545b5fc45026138b20b9594cee7887c6315 Mon Sep 17 00:00:00 2001 From: Wen_He Date: Wed, 18 Mar 2026 01:21:11 +0800 Subject: [PATCH 16/16] fix(aria2): guard push flow and disable default switch when off --- lib/src/pages/details/details_page_logic.dart | 2 +- .../setting/download/aria2/aria2_settings_page.dart | 12 +++++++----- lib/src/widget/eh_archive_dialog.dart | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/src/pages/details/details_page_logic.dart b/lib/src/pages/details/details_page_logic.dart index 295e89b64..d6eab2493 100644 --- a/lib/src/pages/details/details_page_logic.dart +++ b/lib/src/pages/details/details_page_logic.dart @@ -652,7 +652,7 @@ class DetailsPageLogic extends GetxController with LoginRequiredMixin, Scroll2To } ArchiveDownloadedData archive = _buildArchiveTaskFromDialogResult(result); - if (result.pushToAria2) { + if (downloadSetting.enableAria2Push.value && result.pushToAria2) { await archiveDownloadService.pushArchiveToAria2(archive); return; } diff --git a/lib/src/pages/setting/download/aria2/aria2_settings_page.dart b/lib/src/pages/setting/download/aria2/aria2_settings_page.dart index 8088eacdd..e6344a12a 100644 --- a/lib/src/pages/setting/download/aria2/aria2_settings_page.dart +++ b/lib/src/pages/setting/download/aria2/aria2_settings_page.dart @@ -74,11 +74,13 @@ class _Aria2SettingsPageState extends State { contentPadding: EdgeInsets.zero, title: Text('aria2DefaultPushSelected'.tr), subtitle: Text('aria2DefaultPushSelectedHint'.tr), - value: defaultPushSelected, - onChanged: (value) async { - setState(() => defaultPushSelected = value); - await downloadSetting.saveAria2DefaultPushSelected(value); - }, + value: enableAria2Push && defaultPushSelected, + onChanged: !enableAria2Push + ? null + : (value) async { + setState(() => defaultPushSelected = value); + await downloadSetting.saveAria2DefaultPushSelected(value); + }, ), TextField( controller: rpcUrlController, diff --git a/lib/src/widget/eh_archive_dialog.dart b/lib/src/widget/eh_archive_dialog.dart index 082f3fdcd..103359358 100644 --- a/lib/src/widget/eh_archive_dialog.dart +++ b/lib/src/widget/eh_archive_dialog.dart @@ -48,7 +48,7 @@ class _EHArchiveDialogState extends State { super.initState(); group = widget.currentGroup ?? widget.candidates.firstOrNull ?? 'default'.tr; - pushToAria2 = downloadSetting.aria2DefaultPushSelected.value; + pushToAria2 = downloadSetting.enableAria2Push.value && downloadSetting.aria2DefaultPushSelected.value; candidates = List.of(widget.candidates); candidates.remove(group); candidates.insert(0, group);