Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions lib/src/l18n/en_US.dart
Original file line number Diff line number Diff line change
Expand Up @@ -718,6 +718,32 @@ 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',
'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',
'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',
'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',
Expand Down
26 changes: 26 additions & 0 deletions lib/src/l18n/zh_CN.dart
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,32 @@ 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默认目录',
'aria2FilenameTemplate': '文件名模板',
'aria2FilenameTemplateHint': 'ArchiveV2 - {gid} - {title}.zip',
'aria2FilenameTemplateHelp': '文件名模板说明',
'aria2FilenameTemplateOptionGid': '{gid}:画廊ID',
'aria2FilenameTemplateOptionTitle': '{title}:标题',
'aria2FilenameTemplateOptionUploader': '{uploader}:上传者',
'aria2FilenameTemplateOptionCategory': '{category}:分类',
'aria2FilenameTemplateExample': '示例:ArchiveV2 - {gid} - {title}.zip',
'aria2DefaultPushSelected': '默认选中推送至aria2',
'aria2DefaultPushSelectedHint': '在归档弹窗中默认勾选“推送归档至aria2”',
'aria2ConnectTimeout': 'RPC超时',
'testConnection': '测试连接',
'aria2ConnectionSucceeded': 'aria2连接成功',
'aria2ConnectionFailed': 'aria2连接失败',
'pushArchiveToAria2': '推送归档至aria2',
'aria2PushSuccess': '已推送到aria2:%s',
'aria2PushFailed': '推送到aria2失败',
'aria2NotConfiguredHint': '请先在下载设置中启用并配置aria2',

/// archive bot settings
'archiveBotSettings': '归档机器人设置',
Expand Down
52 changes: 30 additions & 22 deletions lib/src/pages/details/details_page_logic.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -651,27 +651,11 @@ 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);
if (downloadSetting.enableAria2Push.value && result.pushToAria2) {
await archiveDownloadService.pushArchiveToAria2(archive);
return;
}
archiveDownloadService.downloadArchive(archive);

updateGlobalGalleryStatus();
Expand Down Expand Up @@ -722,6 +706,30 @@ class DetailsPageLogic extends GetxController with LoginRequiredMixin, Scroll2To
}
}

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,
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<void> handleTapHH() async {
if (!userSetting.hasLoggedIn()) {
showLoginToast();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -228,6 +229,14 @@ mixin ArchiveDownloadPageLogicMixin on GetxController
changeParseSource(archive.gid, ArchiveParseSource.bot);
},
),
if (downloadSetting.enableAria2Push.value)
CupertinoActionSheetAction(
child: Text('pushArchiveToAria2'.tr),
onPressed: () {
backRoute();
archiveDownloadService.pushArchiveToAria2(archive);
},
),
CupertinoActionSheetAction(
child: Text('changeGroup'.tr),
onPressed: () {
Expand Down
232 changes: 232 additions & 0 deletions lib/src/pages/setting/download/aria2/aria2_settings_page.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
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';
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);

@override
State<Aria2SettingsPage> createState() => _Aria2SettingsPageState();
}

class _Aria2SettingsPageState extends State<Aria2SettingsPage> {
late final TextEditingController rpcUrlController;
late final TextEditingController secretController;
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;
LoadingState testConnectionState = LoadingState.idle;
late final Debouncing saveDebouncing;

@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);
saveDebouncing = Debouncing(duration: const Duration(milliseconds: 400));
}

@override
void dispose() {
rpcUrlController.dispose();
secretController.dispose();
dirController.dispose();
filenameTemplateController.dispose();
saveDebouncing.close();
super.dispose();
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text('aria2Settings'.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: enableAria2Push && defaultPushSelected,
onChanged: !enableAria2Push
? null
: (value) async {
setState(() => defaultPushSelected = value);
await downloadSetting.saveAria2DefaultPushSelected(value);
},
),
TextField(
controller: rpcUrlController,
onChanged: (value) => saveDebouncing.debounce(() => downloadSetting.saveAria2RpcUrl(value.trim())),
decoration: InputDecoration(
labelText: 'aria2RpcUrl'.tr,
hintText: 'aria2RpcUrlHint'.tr,
border: const OutlineInputBorder(),
),
),
const SizedBox(height: 12),
TextField(
controller: secretController,
obscureText: obscureSecret,
onChanged: (value) => saveDebouncing.debounce(() => downloadSetting.saveAria2Secret(value.trim())),
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,
onChanged: (value) => saveDebouncing.debounce(() => downloadSetting.saveAria2DownloadDir(value.trim())),
decoration: InputDecoration(
labelText: 'aria2DownloadDir'.tr,
hintText: 'aria2DownloadDirHint'.tr,
border: const OutlineInputBorder(),
),
),
const SizedBox(height: 12),
TextField(
controller: filenameTemplateController,
onChanged: (value) => saveDebouncing.debounce(() => downloadSetting.saveAria2FilenameTemplate(value.trim())),
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<int>(
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) {
setState(() => timeout = value);
downloadSetting.saveAria2ConnectTimeout(value);
}
},
),
const SizedBox(height: 12),
ListTile(
contentPadding: EdgeInsets.zero,
title: Text('testConnection'.tr),
trailing: _buildTestConnectionTrailing(),
onTap: _testConnection,
),
],
).withListTileTheme(context),
);
}

Future<void> _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);
}
}

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<void> _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),
),
],
),
);
}
}
Loading