mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 07:47:24 +00:00
Compare commits
45 Commits
Author | SHA1 | Date | |
---|---|---|---|
fec1926774 | |||
![]() |
7cd0a20785 | ||
ed124d0419 | |||
14c3e9ea43 | |||
d2aca7ce44 | |||
34194559f5 | |||
18c5d5d85a | |||
9b1bafcbe1 | |||
dd7e2d6744 | |||
51c2bf0d6f | |||
53e5ebbbf6 | |||
c600d99c58 | |||
f4804faf52 | |||
c7d72347a9 | |||
a4e2d4f6e4 | |||
5c7cd7a304 | |||
9fb63e47ea | |||
fc66e8ae2d | |||
d04c872491 | |||
426936082e | |||
5129530e56 | |||
3735249de6 | |||
![]() |
8868a02a7e | ||
![]() |
e1b95c9e23 | ||
0b65b4ab53 | |||
df4263f969 | |||
17ef17ca5b | |||
![]() |
e55c45a589 | ||
591f2836d4 | |||
8ab4f7a34b | |||
614c01872b | |||
6be258092a | |||
ce50812857 | |||
f0b1135eb7 | |||
![]() |
cc0f070df5 | ||
35429c132c | |||
998d4c31d3 | |||
0122bb8f28 | |||
33a9fa062b | |||
13081332f2 | |||
![]() |
cdc6c95579 | ||
![]() |
3aca3baafc | ||
58d6ccdde1 | |||
23404b86f6 | |||
![]() |
965187e9de |
7
.github/ISSUE_TEMPLATE/bug.yaml
vendored
7
.github/ISSUE_TEMPLATE/bug.yaml
vendored
@@ -7,6 +7,10 @@ body:
|
||||
attributes:
|
||||
value: |
|
||||
Thank you for reporting a problem, please complete the title and fill in the following information.
|
||||
|
||||
**Please do not report any issues related to config files.**
|
||||
|
||||
To report a bug related to the config file, please send it to the [config repository](https://github.com/venera-app/venera-configs).
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
@@ -19,7 +23,8 @@ body:
|
||||
attributes:
|
||||
label: Version
|
||||
description: |
|
||||
App version
|
||||
App version.
|
||||
|
||||
Please try to update if it is not the latest version
|
||||
validations:
|
||||
required: true
|
||||
|
61
.github/workflows/main.yml
vendored
61
.github/workflows/main.yml
vendored
@@ -39,12 +39,18 @@ jobs:
|
||||
ln -s /Applications dist/dmg_contents/Applications
|
||||
hdiutil create -volname "venera" -srcfolder dist/dmg_contents -ov -format UDZO "dist/venera.dmg"
|
||||
|
||||
- name: Add version to filename
|
||||
run: |
|
||||
APP_VERSION=$(grep "version:" pubspec.yaml | cut -d':' -f2 | tr -d ' ')
|
||||
mkdir -p result
|
||||
mv dist/venera.dmg result/venera-$APP_VERSION.dmg
|
||||
|
||||
# Step 4: Attach and upload artifacts (optional)
|
||||
- name: Upload DMG
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: venera.dmg
|
||||
path: dist/venera.dmg
|
||||
name: macos_build
|
||||
path: result/
|
||||
Build_IOS:
|
||||
runs-on: macos-15
|
||||
steps:
|
||||
@@ -62,12 +68,17 @@ jobs:
|
||||
mv /Users/runner/work/venera/venera/build/ios/iphoneos/Runner.app /Users/runner/work/venera/venera/build/ios/iphoneos/Payload
|
||||
cd /Users/runner/work/venera/venera/build/ios/iphoneos/
|
||||
zip -r venera-ios.ipa Payload
|
||||
- name: Add version to filename
|
||||
run: |
|
||||
APP_VERSION=$(grep "version:" pubspec.yaml | cut -d':' -f2 | tr -d ' ')
|
||||
mkdir -p result
|
||||
mv build/ios/iphoneos/venera-ios.ipa result/venera-ios-$APP_VERSION.ipa
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: app-ios.ipa
|
||||
path: /Users/runner/work/venera/venera/build/ios/iphoneos/venera-ios.ipa
|
||||
name: ios_build
|
||||
path: result/
|
||||
Build_Android:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: subosito/flutter-action@v2
|
||||
@@ -118,7 +129,7 @@ jobs:
|
||||
name: windows_build
|
||||
path: build/windows/Venera-*
|
||||
Build_Linux:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: subosito/flutter-action@v2
|
||||
@@ -130,7 +141,7 @@ jobs:
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y ninja-build libgtk-3-dev webkit2gtk-4.1
|
||||
dart pub global activate flutter_to_debian
|
||||
- run: python3 debian/build.py
|
||||
- run: python3 debian/build.py x64
|
||||
- run: dart run flutter_to_arch
|
||||
- run: |
|
||||
sudo rm -rf build/linux/arch/app.tar.gz
|
||||
@@ -145,19 +156,43 @@ jobs:
|
||||
with:
|
||||
name: arch_build
|
||||
path: build/linux/arch/
|
||||
Build_Linux_ARM64:
|
||||
runs-on: ubuntu-22.04-arm
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Flutter
|
||||
run: |
|
||||
FLUTTER_VERSION=$(grep " flutter:" pubspec.yaml | cut -d':' -f2 | tr -d ' ')
|
||||
sudo apt-get update -y && sudo apt-get upgrade -y;
|
||||
sudo apt-get install -y curl git unzip xz-utils zip libglu1-mesa clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev libstdc++-12-dev
|
||||
git clone --depth 1 --branch $FLUTTER_VERSION https://github.com/flutter/flutter.git $RUNNER_TEMP/flutter
|
||||
echo "$RUNNER_TEMP/flutter/bin" >> $GITHUB_PATH
|
||||
- name: Install Flutter
|
||||
run: flutter doctor
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
flutter pub get
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y ninja-build libgtk-3-dev webkit2gtk-4.1
|
||||
dart pub global activate flutter_to_debian
|
||||
- run: python3 debian/build.py arm64
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: deb_arm64_build
|
||||
path: build/linux/x64/release/debian # This is a bug related to flutter_to_debian, but it's not a big deal.
|
||||
|
||||
Release:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [Build_MacOS, Build_IOS, Build_Android, Build_Windows, Build_Linux]
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [Build_MacOS, Build_IOS, Build_Android, Build_Windows, Build_Linux, Build_Linux_ARM64]
|
||||
if: github.event_name == 'release' # 仅在 push 事件时执行
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: venera.dmg
|
||||
name: macos_build
|
||||
path: outputs
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: app-ios.ipa
|
||||
name: ios_build
|
||||
path: outputs
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
@@ -175,6 +210,10 @@ jobs:
|
||||
with:
|
||||
name: arch_build
|
||||
path: outputs
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: deb_arm64_build
|
||||
path: outputs
|
||||
- uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ github.ref_name }}
|
||||
|
@@ -496,7 +496,7 @@ let Network = {
|
||||
/**
|
||||
* [fetch] function for sending HTTP requests. Same api as the browser fetch.
|
||||
* @param url {string}
|
||||
* @param options {{method: string, headers: Object, body: any}}
|
||||
* @param [options] {{method?: string, headers?: Object, body?: any}}
|
||||
* @returns {Promise<{ok: boolean, status: number, statusText: string, headers: {}, arrayBuffer: (function(): Promise<ArrayBuffer>), text: (function(): Promise<string>), json: (function(): Promise<any>)}>}
|
||||
* @since 1.2.0
|
||||
*/
|
||||
@@ -921,7 +921,7 @@ function Comic({id, title, subtitle, subTitle, cover, tags, description, maxPage
|
||||
* @param description {string?}
|
||||
* @param tags {Map<string, string[]> | {} | null | undefined}
|
||||
* @param chapters {Map<string, string> | {} | null | undefined} - key: chapter id, value: chapter title
|
||||
* @param isFavorite {boolean | null | undefined} - favorite status. If the comic source supports multiple folders, this field should be null
|
||||
* @param isFavorite {boolean | null | undefined} - favorite status.
|
||||
* @param subId {string?} - a param which is passed to comments api
|
||||
* @param thumbnails {string[]?} - for multiple page thumbnails, set this to null, and use `loadThumbnails` api to load thumbnails
|
||||
* @param recommend {Comic[]?} - related comics
|
||||
@@ -1086,6 +1086,19 @@ class ComicSource {
|
||||
});
|
||||
}
|
||||
|
||||
translation = {}
|
||||
|
||||
/**
|
||||
* Translate given string with the current locale using the translation object.
|
||||
* @param key {string}
|
||||
* @returns {string}
|
||||
* @since 1.2.5
|
||||
*/
|
||||
translate(key) {
|
||||
let locale = APP.locale;
|
||||
return this.translation[locale]?.[key] ?? key;
|
||||
}
|
||||
|
||||
init() { }
|
||||
|
||||
static sources = {}
|
||||
|
@@ -139,8 +139,8 @@
|
||||
"Block": "屏蔽",
|
||||
"Add new favorite to": "添加新收藏到",
|
||||
"Move favorite after reading": "阅读后移动收藏",
|
||||
"Delete folder?" : "刪除文件夾?",
|
||||
"Delete folder '@f' ?" : "删除文件夹 '@f' ?",
|
||||
"Delete folder?" : "删除文件夹?",
|
||||
"Delete folder '@f' ?" : "删除文件夹 '@f' ?",
|
||||
"Import from file": "从文件导入",
|
||||
"Failed to import": "导入失败",
|
||||
"Cache Limit": "缓存限制",
|
||||
@@ -321,7 +321,18 @@
|
||||
"Manage": "管理",
|
||||
"Verify": "验证",
|
||||
"Cloudflare verification required": "需要Cloudflare验证",
|
||||
"Success": "成功"
|
||||
"Success": "成功",
|
||||
"Compressing": "压缩中",
|
||||
"Exporting": "导出中",
|
||||
"Search Sources": "搜索源",
|
||||
"Removed": "已移除",
|
||||
"Added to favorites": "已添加到收藏",
|
||||
"Not added": "未添加",
|
||||
"Create a folder": "新建收藏夹",
|
||||
"Created successfully": "创建成功",
|
||||
"name": "名称",
|
||||
"Reverse tap to turn Pages": "反转点击翻页",
|
||||
"Show all": "显示全部"
|
||||
},
|
||||
"zh_TW": {
|
||||
"Home": "首頁",
|
||||
@@ -645,6 +656,17 @@
|
||||
"Manage": "管理",
|
||||
"Verify": "驗證",
|
||||
"Cloudflare verification required": "需要Cloudflare驗證",
|
||||
"Success": "成功"
|
||||
"Success": "成功",
|
||||
"Compressing": "壓縮中",
|
||||
"Exporting": "匯出中",
|
||||
"Search Sources": "搜索源",
|
||||
"Removed": "已移除",
|
||||
"Added to favorites": "已添加到收藏",
|
||||
"Not added": "未添加",
|
||||
"Create a folder": "新建收藏夾",
|
||||
"Created successfully": "創建成功",
|
||||
"name": "名稱",
|
||||
"Reverse tap to turn Pages": "反轉點擊翻頁",
|
||||
"Show all": "顯示全部"
|
||||
}
|
||||
}
|
11
debian/build.py
vendored
11
debian/build.py
vendored
@@ -1,5 +1,7 @@
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
arch = sys.argv[1]
|
||||
debianContent = ''
|
||||
desktopContent = ''
|
||||
version = ''
|
||||
@@ -12,7 +14,14 @@ with open('pubspec.yaml', 'r') as f:
|
||||
version = str.split(str.split(f.read(), 'version: ')[1], '+')[0]
|
||||
|
||||
with open('debian/debian.yaml', 'w') as f:
|
||||
f.write(debianContent.replace('{{Version}}', version))
|
||||
content = debianContent.replace('{{Version}}', version)
|
||||
if arch == 'x64':
|
||||
content = content.replace('{{Arch}}', 'x64')
|
||||
content = content.replace('{{Architecture}}', 'amd64')
|
||||
elif arch == 'arm64':
|
||||
content = content.replace('{{Arch}}', 'arm64')
|
||||
content = content.replace('{{Architecture}}', 'arm64')
|
||||
f.write(content)
|
||||
with open('debian/gui/venera.desktop', 'w') as f:
|
||||
f.write(desktopContent.replace('{{Version}}', version))
|
||||
|
||||
|
6
debian/debian.yaml
vendored
6
debian/debian.yaml
vendored
@@ -1,13 +1,13 @@
|
||||
flutter_app:
|
||||
command: venera
|
||||
arch: x64
|
||||
arch: {{Arch}}
|
||||
parent: /usr/local/lib
|
||||
nonInteractive: false
|
||||
nonInteractive: true
|
||||
|
||||
control:
|
||||
Package: venera
|
||||
Version: {{Version}}
|
||||
Architecture: amd64
|
||||
Architecture: {{Architecture}}
|
||||
Priority: optional
|
||||
Depends: libwebkit2gtk-4.1-0, libgtk-3-0
|
||||
Maintainer: nyne
|
||||
|
@@ -550,7 +550,7 @@ class _ComicDescription extends StatelessWidget {
|
||||
int cnt = (constraints.maxHeight - 22).toInt() ~/ 25;
|
||||
return Container(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
height: 22 + cnt * 25,
|
||||
height: 21 + cnt * 24,
|
||||
width: double.infinity,
|
||||
decoration: const BoxDecoration(),
|
||||
child: Wrap(
|
||||
@@ -562,31 +562,30 @@ class _ComicDescription extends StatelessWidget {
|
||||
children: [
|
||||
for (var s in tags!)
|
||||
Container(
|
||||
height: 22,
|
||||
padding: const EdgeInsets.fromLTRB(3, 2, 3, 2),
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: constraints.maxWidth * 0.45,
|
||||
height: 21,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: constraints.maxWidth * 0.45,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: s == "Unavailable"
|
||||
? context.colorScheme.errorContainer
|
||||
: context.colorScheme.secondaryContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Center(
|
||||
widthFactor: 1,
|
||||
child: Text(
|
||||
enableTranslate
|
||||
? TagsTranslation.translateTag(s)
|
||||
: s.split(':').last,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
softWrap: true,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: s == "Unavailable"
|
||||
? Theme.of(context).colorScheme.errorContainer
|
||||
: Theme.of(context)
|
||||
.colorScheme
|
||||
.secondaryContainer,
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
child: Center(
|
||||
widthFactor: 1,
|
||||
child: Text(
|
||||
enableTranslate
|
||||
? TagsTranslation.translateTag(s)
|
||||
: s.split(':').last,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
softWrap: true,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
))),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
).toAlign(Alignment.topCenter);
|
||||
@@ -607,23 +606,26 @@ class _ComicDescription extends StatelessWidget {
|
||||
style: const TextStyle(
|
||||
fontSize: 12.0,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (badge != null)
|
||||
Container(
|
||||
padding: const EdgeInsets.fromLTRB(6, 4, 6, 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.tertiaryContainer,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
padding: const EdgeInsets.fromLTRB(6, 4, 6, 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.tertiaryContainer,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
"${badge![0].toUpperCase()}${badge!.substring(1).toLowerCase()}",
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
"${badge![0].toUpperCase()}${badge!.substring(1).toLowerCase()}",
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
)),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
@@ -740,7 +742,7 @@ class _SliverGridComicsState extends State<SliverGridComics> {
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant SliverGridComics oldWidget) {
|
||||
if (oldWidget.comics != widget.comics) {
|
||||
if (oldWidget.comics.isEqualTo(widget.comics)) {
|
||||
comics.clear();
|
||||
for (var comic in widget.comics) {
|
||||
if (isBlocked(comic) == null) {
|
||||
@@ -1520,14 +1522,15 @@ class SimpleComicTile extends StatelessWidget {
|
||||
|
||||
return AnimatedTapRegion(
|
||||
borderRadius: 8,
|
||||
onTap: onTap ?? () {
|
||||
context.to(
|
||||
() => ComicPage(
|
||||
id: comic.id,
|
||||
sourceKey: comic.sourceKey,
|
||||
),
|
||||
);
|
||||
},
|
||||
onTap: onTap ??
|
||||
() {
|
||||
context.to(
|
||||
() => ComicPage(
|
||||
id: comic.id,
|
||||
sourceKey: comic.sourceKey,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
width: 92,
|
||||
height: 114,
|
||||
|
@@ -148,3 +148,18 @@ class SliverGridDelegateWithComics extends SliverGridDelegate {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class SliverLazyToBoxAdapter extends StatelessWidget {
|
||||
/// Creates a sliver that contains a single box widget which can be lazy loaded.
|
||||
const SliverLazyToBoxAdapter({super.key, required this.child});
|
||||
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SliverList.list(children: [
|
||||
SizedBox(),
|
||||
child,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@@ -168,7 +168,15 @@ Future<void> showConfirmDialog({
|
||||
}
|
||||
|
||||
class LoadingDialogController {
|
||||
void Function()? closeDialog;
|
||||
double? _progress;
|
||||
|
||||
String? _message;
|
||||
|
||||
void Function()? _closeDialog;
|
||||
|
||||
void Function(double? value)? _serProgress;
|
||||
|
||||
void Function(String message)? _setMessage;
|
||||
|
||||
bool closed = false;
|
||||
|
||||
@@ -177,63 +185,86 @@ class LoadingDialogController {
|
||||
return;
|
||||
}
|
||||
closed = true;
|
||||
if (closeDialog == null) {
|
||||
Future.microtask(closeDialog!);
|
||||
if (_closeDialog == null) {
|
||||
Future.microtask(_closeDialog!);
|
||||
} else {
|
||||
closeDialog!();
|
||||
_closeDialog!();
|
||||
}
|
||||
}
|
||||
|
||||
void setProgress(double? value) {
|
||||
if (closed) {
|
||||
return;
|
||||
}
|
||||
_serProgress?.call(value);
|
||||
}
|
||||
|
||||
void setMessage(String message) {
|
||||
if (closed) {
|
||||
return;
|
||||
}
|
||||
_setMessage?.call(message);
|
||||
}
|
||||
}
|
||||
|
||||
LoadingDialogController showLoadingDialog(BuildContext context,
|
||||
{void Function()? onCancel,
|
||||
bool barrierDismissible = true,
|
||||
bool allowCancel = true,
|
||||
String? message,
|
||||
String cancelButtonText = "Cancel"}) {
|
||||
LoadingDialogController showLoadingDialog(
|
||||
BuildContext context, {
|
||||
void Function()? onCancel,
|
||||
bool barrierDismissible = true,
|
||||
bool allowCancel = true,
|
||||
String? message,
|
||||
String cancelButtonText = "Cancel",
|
||||
bool withProgress = false,
|
||||
}) {
|
||||
var controller = LoadingDialogController();
|
||||
controller._message = message;
|
||||
|
||||
if (withProgress) {
|
||||
controller._progress = 0;
|
||||
}
|
||||
|
||||
var loadingDialogRoute = DialogRoute(
|
||||
context: context,
|
||||
barrierDismissible: barrierDismissible,
|
||||
builder: (BuildContext context) {
|
||||
return Dialog(
|
||||
child: Container(
|
||||
width: 100,
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(
|
||||
width: 30,
|
||||
height: 30,
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
),
|
||||
Text(
|
||||
message ?? 'Loading',
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
const Spacer(),
|
||||
if (allowCancel)
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
controller.close();
|
||||
onCancel?.call();
|
||||
},
|
||||
child: Text(cancelButtonText.tl))
|
||||
],
|
||||
),
|
||||
),
|
||||
context: context,
|
||||
barrierDismissible: barrierDismissible,
|
||||
builder: (BuildContext context) {
|
||||
return StatefulBuilder(builder: (context, setState) {
|
||||
controller._serProgress = (value) {
|
||||
setState(() {
|
||||
controller._progress = value;
|
||||
});
|
||||
};
|
||||
controller._setMessage = (message) {
|
||||
setState(() {
|
||||
controller._message = message;
|
||||
});
|
||||
};
|
||||
return ContentDialog(
|
||||
title: controller._message ?? 'Loading',
|
||||
content: LinearProgressIndicator(
|
||||
value: controller._progress,
|
||||
backgroundColor: context.colorScheme.surfaceContainer,
|
||||
).paddingHorizontal(16).paddingVertical(16),
|
||||
actions: [
|
||||
FilledButton(
|
||||
onPressed: allowCancel
|
||||
? () {
|
||||
controller.close();
|
||||
onCancel?.call();
|
||||
}
|
||||
: null,
|
||||
child: Text(cancelButtonText.tl),
|
||||
)
|
||||
],
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
var navigator = Navigator.of(context, rootNavigator: true);
|
||||
|
||||
navigator.push(loadingDialogRoute).then((value) => controller.closed = true);
|
||||
|
||||
controller.closeDialog = () {
|
||||
controller._closeDialog = () {
|
||||
navigator.removeRoute(loadingDialogRoute);
|
||||
};
|
||||
|
||||
@@ -444,9 +475,7 @@ Future<int?> showSelectDialog({
|
||||
child: Text('Cancel'.tl),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: current == null
|
||||
? null
|
||||
: context.pop,
|
||||
onPressed: current == null ? null : context.pop,
|
||||
child: Text('Confirm'.tl),
|
||||
),
|
||||
],
|
||||
|
@@ -10,7 +10,7 @@ export "widget_utils.dart";
|
||||
export "context.dart";
|
||||
|
||||
class _App {
|
||||
final version = "1.2.3";
|
||||
final version = "1.2.5";
|
||||
|
||||
bool get isAndroid => Platform.isAndroid;
|
||||
|
||||
@@ -52,7 +52,7 @@ class _App {
|
||||
BuildContext get rootContext => rootNavigatorKey.currentContext!;
|
||||
|
||||
void rootPop() {
|
||||
rootNavigatorKey.currentState?.pop();
|
||||
rootNavigatorKey.currentState?.maybePop();
|
||||
}
|
||||
|
||||
void pop() {
|
||||
|
@@ -126,6 +126,7 @@ class _Settings with ChangeNotifier {
|
||||
'explore_pages': [],
|
||||
'categories': [],
|
||||
'favorites': [],
|
||||
'searchSources': null,
|
||||
'showFavoriteStatusOnTile': true,
|
||||
'showHistoryStatusOnTile': false,
|
||||
'blockedWords': [],
|
||||
@@ -134,6 +135,7 @@ class _Settings with ChangeNotifier {
|
||||
'readerMode': 'galleryLeftToRight', // values of [ReaderMode]
|
||||
'readerScreenPicNumber': 1, // 1 - 5
|
||||
'enableTapToTurnPages': true,
|
||||
'reverseTapToTurnPages': false,
|
||||
'enablePageAnimation': true,
|
||||
'language': 'system', // system, zh-CN, zh-TW, en-US
|
||||
'cacheSize': 2048, // in MB
|
||||
@@ -155,7 +157,7 @@ class _Settings with ChangeNotifier {
|
||||
'customImageProcessing': defaultCustomImageProcessing,
|
||||
'sni': true,
|
||||
'autoAddLanguageFilter': 'none', // none, chinese, english, japanese
|
||||
'comicSourceListUrl': "https://raw.githubusercontent.com/venera-app/venera-configs/master/index.json",
|
||||
'comicSourceListUrl': "https://cdn.jsdelivr.net/gh/venera-app/venera-configs@latest/index.json",
|
||||
};
|
||||
|
||||
operator [](String key) {
|
||||
|
@@ -417,7 +417,7 @@ class SearchOptions {
|
||||
|
||||
const SearchOptions(this.options, this.label, this.type, this.defaultVal);
|
||||
|
||||
String get defaultValue => defaultVal ?? options.keys.first;
|
||||
String get defaultValue => defaultVal ?? options.keys.firstOrNull ?? "";
|
||||
}
|
||||
|
||||
typedef CategoryComicsLoader = Future<Res<List<Comic>>> Function(
|
||||
|
@@ -37,6 +37,8 @@ class FavoriteData {
|
||||
|
||||
final AddOrDelFavFunc? addOrDelFavorite;
|
||||
|
||||
final bool singleFolderForSingleComic;
|
||||
|
||||
const FavoriteData({
|
||||
required this.key,
|
||||
required this.title,
|
||||
@@ -49,6 +51,7 @@ class FavoriteData {
|
||||
this.allFavoritesId,
|
||||
this.addOrDelFavorite,
|
||||
this.isOldToNewSort,
|
||||
this.singleFolderForSingleComic = false,
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -620,6 +620,7 @@ class ComicSourceParser {
|
||||
|
||||
final bool multiFolder = _getValue("favorites.multiFolder");
|
||||
final bool? isOldToNewSort = _getValue("favorites.isOldToNewSort");
|
||||
final bool? singleFolderForSingleComic = _getValue("favorites.singleFolderForSingleComic");
|
||||
|
||||
Future<Res<T>> retryZone<T>(Future<Res<T>> Function() func) async {
|
||||
if (!ComicSource.find(_key!)!.isLogged) {
|
||||
@@ -773,6 +774,7 @@ class ComicSourceParser {
|
||||
deleteFolder: deleteFolder,
|
||||
addOrDelFavorite: addOrDelFavFunc,
|
||||
isOldToNewSort: isOldToNewSort,
|
||||
singleFolderForSingleComic: singleFolderForSingleComic ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -156,7 +156,7 @@ class JsEngine with _JSEngineApi, JsUiApi {
|
||||
case "UI":
|
||||
return handleUIMessage(Map.from(message));
|
||||
case "getLocale":
|
||||
return "${App.locale.languageCode}-${App.locale.countryCode}";
|
||||
return "${App.locale.languageCode}_${App.locale.countryCode}";
|
||||
case "getPlatform":
|
||||
return Platform.operatingSystem;
|
||||
}
|
||||
|
@@ -42,11 +42,16 @@ Future<void> init() async {
|
||||
await ComicSource.init().wait();
|
||||
await LocalManager().init().wait();
|
||||
CacheManager().setLimitSize(appdata.settings['cacheSize']);
|
||||
if (appdata.settings['searchSources'] == null) {
|
||||
appdata.settings['searchSources'] = ComicSource.all()
|
||||
.where((e) => e.searchPageData != null)
|
||||
.map((e) => e.key)
|
||||
.toList();
|
||||
}
|
||||
if (App.isAndroid) {
|
||||
handleLinks();
|
||||
}
|
||||
FlutterError.onError = (details) {
|
||||
Log.error(
|
||||
"Unhandled Exception", "${details.exception}\n${details.stack}");
|
||||
Log.error("Unhandled Exception", "${details.exception}\n${details.stack}");
|
||||
};
|
||||
}
|
@@ -132,6 +132,38 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
||||
};
|
||||
}
|
||||
|
||||
ThemeData getTheme(
|
||||
Color primary,
|
||||
Color? secondary,
|
||||
Color? tertiary,
|
||||
Brightness brightness,
|
||||
) {
|
||||
String? font;
|
||||
List<String>? fallback;
|
||||
if (App.isWindows) {
|
||||
font = 'Segoe UI';
|
||||
fallback = [
|
||||
'Segoe UI',
|
||||
'Microsoft YaHei',
|
||||
'PingFang SC',
|
||||
'Noto Sans CJK',
|
||||
'Arial',
|
||||
'sans-serif'
|
||||
];
|
||||
}
|
||||
return ThemeData(
|
||||
colorScheme: SeedColorScheme.fromSeeds(
|
||||
primaryKey: primary,
|
||||
secondaryKey: secondary,
|
||||
tertiaryKey: tertiary,
|
||||
brightness: brightness,
|
||||
tones: FlexTones.vividBackground(brightness),
|
||||
),
|
||||
fontFamily: font,
|
||||
fontFamilyFallback: fallback,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget home;
|
||||
@@ -158,24 +190,9 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
||||
return MaterialApp(
|
||||
home: home,
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: ThemeData(
|
||||
colorScheme: SeedColorScheme.fromSeeds(
|
||||
primaryKey: primary,
|
||||
secondaryKey: secondary,
|
||||
tertiaryKey: tertiary,
|
||||
tones: FlexTones.vividBackground(Brightness.light),
|
||||
),
|
||||
),
|
||||
theme: getTheme(primary, secondary, tertiary, Brightness.light),
|
||||
navigatorKey: App.rootNavigatorKey,
|
||||
darkTheme: ThemeData(
|
||||
colorScheme: SeedColorScheme.fromSeeds(
|
||||
primaryKey: primary,
|
||||
secondaryKey: secondary,
|
||||
tertiaryKey: tertiary,
|
||||
brightness: Brightness.dark,
|
||||
tones: FlexTones.vividBackground(Brightness.dark),
|
||||
),
|
||||
),
|
||||
darkTheme: getTheme(primary, secondary, tertiary, Brightness.dark),
|
||||
themeMode: switch (appdata.settings['theme_mode']) {
|
||||
'light' => ThemeMode.light,
|
||||
'dark' => ThemeMode.dark,
|
||||
|
@@ -118,7 +118,7 @@ void passCloudflare(CloudflareException e, void Function() onFinished) async {
|
||||
|
||||
// windows version of package `flutter_inappwebview` cannot get some cookies
|
||||
// Using DesktopWebview instead
|
||||
if (App.isLinux || App.isWindows) {
|
||||
if (App.isLinux) {
|
||||
var webview = DesktopWebview(
|
||||
initialUrl: url,
|
||||
onTitleChange: (title, controller) async {
|
||||
@@ -152,6 +152,7 @@ void passCloudflare(CloudflareException e, void Function() onFinished) async {
|
||||
);
|
||||
webview.open();
|
||||
} else {
|
||||
bool success = false;
|
||||
void check(InAppWebViewController controller) async {
|
||||
var head = await controller.evaluateJavascript(
|
||||
source: "document.head.innerHTML") as String;
|
||||
@@ -176,7 +177,10 @@ void passCloudflare(CloudflareException e, void Function() onFinished) async {
|
||||
return;
|
||||
}
|
||||
SingleInstanceCookieJar.instance?.saveFromResponse(uri, cookies);
|
||||
App.rootPop();
|
||||
if (!success) {
|
||||
App.rootPop();
|
||||
success = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -2,6 +2,7 @@ import "package:flutter/material.dart";
|
||||
import 'package:shimmer_animation/shimmer_animation.dart';
|
||||
import "package:venera/components/components.dart";
|
||||
import "package:venera/foundation/app.dart";
|
||||
import "package:venera/foundation/appdata.dart";
|
||||
import "package:venera/foundation/comic_source/comic_source.dart";
|
||||
import "package:venera/pages/search_result_page.dart";
|
||||
import "package:venera/utils/translations.dart";
|
||||
@@ -24,7 +25,18 @@ class _AggregatedSearchPageState extends State<AggregatedSearchPage> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
sources = ComicSource.all().where((e) => e.searchPageData != null).toList();
|
||||
var all = ComicSource.all()
|
||||
.where((e) => e.searchPageData != null)
|
||||
.map((e) => e.key)
|
||||
.toList();
|
||||
var settings = appdata.settings['searchSources'] as List;
|
||||
var sources = <String>[];
|
||||
for (var source in settings) {
|
||||
if (all.contains(source)) {
|
||||
sources.add(source);
|
||||
}
|
||||
}
|
||||
this.sources = sources.map((e) => ComicSource.find(e)!).toList();
|
||||
_keyword = widget.keyword;
|
||||
controller = SearchBarController(
|
||||
currentText: widget.keyword,
|
||||
@@ -46,7 +58,11 @@ class _AggregatedSearchPageState extends State<AggregatedSearchPage> {
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
final source = sources[index];
|
||||
return _SliverSearchResult(source: source, keyword: _keyword);
|
||||
return _SliverSearchResult(
|
||||
key: ValueKey(source.key),
|
||||
source: source,
|
||||
keyword: _keyword,
|
||||
);
|
||||
},
|
||||
childCount: sources.length,
|
||||
),
|
||||
@@ -56,7 +72,11 @@ class _AggregatedSearchPageState extends State<AggregatedSearchPage> {
|
||||
}
|
||||
|
||||
class _SliverSearchResult extends StatefulWidget {
|
||||
const _SliverSearchResult({required this.source, required this.keyword});
|
||||
const _SliverSearchResult({
|
||||
required this.source,
|
||||
required this.keyword,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final ComicSource source;
|
||||
|
||||
@@ -78,6 +98,8 @@ class _SliverSearchResultState extends State<_SliverSearchResult>
|
||||
|
||||
List<Comic>? comics;
|
||||
|
||||
String? error;
|
||||
|
||||
void load() async {
|
||||
final data = widget.source.searchPageData!;
|
||||
var options =
|
||||
@@ -89,6 +111,11 @@ class _SliverSearchResultState extends State<_SliverSearchResult>
|
||||
comics = res.data;
|
||||
isLoading = false;
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
error = res.errorMessage ?? "Unknown error".tl;
|
||||
isLoading = false;
|
||||
});
|
||||
}
|
||||
} else if (data.loadNext != null) {
|
||||
var res = await data.loadNext!(widget.keyword, null, options);
|
||||
@@ -97,6 +124,11 @@ class _SliverSearchResultState extends State<_SliverSearchResult>
|
||||
comics = res.data;
|
||||
isLoading = false;
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
error = res.errorMessage ?? "Unknown error".tl;
|
||||
isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -127,6 +159,9 @@ class _SliverSearchResultState extends State<_SliverSearchResult>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (error != null && error!.startsWith("CloudflareException")) {
|
||||
error = "Cloudflare verification required".tl;
|
||||
}
|
||||
super.build(context);
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
@@ -169,7 +204,7 @@ class _SliverSearchResultState extends State<_SliverSearchResult>
|
||||
}),
|
||||
),
|
||||
)
|
||||
else if (comics == null || comics!.isEmpty)
|
||||
else if (error != null || comics == null || comics!.isEmpty)
|
||||
SizedBox(
|
||||
height: _kComicHeight,
|
||||
child: Column(
|
||||
@@ -178,7 +213,13 @@ class _SliverSearchResultState extends State<_SliverSearchResult>
|
||||
children: [
|
||||
const Icon(Icons.error_outline),
|
||||
const SizedBox(width: 8),
|
||||
Text("No search results found".tl),
|
||||
Expanded(
|
||||
child: Text(
|
||||
error ?? "No search results found".tl,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
|
@@ -32,7 +32,7 @@ class _CategoriesPageState extends State<CategoriesPage> {
|
||||
.toList();
|
||||
categories =
|
||||
categories.where((element) => allCategories.contains(element)).toList();
|
||||
if (!categories.isEqualsTo(this.categories)) {
|
||||
if (!categories.isEqualTo(this.categories)) {
|
||||
setState(() {
|
||||
this.categories = categories;
|
||||
});
|
||||
|
@@ -49,19 +49,19 @@ class ComicPage extends StatefulWidget {
|
||||
|
||||
class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
||||
with _ComicPageActions {
|
||||
@override
|
||||
History? history;
|
||||
|
||||
bool showAppbarTitle = false;
|
||||
|
||||
var scrollController = ScrollController();
|
||||
|
||||
bool isDownloaded = false;
|
||||
|
||||
void updateHistory() async {
|
||||
var newHistory = await HistoryManager()
|
||||
.find(widget.id, ComicType(widget.sourceKey.hashCode));
|
||||
if (newHistory?.ep != history?.ep || newHistory?.page != history?.page) {
|
||||
history = newHistory;
|
||||
update();
|
||||
}
|
||||
@override
|
||||
void onReadEnd() {
|
||||
// The history is passed by reference, so it will be updated automatically.
|
||||
update();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -77,14 +77,12 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
||||
@override
|
||||
void initState() {
|
||||
scrollController.addListener(onScroll);
|
||||
HistoryManager().addListener(updateHistory);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
scrollController.removeListener(onScroll);
|
||||
HistoryManager().removeListener(updateHistory);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -206,62 +204,64 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
||||
|
||||
yield const SliverPadding(padding: EdgeInsets.only(top: 8));
|
||||
|
||||
yield Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(width: 16),
|
||||
Hero(
|
||||
tag: "cover${comic.id}${comic.sourceKey}",
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: context.colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: context.colorScheme.outlineVariant,
|
||||
blurRadius: 1,
|
||||
offset: const Offset(0, 1),
|
||||
yield SliverLazyToBoxAdapter(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(width: 16),
|
||||
Hero(
|
||||
tag: "cover${comic.id}${comic.sourceKey}",
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: context.colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: context.colorScheme.outlineVariant,
|
||||
blurRadius: 1,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
height: 144,
|
||||
width: 144 * 0.72,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: AnimatedImage(
|
||||
image: CachedImageProvider(
|
||||
widget.cover ?? comic.cover,
|
||||
sourceKey: comic.sourceKey,
|
||||
cid: comic.id,
|
||||
),
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SelectableText(comic.title, style: ts.s18),
|
||||
if (comic.subTitle != null)
|
||||
SelectableText(comic.subTitle!, style: ts.s14)
|
||||
.paddingVertical(4),
|
||||
Text(
|
||||
(ComicSource.find(comic.sourceKey)?.name) ?? '',
|
||||
style: ts.s12,
|
||||
),
|
||||
],
|
||||
),
|
||||
height: 144,
|
||||
width: 144 * 0.72,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: AnimatedImage(
|
||||
image: CachedImageProvider(
|
||||
widget.cover ?? comic.cover,
|
||||
sourceKey: comic.sourceKey,
|
||||
cid: comic.id,
|
||||
),
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SelectableText(comic.title, style: ts.s18),
|
||||
if (comic.subTitle != null)
|
||||
SelectableText(comic.subTitle!, style: ts.s14)
|
||||
.paddingVertical(4),
|
||||
Text(
|
||||
(ComicSource.find(comic.sourceKey)?.name) ?? '',
|
||||
style: ts.s12,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
).toSliver();
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildActions() {
|
||||
bool isMobile = context.width < changePoint;
|
||||
bool hasHistory = history != null && (history!.ep > 1 || history!.page > 1);
|
||||
return SliverToBoxAdapter(
|
||||
return SliverLazyToBoxAdapter(
|
||||
child: Column(
|
||||
children: [
|
||||
ListView(
|
||||
@@ -354,7 +354,7 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
||||
if (comic.description == null || comic.description!.trim().isEmpty) {
|
||||
return const SliverPadding(padding: EdgeInsets.zero);
|
||||
}
|
||||
return SliverToBoxAdapter(
|
||||
return SliverLazyToBoxAdapter(
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
@@ -482,7 +482,7 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
||||
bool enableTranslation =
|
||||
App.locale.languageCode == 'zh' && comicSource.enableTagsTranslate;
|
||||
|
||||
return SliverToBoxAdapter(
|
||||
return SliverLazyToBoxAdapter(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -550,7 +550,7 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
||||
if (comic.chapters == null) {
|
||||
return const SliverPadding(padding: EdgeInsets.zero);
|
||||
}
|
||||
return const _ComicChapters();
|
||||
return _ComicChapters(history);
|
||||
}
|
||||
|
||||
Widget buildThumbnails() {
|
||||
@@ -592,7 +592,7 @@ abstract mixin class _ComicPageActions {
|
||||
|
||||
ComicSource get comicSource => ComicSource.find(comic.sourceKey)!;
|
||||
|
||||
History? history;
|
||||
History? get history;
|
||||
|
||||
bool isLiking = false;
|
||||
|
||||
@@ -612,8 +612,10 @@ abstract mixin class _ComicPageActions {
|
||||
update();
|
||||
}
|
||||
|
||||
/// whether the comic is added to local favorite
|
||||
bool isAddToLocalFav = false;
|
||||
|
||||
/// whether the comic is favorite on the server
|
||||
bool isFavorite = false;
|
||||
|
||||
FavoriteItem _toFavoriteItem() {
|
||||
@@ -684,11 +686,13 @@ abstract mixin class _ComicPageActions {
|
||||
chapters: comic.chapters,
|
||||
initialChapter: ep,
|
||||
initialPage: page,
|
||||
history: History.fromModel(model: comic, ep: 0, page: 0),
|
||||
history: history ?? History.fromModel(model: comic, ep: 0, page: 0),
|
||||
author: comic.findAuthor() ?? '',
|
||||
tags: comic.plainTags,
|
||||
),
|
||||
);
|
||||
).then((_) {
|
||||
onReadEnd();
|
||||
});
|
||||
}
|
||||
|
||||
void continueRead() {
|
||||
@@ -697,6 +701,8 @@ abstract mixin class _ComicPageActions {
|
||||
read(ep, page);
|
||||
}
|
||||
|
||||
void onReadEnd();
|
||||
|
||||
void download() async {
|
||||
if (LocalManager().isDownloading(comic.id, comic.comicType)) {
|
||||
App.rootContext.showMessage(message: "The comic is downloading".tl);
|
||||
@@ -1079,7 +1085,9 @@ class _ActionButton extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _ComicChapters extends StatefulWidget {
|
||||
const _ComicChapters();
|
||||
const _ComicChapters(this.history);
|
||||
|
||||
final History? history;
|
||||
|
||||
@override
|
||||
State<_ComicChapters> createState() => _ComicChaptersState();
|
||||
@@ -1092,104 +1100,133 @@ class _ComicChaptersState extends State<_ComicChapters> {
|
||||
|
||||
bool showAll = false;
|
||||
|
||||
late History? history;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
history = widget.history;
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
state = context.findAncestorStateOfType<_ComicPageState>()!;
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant _ComicChapters oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
setState(() {
|
||||
history = widget.history;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final eps = state.comic.chapters!;
|
||||
|
||||
int length = eps.length;
|
||||
return SliverLayoutBuilder(
|
||||
builder: (context, constrains) {
|
||||
int length = eps.length;
|
||||
bool canShowAll = showAll;
|
||||
if (!showAll) {
|
||||
var width = constrains.crossAxisExtent - 16;
|
||||
var crossItems = width ~/ 200;
|
||||
if (width % 200 != 0) {
|
||||
crossItems += 1;
|
||||
}
|
||||
length = math.min(length, crossItems * 8);
|
||||
if (length == eps.length) {
|
||||
canShowAll = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!showAll) {
|
||||
length = math.min(length, 20);
|
||||
}
|
||||
|
||||
return SliverMainAxisGroup(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: ListTile(
|
||||
title: Text("Chapters".tl),
|
||||
trailing: Tooltip(
|
||||
message: "Order".tl,
|
||||
child: IconButton(
|
||||
icon: Icon(reverse
|
||||
? Icons.vertical_align_top
|
||||
: Icons.vertical_align_bottom_outlined),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
reverse = !reverse;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverGrid(
|
||||
delegate:
|
||||
SliverChildBuilderDelegate(childCount: length, (context, i) {
|
||||
if (reverse) {
|
||||
i = eps.length - i - 1;
|
||||
}
|
||||
var key = eps.keys.elementAt(i);
|
||||
var value = eps[key]!;
|
||||
bool visited =
|
||||
(state.history?.readEpisode ?? const {}).contains(i + 1);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8, 4, 8, 4),
|
||||
child: Material(
|
||||
color: context.colorScheme.surfaceContainer,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
child: InkWell(
|
||||
onTap: () => state.read(i + 1),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
child: Center(
|
||||
child: Text(
|
||||
value,
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: visited ? context.colorScheme.outline : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
return SliverMainAxisGroup(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: ListTile(
|
||||
title: Text("Chapters".tl),
|
||||
trailing: Tooltip(
|
||||
message: "Order".tl,
|
||||
child: IconButton(
|
||||
icon: Icon(reverse
|
||||
? Icons.vertical_align_top
|
||||
: Icons.vertical_align_bottom_outlined),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
reverse = !reverse;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
gridDelegate: const SliverGridDelegateWithFixedHeight(
|
||||
maxCrossAxisExtent: 200, itemHeight: 48),
|
||||
).sliverPadding(const EdgeInsets.symmetric(horizontal: 8)),
|
||||
if (eps.length > 20 && !showAll)
|
||||
SliverToBoxAdapter(
|
||||
child: Align(
|
||||
alignment: Alignment.center,
|
||||
child: FilledButton.tonal(
|
||||
style: ButtonStyle(
|
||||
shape: WidgetStateProperty.all(const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)))),
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
showAll = true;
|
||||
});
|
||||
},
|
||||
child: Text("${"Show all".tl} (${eps.length})"),
|
||||
).paddingTop(12),
|
||||
),
|
||||
),
|
||||
const SliverToBoxAdapter(
|
||||
child: Divider(),
|
||||
),
|
||||
],
|
||||
SliverGrid(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
childCount: length,
|
||||
(context, i) {
|
||||
if (reverse) {
|
||||
i = eps.length - i - 1;
|
||||
}
|
||||
var key = eps.keys.elementAt(i);
|
||||
var value = eps[key]!;
|
||||
bool visited = (history?.readEpisode ?? {}).contains(i + 1);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(6, 4, 6, 4),
|
||||
child: Material(
|
||||
color: context.colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: InkWell(
|
||||
onTap: () => state.read(i + 1),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Center(
|
||||
child: Text(
|
||||
value,
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: visited
|
||||
? context.colorScheme.outline
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
gridDelegate: const SliverGridDelegateWithFixedHeight(
|
||||
maxCrossAxisExtent: 200,
|
||||
itemHeight: 48,
|
||||
),
|
||||
).sliverPadding(const EdgeInsets.symmetric(horizontal: 8)),
|
||||
if (eps.length > 20 && !canShowAll)
|
||||
SliverToBoxAdapter(
|
||||
child: Align(
|
||||
alignment: Alignment.center,
|
||||
child: TextButton.icon(
|
||||
icon: const Icon(Icons.arrow_drop_down),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
showAll = true;
|
||||
});
|
||||
},
|
||||
label: Text("${"Show all".tl} (${eps.length})"),
|
||||
).paddingTop(12),
|
||||
),
|
||||
),
|
||||
const SliverToBoxAdapter(
|
||||
child: Divider(),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1388,42 +1425,67 @@ class _FavoritePanel extends StatefulWidget {
|
||||
State<_FavoritePanel> createState() => _FavoritePanelState();
|
||||
}
|
||||
|
||||
class _FavoritePanelState extends State<_FavoritePanel> {
|
||||
class _FavoritePanelState extends State<_FavoritePanel>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late ComicSource comicSource;
|
||||
|
||||
late TabController tabController;
|
||||
|
||||
late bool hasNetwork;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
comicSource = widget.type.comicSource!;
|
||||
localFolders = LocalFavoritesManager().folderNames;
|
||||
added = LocalFavoritesManager().find(widget.cid, widget.type);
|
||||
hasNetwork = comicSource.favoriteData != null && comicSource.isLogged;
|
||||
var initIndex = 0;
|
||||
if (appdata.implicitData['favoritePanelIndex'] is int) {
|
||||
initIndex = appdata.implicitData['favoritePanelIndex'];
|
||||
}
|
||||
initIndex = initIndex.clamp(0, hasNetwork ? 1 : 0);
|
||||
tabController = TabController(
|
||||
initialIndex: initIndex,
|
||||
length: hasNetwork ? 2 : 1,
|
||||
vsync: this,
|
||||
);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
var currentIndex = tabController.index;
|
||||
appdata.implicitData['favoritePanelIndex'] = currentIndex;
|
||||
appdata.writeImplicitData();
|
||||
tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var hasNetwork = comicSource.favoriteData != null && comicSource.isLogged;
|
||||
return Scaffold(
|
||||
appBar: Appbar(
|
||||
title: Text("Favorite".tl),
|
||||
),
|
||||
body: DefaultTabController(
|
||||
length: hasNetwork ? 2 : 1,
|
||||
child: Column(
|
||||
children: [
|
||||
TabBar(tabs: [
|
||||
body: Column(
|
||||
children: [
|
||||
TabBar(
|
||||
controller: tabController,
|
||||
tabs: [
|
||||
Tab(text: "Local".tl),
|
||||
if (hasNetwork) Tab(text: "Network".tl),
|
||||
]),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
children: [
|
||||
buildLocal(),
|
||||
if (hasNetwork) buildNetwork(),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: tabController,
|
||||
children: [
|
||||
buildLocal(),
|
||||
if (hasNetwork) buildNetwork(),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1645,6 +1707,42 @@ class _NetworkFavoritesState extends State<_NetworkFavorites> {
|
||||
}
|
||||
|
||||
Widget buildMultiFolder() {
|
||||
if (widget.isFavorite == true &&
|
||||
widget.comicSource.favoriteData!.singleFolderForSingleComic) {
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Text("Added to favorites".tl),
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Button.filled(
|
||||
isLoading: isLoading,
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
isLoading = true;
|
||||
});
|
||||
|
||||
var res = await widget.comicSource.favoriteData!
|
||||
.addOrDelFavorite!(widget.cid, '', false, null);
|
||||
if (res.success) {
|
||||
widget.onFavorite(false);
|
||||
context.pop();
|
||||
App.rootContext.showMessage(message: "Removed".tl);
|
||||
} else {
|
||||
setState(() {
|
||||
isLoading = false;
|
||||
});
|
||||
context.showMessage(message: res.errorMessage!);
|
||||
}
|
||||
},
|
||||
child: Text("Remove".tl),
|
||||
).paddingVertical(8),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
if (isLoadingFolders) {
|
||||
loadFolders();
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
@@ -1850,7 +1948,7 @@ class _CommentsPartState extends State<_CommentsPart> {
|
||||
Widget build(BuildContext context) {
|
||||
return MultiSliver(
|
||||
children: [
|
||||
SliverToBoxAdapter(
|
||||
SliverLazyToBoxAdapter(
|
||||
child: ListTile(
|
||||
title: Text("Comments".tl),
|
||||
trailing: Row(
|
||||
|
@@ -1011,7 +1011,7 @@ class _LoginPageState extends State<_LoginPage> {
|
||||
if (widget.config.loginWebsite != null)
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
if (App.isWindows || App.isLinux) {
|
||||
if (App.isLinux) {
|
||||
loginWithWebview2();
|
||||
} else {
|
||||
loginWithWebview();
|
||||
@@ -1127,7 +1127,7 @@ class _LoginPageState extends State<_LoginPage> {
|
||||
}
|
||||
}
|
||||
|
||||
// for windows and linux
|
||||
// for linux
|
||||
void loginWithWebview2() async {
|
||||
if (!await DesktopWebview.isAvailable()) {
|
||||
context.showMessage(message: "Webview is not available".tl);
|
||||
|
@@ -42,7 +42,7 @@ class _CommentsPageState extends State<CommentsPage> {
|
||||
_error = res.errorMessage;
|
||||
_loading = false;
|
||||
});
|
||||
} else {
|
||||
} else if (mounted) {
|
||||
setState(() {
|
||||
_comments = res.data;
|
||||
_loading = false;
|
||||
|
@@ -37,7 +37,7 @@ class _ExplorePageState extends State<ExplorePage>
|
||||
.expand((e) => e.map((e) => e.title))
|
||||
.toList();
|
||||
explorePages = explorePages.where((e) => all.contains(e)).toList();
|
||||
if (!pages.isEqualsTo(explorePages)) {
|
||||
if (!pages.isEqualTo(explorePages)) {
|
||||
setState(() {
|
||||
pages = explorePages;
|
||||
controller = TabController(
|
||||
|
@@ -16,6 +16,7 @@ import 'package:venera/foundation/res.dart';
|
||||
import 'package:venera/network/download.dart';
|
||||
import 'package:venera/pages/comic_page.dart';
|
||||
import 'package:venera/pages/reader/reader.dart';
|
||||
import 'package:venera/pages/settings/settings_page.dart';
|
||||
import 'package:venera/utils/io.dart';
|
||||
import 'package:venera/utils/translations.dart';
|
||||
|
||||
|
@@ -476,55 +476,47 @@ class _CreateFolderDialogState extends State<_CreateFolderDialog> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SimpleDialog(
|
||||
title: Text("Create a folder".tl),
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: "name".tl,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 200,
|
||||
height: 10,
|
||||
),
|
||||
if (loading)
|
||||
Center(
|
||||
child: const CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
).fixWidth(24).fixHeight(24),
|
||||
)
|
||||
else
|
||||
SizedBox(
|
||||
height: 35,
|
||||
child: Center(
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
loading = true;
|
||||
});
|
||||
widget.data.addFolder!(controller.text).then((b) {
|
||||
if (b.error) {
|
||||
context.showMessage(message: b.errorMessage!);
|
||||
setState(() {
|
||||
loading = false;
|
||||
});
|
||||
} else {
|
||||
context.pop();
|
||||
context.showMessage(message: "Created successfully".tl);
|
||||
widget.updateState();
|
||||
}
|
||||
});
|
||||
},
|
||||
child: Text("Submit".tl),
|
||||
return ContentDialog(
|
||||
title: "Create a folder".tl,
|
||||
content: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 0),
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: "name".tl,
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
Button.filled(
|
||||
isLoading: loading,
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
loading = true;
|
||||
});
|
||||
widget.data.addFolder!(controller.text).then((b) {
|
||||
if (b.error) {
|
||||
context.showMessage(message: b.errorMessage!);
|
||||
setState(() {
|
||||
loading = false;
|
||||
});
|
||||
} else {
|
||||
context.pop();
|
||||
context.showMessage(message: "Created successfully".tl);
|
||||
widget.updateState();
|
||||
}
|
||||
});
|
||||
},
|
||||
child: Text("Submit".tl),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@@ -20,22 +20,35 @@ class _LeftBarState extends State<_LeftBar> implements FolderList {
|
||||
|
||||
var networkFolders = <String>[];
|
||||
|
||||
void findNetworkFolders() {
|
||||
networkFolders.clear();
|
||||
var all = ComicSource.all()
|
||||
.where((e) => e.favoriteData != null)
|
||||
.map((e) => e.favoriteData!.key)
|
||||
.toList();
|
||||
var settings = appdata.settings['favorites'] as List;
|
||||
for (var p in settings) {
|
||||
if (all.contains(p) && !networkFolders.contains(p)) {
|
||||
networkFolders.add(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
favPage = widget.favPage ??
|
||||
context.findAncestorStateOfType<_FavoritesPageState>()!;
|
||||
favPage.folderList = this;
|
||||
folders = LocalFavoritesManager().folderNames;
|
||||
networkFolders = ComicSource.all()
|
||||
.where((e) => e.favoriteData != null && e.isLogged)
|
||||
.map((e) => e.favoriteData!.key)
|
||||
.toList();
|
||||
findNetworkFolders();
|
||||
appdata.settings.addListener(updateFolders);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
appdata.settings.removeListener(updateFolders);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -102,7 +115,8 @@ class _LeftBarState extends State<_LeftBar> implements FolderList {
|
||||
onClick: () {
|
||||
newFolder().then((value) {
|
||||
setState(() {
|
||||
folders = LocalFavoritesManager().folderNames;
|
||||
folders =
|
||||
LocalFavoritesManager().folderNames;
|
||||
});
|
||||
});
|
||||
},
|
||||
@@ -113,7 +127,8 @@ class _LeftBarState extends State<_LeftBar> implements FolderList {
|
||||
onClick: () {
|
||||
sortFolders().then((value) {
|
||||
setState(() {
|
||||
folders = LocalFavoritesManager().folderNames;
|
||||
folders =
|
||||
LocalFavoritesManager().folderNames;
|
||||
});
|
||||
});
|
||||
},
|
||||
@@ -143,15 +158,24 @@ class _LeftBarState extends State<_LeftBar> implements FolderList {
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(width: 16),
|
||||
Icon(
|
||||
Icons.cloud,
|
||||
color: context.colorScheme.secondary,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text("Network".tl),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.settings),
|
||||
onPressed: () {
|
||||
showPopUpWidget(
|
||||
App.rootContext,
|
||||
setFavoritesPagesWidget(),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
).paddingHorizontal(16),
|
||||
);
|
||||
}
|
||||
index--;
|
||||
@@ -241,10 +265,7 @@ class _LeftBarState extends State<_LeftBar> implements FolderList {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
folders = LocalFavoritesManager().folderNames;
|
||||
networkFolders = ComicSource.all()
|
||||
.where((e) => e.favoriteData != null)
|
||||
.map((e) => e.favoriteData!.key)
|
||||
.toList();
|
||||
findNetworkFolders();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -12,6 +12,7 @@ import 'package:venera/utils/epub.dart';
|
||||
import 'package:venera/utils/io.dart';
|
||||
import 'package:venera/utils/pdf.dart';
|
||||
import 'package:venera/utils/translations.dart';
|
||||
import 'package:zip_flutter/zip_flutter.dart';
|
||||
|
||||
class LocalComicsPage extends StatefulWidget {
|
||||
const LocalComicsPage({super.key});
|
||||
@@ -147,13 +148,13 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
|
||||
text: "View Detail".tl,
|
||||
onClick: () {
|
||||
context.to(() => ComicPage(
|
||||
id: selectedComics.keys.first.id,
|
||||
sourceKey: selectedComics.keys.first.sourceKey,
|
||||
));
|
||||
id: selectedComics.keys.first.id,
|
||||
sourceKey: selectedComics.keys.first.sourceKey,
|
||||
));
|
||||
},
|
||||
),
|
||||
if (selectedComics.length == 1)
|
||||
...exportActions(selectedComics.keys.first),
|
||||
if (selectedComics.isNotEmpty)
|
||||
...exportActions(selectedComics.keys.toList()),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -322,7 +323,7 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
|
||||
});
|
||||
},
|
||||
),
|
||||
...exportActions(c as LocalComic),
|
||||
...exportActions([c as LocalComic]),
|
||||
];
|
||||
},
|
||||
),
|
||||
@@ -390,79 +391,102 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
|
||||
return isDeleted;
|
||||
}
|
||||
|
||||
List<MenuEntry> exportActions(LocalComic c) {
|
||||
List<MenuEntry> exportActions(List<LocalComic> comics) {
|
||||
return [
|
||||
MenuEntry(
|
||||
icon: Icons.outbox_outlined,
|
||||
text: "Export as cbz".tl,
|
||||
onClick: () async {
|
||||
var controller = showLoadingDialog(
|
||||
context,
|
||||
allowCancel: false,
|
||||
);
|
||||
try {
|
||||
var file = await CBZ.export(c);
|
||||
await saveFile(filename: file.name, file: file);
|
||||
await file.delete();
|
||||
} catch (e, s) {
|
||||
context.showMessage(message: e.toString());
|
||||
Log.error("CBZ Export", e, s);
|
||||
}
|
||||
controller.close();
|
||||
}),
|
||||
icon: Icons.outbox_outlined,
|
||||
text: "Export as cbz".tl,
|
||||
onClick: () {
|
||||
exportComics(comics, CBZ.export, ".cbz");
|
||||
},
|
||||
),
|
||||
MenuEntry(
|
||||
icon: Icons.picture_as_pdf_outlined,
|
||||
text: "Export as pdf".tl,
|
||||
onClick: () async {
|
||||
var cache = FilePath.join(App.cachePath, 'temp.pdf');
|
||||
var controller = showLoadingDialog(
|
||||
context,
|
||||
allowCancel: false,
|
||||
);
|
||||
try {
|
||||
await createPdfFromComicIsolate(
|
||||
comic: c,
|
||||
savePath: cache,
|
||||
);
|
||||
await saveFile(
|
||||
file: File(cache),
|
||||
filename: "${c.title}.pdf",
|
||||
);
|
||||
} catch (e, s) {
|
||||
Log.error("PDF Export", e, s);
|
||||
context.showMessage(message: e.toString());
|
||||
} finally {
|
||||
controller.close();
|
||||
File(cache).deleteIgnoreError();
|
||||
}
|
||||
exportComics(comics, createPdfFromComicIsolate, ".pdf");
|
||||
},
|
||||
),
|
||||
MenuEntry(
|
||||
icon: Icons.import_contacts_outlined,
|
||||
text: "Export as epub".tl,
|
||||
onClick: () async {
|
||||
var controller = showLoadingDialog(
|
||||
context,
|
||||
allowCancel: false,
|
||||
);
|
||||
File? file;
|
||||
try {
|
||||
file = await createEpubWithLocalComic(
|
||||
c,
|
||||
);
|
||||
await saveFile(
|
||||
file: file,
|
||||
filename: "${c.title}.epub",
|
||||
);
|
||||
} catch (e, s) {
|
||||
Log.error("EPUB Export", e, s);
|
||||
context.showMessage(message: e.toString());
|
||||
} finally {
|
||||
controller.close();
|
||||
file?.deleteIgnoreError();
|
||||
}
|
||||
exportComics(comics, createEpubWithLocalComic, ".epub");
|
||||
},
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
/// Export given comics to a file
|
||||
void exportComics(
|
||||
List<LocalComic> comics, ExportComicFunc export, String ext) async {
|
||||
var current = 0;
|
||||
var cacheDir = FilePath.join(App.cachePath, 'comics_export');
|
||||
var outFile = FilePath.join(App.cachePath, 'comics_export.zip');
|
||||
bool canceled = false;
|
||||
if (Directory(cacheDir).existsSync()) {
|
||||
Directory(cacheDir).deleteSync(recursive: true);
|
||||
}
|
||||
Directory(cacheDir).createSync();
|
||||
var loadingController = showLoadingDialog(
|
||||
context,
|
||||
allowCancel: true,
|
||||
message: "${"Exporting".tl} $current/${comics.length}",
|
||||
withProgress: comics.length > 1,
|
||||
onCancel: () {
|
||||
canceled = true;
|
||||
},
|
||||
);
|
||||
try {
|
||||
var fileName = "";
|
||||
// For each comic, export it to a file
|
||||
for (var comic in comics) {
|
||||
fileName = FilePath.join(cacheDir, sanitizeFileName(comic.title) + ext);
|
||||
await export(comic, fileName);
|
||||
current++;
|
||||
if (comics.length > 1) {
|
||||
loadingController
|
||||
.setMessage("${"Exporting".tl} $current/${comics.length}");
|
||||
loadingController.setProgress(current / comics.length);
|
||||
}
|
||||
if (canceled) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// For single comic, just save the file
|
||||
if (comics.length == 1) {
|
||||
await saveFile(
|
||||
file: File(fileName),
|
||||
filename: File(fileName).name,
|
||||
);
|
||||
Directory(cacheDir).deleteSync(recursive: true);
|
||||
loadingController.close();
|
||||
return;
|
||||
}
|
||||
// For multiple comics, compress the folder
|
||||
loadingController.setProgress(null);
|
||||
loadingController.setMessage("Compressing".tl);
|
||||
await ZipFile.compressFolderAsync(cacheDir, outFile);
|
||||
if (canceled) {
|
||||
File(outFile).deleteIgnoreError();
|
||||
return;
|
||||
}
|
||||
} catch (e, s) {
|
||||
Log.error("Export Comics", e, s);
|
||||
context.showMessage(message: e.toString());
|
||||
loadingController.close();
|
||||
return;
|
||||
} finally {
|
||||
Directory(cacheDir).deleteIgnoreError(recursive: true);
|
||||
}
|
||||
await saveFile(
|
||||
file: File(outFile),
|
||||
filename: "comics_export.zip",
|
||||
);
|
||||
loadingController.close();
|
||||
File(outFile).deleteIgnoreError();
|
||||
}
|
||||
}
|
||||
|
||||
typedef ExportComicFunc = Future<File> Function(
|
||||
LocalComic comic, String outFilePath);
|
||||
|
@@ -24,6 +24,8 @@ class _ReaderGestureDetectorState extends State<_ReaderGestureDetector> {
|
||||
|
||||
int fingers = 0;
|
||||
|
||||
late _ReaderState reader;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_tapGestureRecognizer = TapGestureRecognizer()
|
||||
@@ -33,6 +35,7 @@ class _ReaderGestureDetectorState extends State<_ReaderGestureDetector> {
|
||||
};
|
||||
super.initState();
|
||||
context.readerScaffold._gestureDetectorState = this;
|
||||
reader = context.reader;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -166,7 +169,9 @@ class _ReaderGestureDetectorState extends State<_ReaderGestureDetector> {
|
||||
}
|
||||
|
||||
void onTap(Offset location) {
|
||||
if (context.readerScaffold.isOpen) {
|
||||
if (reader._imageViewController!.handleOnTap(location)) {
|
||||
return;
|
||||
} else if (context.readerScaffold.isOpen) {
|
||||
context.readerScaffold.openOrClose();
|
||||
} else {
|
||||
if (appdata.settings['enableTapToTurnPages']) {
|
||||
@@ -186,31 +191,37 @@ class _ReaderGestureDetectorState extends State<_ReaderGestureDetector> {
|
||||
isBottom = true;
|
||||
}
|
||||
bool isCenter = false;
|
||||
var prev = context.reader.toPrevPage;
|
||||
var next = context.reader.toNextPage;
|
||||
if (appdata.settings['reverseTapToTurnPages']) {
|
||||
prev = context.reader.toNextPage;
|
||||
next = context.reader.toPrevPage;
|
||||
}
|
||||
switch (context.reader.mode) {
|
||||
case ReaderMode.galleryLeftToRight:
|
||||
case ReaderMode.continuousLeftToRight:
|
||||
if (isLeft) {
|
||||
context.reader.toPrevPage();
|
||||
prev();
|
||||
} else if (isRight) {
|
||||
context.reader.toNextPage();
|
||||
next();
|
||||
} else {
|
||||
isCenter = true;
|
||||
}
|
||||
case ReaderMode.galleryRightToLeft:
|
||||
case ReaderMode.continuousRightToLeft:
|
||||
if (isLeft) {
|
||||
context.reader.toNextPage();
|
||||
next();
|
||||
} else if (isRight) {
|
||||
context.reader.toPrevPage();
|
||||
prev();
|
||||
} else {
|
||||
isCenter = true;
|
||||
}
|
||||
case ReaderMode.galleryTopToBottom:
|
||||
case ReaderMode.continuousTopToBottom:
|
||||
if (isTop) {
|
||||
context.reader.toPrevPage();
|
||||
prev();
|
||||
} else if (isBottom) {
|
||||
context.reader.toNextPage();
|
||||
next();
|
||||
} else {
|
||||
isCenter = true;
|
||||
}
|
||||
|
@@ -335,6 +335,11 @@ class _GalleryModeState extends State<_GalleryMode>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool handleOnTap(Offset location) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const Set<PointerDeviceKind> _kTouchLikeDeviceTypes = <PointerDeviceKind>{
|
||||
@@ -366,6 +371,18 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
||||
var fingers = 0;
|
||||
bool disableScroll = false;
|
||||
|
||||
/// Whether the user was scrolling the page.
|
||||
/// The gesture detector has a delay to detect tap event.
|
||||
/// To handle the tap event, we need to know if the user was scrolling before the delay.
|
||||
bool delayedIsScrolling = false;
|
||||
|
||||
void delayedSetIsScrolling(bool value) {
|
||||
Future.delayed(
|
||||
const Duration(milliseconds: 300),
|
||||
() => delayedIsScrolling = value,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
reader = context.reader;
|
||||
@@ -374,6 +391,12 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
itemPositionsListener.itemPositions.removeListener(onPositionChanged);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void onPositionChanged() {
|
||||
var page = itemPositionsListener.itemPositions.value.first.index;
|
||||
page = page.clamp(1, reader.maxPage);
|
||||
@@ -489,6 +512,14 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
||||
});
|
||||
}
|
||||
},
|
||||
onPointerCancel: (event) {
|
||||
fingers--;
|
||||
if (fingers <= 1 && disableScroll) {
|
||||
setState(() {
|
||||
disableScroll = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
onPointerPanZoomUpdate: (event) {
|
||||
if (event.scale == 1.0) {
|
||||
smoothTo(0 - event.panDelta.dy);
|
||||
@@ -516,8 +547,14 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
||||
child: widget,
|
||||
);
|
||||
|
||||
widget = NotificationListener<ScrollUpdateNotification>(
|
||||
widget = NotificationListener<ScrollNotification>(
|
||||
onNotification: (notification) {
|
||||
if (notification is ScrollStartNotification) {
|
||||
delayedSetIsScrolling(true);
|
||||
} else if (notification is ScrollEndNotification) {
|
||||
delayedSetIsScrolling(false);
|
||||
}
|
||||
|
||||
var length = reader.maxChapter;
|
||||
if (!scrollController.hasClients) return false;
|
||||
if (scrollController.position.pixels <=
|
||||
@@ -592,7 +629,7 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
||||
|
||||
@override
|
||||
void handleLongPressDown(Offset location) {
|
||||
if (!appdata.settings['enableLongPressToZoom']) {
|
||||
if (!appdata.settings['enableLongPressToZoom'] || delayedIsScrolling) {
|
||||
return;
|
||||
}
|
||||
double target = photoViewController.getInitialScale!.call()! * 1.75;
|
||||
@@ -667,6 +704,14 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool handleOnTap(Offset location) {
|
||||
if (delayedIsScrolling) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
ImageProvider _createImageProviderFromKey(
|
||||
|
@@ -237,6 +237,7 @@ class _ReaderState extends State<Reader> with _ReaderLocation, _ReaderWindow {
|
||||
history!.maxPage = maxPage;
|
||||
}
|
||||
history!.readEpisode.add(chapter);
|
||||
print(history!.readEpisode);
|
||||
history!.time = DateTime.now();
|
||||
HistoryManager().addHistory(history!);
|
||||
}
|
||||
@@ -430,4 +431,7 @@ abstract interface class _ImageViewController {
|
||||
void handleLongPressUp(Offset location);
|
||||
|
||||
void handleKeyEvent(KeyEvent event);
|
||||
|
||||
/// Returns true if the event is handled.
|
||||
bool handleOnTap(Offset location);
|
||||
}
|
||||
|
@@ -660,12 +660,16 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
||||
App.rootContext.pop();
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||
foregroundDecoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
child: Image(
|
||||
|
@@ -10,12 +10,14 @@ import 'package:venera/foundation/comic_source/comic_source.dart';
|
||||
import 'package:venera/foundation/state_controller.dart';
|
||||
import 'package:venera/pages/aggregated_search_page.dart';
|
||||
import 'package:venera/pages/search_result_page.dart';
|
||||
import 'package:venera/pages/settings/settings_page.dart';
|
||||
import 'package:venera/utils/app_links.dart';
|
||||
import 'package:venera/utils/ext.dart';
|
||||
import 'package:venera/utils/tags_translation.dart';
|
||||
import 'package:venera/utils/translations.dart';
|
||||
|
||||
import 'comic_page.dart';
|
||||
import 'comic_source_page.dart';
|
||||
|
||||
class SearchPage extends StatefulWidget {
|
||||
const SearchPage({super.key});
|
||||
@@ -27,8 +29,13 @@ class SearchPage extends StatefulWidget {
|
||||
class _SearchPageState extends State<SearchPage> {
|
||||
late final SearchBarController controller;
|
||||
|
||||
late List<String> searchSources;
|
||||
|
||||
String searchTarget = "";
|
||||
|
||||
SearchPageData get currentSearchPageData =>
|
||||
ComicSource.find(searchTarget)!.searchPageData!;
|
||||
|
||||
bool aggregatedSearch = false;
|
||||
|
||||
var focusNode = FocusNode();
|
||||
@@ -139,29 +146,85 @@ class _SearchPageState extends State<SearchPage> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
findSearchSources();
|
||||
var defaultSearchTarget = appdata.settings['defaultSearchTarget'];
|
||||
if (defaultSearchTarget == "_aggregated_") {
|
||||
aggregatedSearch = true;
|
||||
} else if (defaultSearchTarget != null &&
|
||||
ComicSource.find(defaultSearchTarget) != null) {
|
||||
searchSources.contains(defaultSearchTarget)) {
|
||||
searchTarget = defaultSearchTarget;
|
||||
} else {
|
||||
searchTarget = ComicSource.all().first.key;
|
||||
}
|
||||
controller = SearchBarController(
|
||||
onSearch: search,
|
||||
);
|
||||
appdata.settings.addListener(updateSearchSourcesIfNeeded);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
focusNode.dispose();
|
||||
appdata.settings.removeListener(updateSearchSourcesIfNeeded);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void findSearchSources() {
|
||||
var all = ComicSource.all()
|
||||
.where((e) => e.searchPageData != null)
|
||||
.map((e) => e.key)
|
||||
.toList();
|
||||
var settings = appdata.settings['searchSources'] as List;
|
||||
var sources = <String>[];
|
||||
for (var source in settings) {
|
||||
if (all.contains(source)) {
|
||||
sources.add(source);
|
||||
}
|
||||
}
|
||||
searchSources = sources;
|
||||
if (!searchSources.contains(searchTarget)) {
|
||||
searchTarget = searchSources.firstOrNull ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
void updateSearchSourcesIfNeeded() {
|
||||
var old = searchSources;
|
||||
findSearchSources();
|
||||
if (old.isEqualTo(searchSources)) {
|
||||
return;
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void manageSearchSources() {
|
||||
showPopUpWidget(App.rootContext, setSearchSourcesWidget());
|
||||
}
|
||||
|
||||
Widget buildEmpty() {
|
||||
var msg = "No Search Sources".tl;
|
||||
msg += '\n';
|
||||
VoidCallback onTap;
|
||||
if (ComicSource.isEmpty) {
|
||||
msg += "Please add some sources".tl;
|
||||
onTap = () {
|
||||
context.to(() => ComicSourcePage());
|
||||
};
|
||||
} else {
|
||||
msg += "Please check your settings".tl;
|
||||
onTap = manageSearchSources;
|
||||
}
|
||||
return NetworkError(
|
||||
message: msg,
|
||||
retry: onTap,
|
||||
withAppbar: true,
|
||||
buttonText: "Manage".tl,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (searchSources.isEmpty) {
|
||||
return buildEmpty();
|
||||
}
|
||||
return Scaffold(
|
||||
body: SmoothCustomScrollView(
|
||||
slivers: buildSlivers().toList(),
|
||||
@@ -190,8 +253,7 @@ class _SearchPageState extends State<SearchPage> {
|
||||
}
|
||||
|
||||
Widget buildSearchTarget() {
|
||||
var sources =
|
||||
ComicSource.all().where((e) => e.searchPageData != null).toList();
|
||||
var sources = searchSources.map((e) => ComicSource.find(e)!).toList();
|
||||
return SliverToBoxAdapter(
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
@@ -203,6 +265,10 @@ class _SearchPageState extends State<SearchPage> {
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const Icon(Icons.search),
|
||||
title: Text("Search in".tl),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.settings),
|
||||
onPressed: manageSearchSources,
|
||||
),
|
||||
),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
@@ -229,11 +295,6 @@ class _SearchPageState extends State<SearchPage> {
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
aggregatedSearch = value ?? false;
|
||||
if (!aggregatedSearch &&
|
||||
appdata.settings['defaultSearchTarget'] ==
|
||||
"_aggregated_") {
|
||||
searchTarget = sources.first.key;
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
@@ -245,9 +306,7 @@ class _SearchPageState extends State<SearchPage> {
|
||||
}
|
||||
|
||||
void useDefaultOptions() {
|
||||
final searchOptions =
|
||||
ComicSource.find(searchTarget)!.searchPageData!.searchOptions ??
|
||||
<SearchOptions>[];
|
||||
final searchOptions = currentSearchPageData.searchOptions ?? [];
|
||||
options = searchOptions.map((e) => e.defaultValue).toList();
|
||||
}
|
||||
|
||||
@@ -258,9 +317,7 @@ class _SearchPageState extends State<SearchPage> {
|
||||
|
||||
var children = <Widget>[];
|
||||
|
||||
final searchOptions =
|
||||
ComicSource.find(searchTarget)!.searchPageData!.searchOptions ??
|
||||
<SearchOptions>[];
|
||||
final searchOptions = currentSearchPageData.searchOptions ?? [];
|
||||
if (searchOptions.length != options.length) {
|
||||
useDefaultOptions();
|
||||
}
|
||||
@@ -394,7 +451,9 @@ class _SearchPageState extends State<SearchPage> {
|
||||
Text(
|
||||
subTitle,
|
||||
style: TextStyle(
|
||||
fontSize: 14, color: Theme.of(context).colorScheme.outline),
|
||||
fontSize: 14,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
|
@@ -116,13 +116,13 @@ class _SearchResultPageState extends State<SearchResultPage> {
|
||||
@override
|
||||
void initState() {
|
||||
sourceKey = widget.sourceKey;
|
||||
text = checkAutoLanguage(widget.text);
|
||||
controller = SearchBarController(
|
||||
currentText: checkAutoLanguage(widget.text),
|
||||
currentText: text,
|
||||
onSearch: search,
|
||||
);
|
||||
options = widget.options ?? const [];
|
||||
validateOptions();
|
||||
text = widget.text;
|
||||
appdata.addSearchHistory(text);
|
||||
suggestionsController = _SuggestionsController(controller);
|
||||
super.initState();
|
||||
@@ -187,7 +187,7 @@ class _SearchResultPageState extends State<SearchResultPage> {
|
||||
suggestionsController.remove();
|
||||
}
|
||||
|
||||
var previousOptions = options;
|
||||
var previousOptions = List<String>.from(options);
|
||||
var previousSourceKey = sourceKey;
|
||||
await showDialog(
|
||||
context: context,
|
||||
@@ -196,7 +196,8 @@ class _SearchResultPageState extends State<SearchResultPage> {
|
||||
return _SearchSettingsDialog(state: this);
|
||||
},
|
||||
);
|
||||
if (previousOptions != options || previousSourceKey != sourceKey) {
|
||||
if (!previousOptions.isEqualTo(options) ||
|
||||
previousSourceKey != sourceKey) {
|
||||
text = checkAutoLanguage(controller.text);
|
||||
controller.currentText = text;
|
||||
setState(() {});
|
||||
|
@@ -86,7 +86,7 @@ class _AboutSettingsState extends State<AboutSettings> {
|
||||
|
||||
Future<bool> checkUpdate() async {
|
||||
var res = await AppDio().get(
|
||||
"https://raw.githubusercontent.com/venera-app/venera/refs/heads/master/pubspec.yaml");
|
||||
"https://cdn.jsdelivr.net/gh/venera-app/venera@latest/pubspec.yaml");
|
||||
if (res.statusCode == 200) {
|
||||
var data = loadYaml(res.data);
|
||||
if (data["version"] != null) {
|
||||
|
@@ -38,19 +38,11 @@ class _ExploreSettingsState extends State<ExploreSettings> {
|
||||
).toSliver(),
|
||||
_PopupWindowSetting(
|
||||
title: "Network Favorite Pages".tl,
|
||||
builder: () {
|
||||
var pages = <String, String>{};
|
||||
for (var c in ComicSource.all()) {
|
||||
if (c.favoriteData != null) {
|
||||
pages[c.favoriteData!.key] = c.favoriteData!.title;
|
||||
}
|
||||
}
|
||||
return _MultiPagesFilter(
|
||||
title: "Network Favorite Pages".tl,
|
||||
settingsIndex: "favorites",
|
||||
pages: pages,
|
||||
);
|
||||
},
|
||||
builder: setFavoritesPagesWidget,
|
||||
).toSliver(),
|
||||
_PopupWindowSetting(
|
||||
title: "Search Sources".tl,
|
||||
builder: setSearchSourcesWidget,
|
||||
).toSliver(),
|
||||
_SwitchSetting(
|
||||
title: "Show favorite status on comic tile".tl,
|
||||
@@ -209,3 +201,31 @@ Widget setCategoryPagesWidget() {
|
||||
pages: pages,
|
||||
);
|
||||
}
|
||||
|
||||
Widget setFavoritesPagesWidget() {
|
||||
var pages = <String, String>{};
|
||||
for (var c in ComicSource.all()) {
|
||||
if (c.favoriteData != null) {
|
||||
pages[c.favoriteData!.key] = c.favoriteData!.title;
|
||||
}
|
||||
}
|
||||
return _MultiPagesFilter(
|
||||
title: "Network Favorite Pages".tl,
|
||||
settingsIndex: "favorites",
|
||||
pages: pages,
|
||||
);
|
||||
}
|
||||
|
||||
Widget setSearchSourcesWidget() {
|
||||
var pages = <String, String>{};
|
||||
for (var c in ComicSource.all()) {
|
||||
if (c.searchPageData != null) {
|
||||
pages[c.key] = c.name;
|
||||
}
|
||||
}
|
||||
return _MultiPagesFilter(
|
||||
title: "Search Sources".tl,
|
||||
settingsIndex: "searchSources",
|
||||
pages: pages,
|
||||
);
|
||||
}
|
@@ -22,6 +22,13 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
||||
widget.onChanged?.call("enableTapToTurnPages");
|
||||
},
|
||||
).toSliver(),
|
||||
_SwitchSetting(
|
||||
title: "Reverse tap to turn Pages".tl,
|
||||
settingKey: "reverseTapToTurnPages",
|
||||
onChanged: () {
|
||||
widget.onChanged?.call("reverseTapToTurnPages");
|
||||
},
|
||||
).toSliver(),
|
||||
_SwitchSetting(
|
||||
title: "Page animation".tl,
|
||||
settingKey: "enablePageAnimation",
|
||||
|
@@ -25,8 +25,13 @@ extension WebviewExtension on InAppWebViewController {
|
||||
if (url[url.length - 1] == '/') {
|
||||
url = url.substring(0, url.length - 1);
|
||||
}
|
||||
CookieManager cookieManager = CookieManager.instance();
|
||||
final cookies = await cookieManager.getCookies(url: WebUri(url));
|
||||
CookieManager cookieManager = CookieManager.instance(
|
||||
webViewEnvironment: AppWebview.webViewEnvironment,
|
||||
);
|
||||
final cookies = await cookieManager.getCookies(
|
||||
url: WebUri(url),
|
||||
webViewController: this,
|
||||
);
|
||||
var res = <io.Cookie>[];
|
||||
for (var cookie in cookies) {
|
||||
var c = io.Cookie(cookie.name, cookie.value);
|
||||
@@ -86,11 +91,12 @@ class _AppWebviewState extends State<AppWebview> {
|
||||
|
||||
late var future = _createWebviewEnvironment();
|
||||
|
||||
Future<WebViewEnvironment> _createWebviewEnvironment() async {
|
||||
Future<bool> _createWebviewEnvironment() async {
|
||||
var proxy = appdata.settings['proxy'].toString();
|
||||
if (proxy != "system" && proxy != "direct") {
|
||||
var proxyAvailable = await WebViewFeature.isFeatureSupported(
|
||||
WebViewFeature.PROXY_OVERRIDE);
|
||||
WebViewFeature.PROXY_OVERRIDE,
|
||||
);
|
||||
if (proxyAvailable) {
|
||||
ProxyController proxyController = ProxyController.instance();
|
||||
await proxyController.clearProxyOverride();
|
||||
@@ -104,11 +110,15 @@ class _AppWebviewState extends State<AppWebview> {
|
||||
);
|
||||
}
|
||||
}
|
||||
return WebViewEnvironment.create(
|
||||
if (!App.isWindows) {
|
||||
return true;
|
||||
}
|
||||
AppWebview.webViewEnvironment = await WebViewEnvironment.create(
|
||||
settings: WebViewEnvironmentSettings(
|
||||
userDataFolder: "${App.dataPath}\\webview",
|
||||
),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -147,22 +157,20 @@ class _AppWebviewState extends State<AppWebview> {
|
||||
)
|
||||
];
|
||||
|
||||
Widget body = (App.isWindows && AppWebview.webViewEnvironment == null)
|
||||
? FutureBuilder(
|
||||
future: future,
|
||||
builder: (context, e) {
|
||||
if (e.error != null) {
|
||||
return Center(child: Text("Error: ${e.error}"));
|
||||
}
|
||||
if (e.data == null) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
AppWebview.webViewEnvironment = e.data;
|
||||
return createWebviewWithEnvironment(
|
||||
AppWebview.webViewEnvironment);
|
||||
},
|
||||
)
|
||||
: createWebviewWithEnvironment(AppWebview.webViewEnvironment);
|
||||
Widget body = FutureBuilder(
|
||||
future: future,
|
||||
builder: (context, e) {
|
||||
if (e.error != null) {
|
||||
return Center(child: Text("Error: ${e.error}"));
|
||||
}
|
||||
if (!e.hasData) {
|
||||
return const SizedBox();
|
||||
}
|
||||
return createWebviewWithEnvironment(
|
||||
AppWebview.webViewEnvironment,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
body = Stack(
|
||||
children: [
|
||||
|
@@ -85,6 +85,10 @@ abstract class CBZ {
|
||||
if (cache.existsSync()) cache.deleteSync(recursive: true);
|
||||
cache.createSync();
|
||||
await extractArchive(file, cache);
|
||||
var f = cache.listSync();
|
||||
if (f.length == 1 && f.first is Directory) {
|
||||
cache = f.first as Directory;
|
||||
}
|
||||
var metaDataFile = File(FilePath.join(cache.path, 'metadata.json'));
|
||||
ComicMetaData? metaData;
|
||||
if (metaDataFile.existsSync()) {
|
||||
@@ -111,7 +115,17 @@ abstract class CBZ {
|
||||
cache.deleteSync(recursive: true);
|
||||
throw Exception('No images found in the archive');
|
||||
}
|
||||
files.sort((a, b) => a.path.compareTo(b.path));
|
||||
files.sort((a, b) {
|
||||
var aName = a.basenameWithoutExt;
|
||||
var bName = b.basenameWithoutExt;
|
||||
var aIndex = int.tryParse(aName);
|
||||
var bIndex = int.tryParse(bName);
|
||||
if (aIndex != null && bIndex != null) {
|
||||
return aIndex.compareTo(bIndex);
|
||||
} else {
|
||||
return a.path.compareTo(b.path);
|
||||
}
|
||||
});
|
||||
var coverFile = files.firstWhereOrNull(
|
||||
(element) =>
|
||||
element.path.endsWith('cover.${element.path.split('.').last}'),
|
||||
@@ -171,7 +185,7 @@ abstract class CBZ {
|
||||
return comic;
|
||||
}
|
||||
|
||||
static Future<File> export(LocalComic comic) async {
|
||||
static Future<File> export(LocalComic comic, String outFilePath) async {
|
||||
var cache = Directory(FilePath.join(App.cachePath, 'cbz_export'));
|
||||
if (cache.existsSync()) cache.deleteSync(recursive: true);
|
||||
cache.createSync();
|
||||
@@ -230,7 +244,7 @@ abstract class CBZ {
|
||||
).toJson(),
|
||||
),
|
||||
);
|
||||
var cbz = File(FilePath.join(App.cachePath, sanitizeFileName('${comic.title}.cbz')));
|
||||
var cbz = File(outFilePath);
|
||||
if (cbz.existsSync()) cbz.deleteSync();
|
||||
await _compress(cache.path, cbz.path);
|
||||
cache.deleteSync(recursive: true);
|
||||
|
@@ -118,6 +118,7 @@ class DataSync with ChangeNotifier {
|
||||
await client.remove(files.first.name!);
|
||||
}
|
||||
await client.write(filename, await data.readAsBytes());
|
||||
data.deleteIgnoreError();
|
||||
Log.info("Upload Data", "Data uploaded successfully");
|
||||
return const Res(true);
|
||||
} catch (e, s) {
|
||||
|
@@ -24,7 +24,8 @@ class EpubData {
|
||||
});
|
||||
}
|
||||
|
||||
Future<File> createEpubComic(EpubData data, String cacheDir) async {
|
||||
Future<File> createEpubComic(
|
||||
EpubData data, String cacheDir, String outFilePath) async {
|
||||
final workingDir = Directory(FilePath.join(cacheDir, 'epub'));
|
||||
if (workingDir.existsSync()) {
|
||||
workingDir.deleteSync(recursive: true);
|
||||
@@ -109,8 +110,7 @@ ${images.map((e) => ' <img src="$e" alt="$e"/>').join('\n')}
|
||||
}
|
||||
|
||||
// content.opf
|
||||
final contentOpf =
|
||||
File(FilePath.join(workingDir.path, 'content.opf'));
|
||||
final contentOpf = File(FilePath.join(workingDir.path, 'content.opf'));
|
||||
final uuid = const Uuid().v4();
|
||||
var spineStrBuilder = StringBuffer();
|
||||
for (var i = 0; i < chapterIndex; i++) {
|
||||
@@ -171,16 +171,15 @@ ${navMapStrBuilder.toString()}
|
||||
</ncx>
|
||||
''');
|
||||
|
||||
// zip
|
||||
final zipPath = FilePath.join(cacheDir, '${data.title}.epub');
|
||||
ZipFile.compressFolder(workingDir.path, zipPath);
|
||||
ZipFile.compressFolder(workingDir.path, outFilePath);
|
||||
|
||||
workingDir.deleteSync(recursive: true);
|
||||
|
||||
return File(zipPath);
|
||||
return File(outFilePath);
|
||||
}
|
||||
|
||||
Future<File> createEpubWithLocalComic(LocalComic comic) async {
|
||||
Future<File> createEpubWithLocalComic(
|
||||
LocalComic comic, String outFilePath) async {
|
||||
var chapters = <String, List<File>>{};
|
||||
if (comic.chapters == null) {
|
||||
chapters[comic.title] =
|
||||
@@ -188,11 +187,11 @@ Future<File> createEpubWithLocalComic(LocalComic comic) async {
|
||||
.map((e) => File(e))
|
||||
.toList();
|
||||
} else {
|
||||
for (var chapter in comic.chapters!.keys) {
|
||||
chapters[comic.chapters![chapter]!] = (await LocalManager()
|
||||
.getImages(comic.id, comic.comicType, chapter))
|
||||
.map((e) => File(e))
|
||||
.toList();
|
||||
for (var chapter in comic.downloadedChapters) {
|
||||
chapters[comic.chapters![chapter]!] =
|
||||
(await LocalManager().getImages(comic.id, comic.comicType, chapter))
|
||||
.map((e) => File(e))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
var data = EpubData(
|
||||
@@ -205,6 +204,6 @@ Future<File> createEpubWithLocalComic(LocalComic comic) async {
|
||||
final cacheDir = App.cachePath;
|
||||
|
||||
return Isolate.run(() => overrideIO(() async {
|
||||
return createEpubComic(data, cacheDir);
|
||||
return createEpubComic(data, cacheDir, outFilePath);
|
||||
}));
|
||||
}
|
||||
|
@@ -25,7 +25,9 @@ extension ListExt<T> on List<T>{
|
||||
}
|
||||
}
|
||||
|
||||
bool isEqualsTo(List<T> list){
|
||||
/// Compare every element of this list with another list.
|
||||
/// Return true if all elements are equal.
|
||||
bool isEqualTo(List<T> list){
|
||||
if(length != list.length){
|
||||
return false;
|
||||
}
|
||||
@@ -81,10 +83,6 @@ extension StringExt on String{
|
||||
return '$before$to$after';
|
||||
}
|
||||
|
||||
static bool hasMatch(String? value, String pattern) {
|
||||
return (value == null) ? false : RegExp(pattern).hasMatch(value);
|
||||
}
|
||||
|
||||
bool _isURL(){
|
||||
final regex = RegExp(
|
||||
r'^((http|https|ftp)://)[\w-]+(\.[\w-]+)+([\w.,@?^=%&:/~+#-|]*[\w@?^=%&/~+#-])?$',
|
||||
|
@@ -35,19 +35,9 @@ class FilePath {
|
||||
}
|
||||
|
||||
extension FileSystemEntityExt on FileSystemEntity {
|
||||
/// Get the base name of the file or directory.
|
||||
String get name {
|
||||
var path = this.path;
|
||||
if (path.endsWith('/') || path.endsWith('\\')) {
|
||||
path = path.substring(0, path.length - 1);
|
||||
}
|
||||
|
||||
int i = path.length - 1;
|
||||
|
||||
while (i >= 0 && path[i] != '\\' && path[i] != '/') {
|
||||
i--;
|
||||
}
|
||||
|
||||
return path.substring(i + 1);
|
||||
return p.basename(path);
|
||||
}
|
||||
|
||||
Future<void> deleteIgnoreError({bool recursive = false}) async {
|
||||
@@ -83,6 +73,10 @@ extension FileExtension on File {
|
||||
// Stream is not usable since [AndroidFile] does not support [openRead].
|
||||
await newFile.writeAsBytes(await readAsBytes());
|
||||
}
|
||||
|
||||
String get basenameWithoutExt {
|
||||
return p.basenameWithoutExtension(path);
|
||||
}
|
||||
}
|
||||
|
||||
extension DirectoryExtension on Directory {
|
||||
|
@@ -30,14 +30,14 @@ Future<void> _createPdfFromComic({
|
||||
files.removeWhere(
|
||||
(element) => element is! File || element.path.startsWith('cover'));
|
||||
files.sort((a, b) {
|
||||
var aName = (a as File).name;
|
||||
var bName = (b as File).name;
|
||||
var aName = (a as File).basenameWithoutExt;
|
||||
var bName = (b as File).basenameWithoutExt;
|
||||
var aNumber = int.tryParse(aName);
|
||||
var bNumber = int.tryParse(bName);
|
||||
if (aNumber != null && bNumber != null) {
|
||||
return aNumber.compareTo(bNumber);
|
||||
}
|
||||
return aName.compareTo(bName);
|
||||
return a.name.compareTo(b.name);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ Future<void> _createPdfFromComic({
|
||||
images.add(file.path);
|
||||
}
|
||||
} else {
|
||||
for (var chapter in comic.chapters!.keys) {
|
||||
for (var chapter in comic.downloadedChapters) {
|
||||
var files = Directory(FilePath.join(baseDir, chapter)).listSync();
|
||||
reorderFiles(files);
|
||||
for (var file in files) {
|
||||
@@ -112,10 +112,7 @@ Future<Isolate> _runIsolate(
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> createPdfFromComicIsolate({
|
||||
required LocalComic comic,
|
||||
required String savePath,
|
||||
}) async {
|
||||
Future<File> createPdfFromComicIsolate(LocalComic comic, String savePath) async {
|
||||
var receivePort = ReceivePort();
|
||||
SendPort? sendPort;
|
||||
Isolate? isolate;
|
||||
@@ -134,7 +131,8 @@ Future<void> createPdfFromComicIsolate({
|
||||
}
|
||||
});
|
||||
isolate = await _runIsolate(comic, savePath, receivePort.sendPort);
|
||||
return completer.future;
|
||||
await completer.future;
|
||||
return File(savePath);
|
||||
}
|
||||
|
||||
class PdfGenerator {
|
||||
|
@@ -15,9 +15,6 @@ extension TagsTranslation on String{
|
||||
static final Map<String, Map<String, String>> _data = {};
|
||||
|
||||
static Future<void> readData() async{
|
||||
if(App.locale.languageCode != "zh"){
|
||||
return;
|
||||
}
|
||||
var fileName = App.locale.countryCode == 'TW'
|
||||
? "assets/tags_tw.json"
|
||||
: "assets/tags.json";
|
||||
|
79
pubspec.lock
79
pubspec.lock
@@ -307,19 +307,21 @@ packages:
|
||||
flutter_inappwebview:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_inappwebview
|
||||
sha256: "80092d13d3e29b6227e25b67973c67c7210bd5e35c4b747ca908e31eb71a46d5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.5"
|
||||
path: flutter_inappwebview
|
||||
ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
|
||||
resolved-ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
|
||||
url: "https://github.com/pichillilorenzo/flutter_inappwebview"
|
||||
source: git
|
||||
version: "6.2.0-beta.3"
|
||||
flutter_inappwebview_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_inappwebview_android
|
||||
sha256: "62557c15a5c2db5d195cb3892aab74fcaec266d7b86d59a6f0027abd672cddba"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.3"
|
||||
path: flutter_inappwebview_android
|
||||
ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
|
||||
resolved-ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
|
||||
url: "https://github.com/pichillilorenzo/flutter_inappwebview"
|
||||
source: git
|
||||
version: "1.2.0-beta.3"
|
||||
flutter_inappwebview_internal_annotations:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -331,43 +333,48 @@ packages:
|
||||
flutter_inappwebview_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_inappwebview_ios
|
||||
sha256: "5818cf9b26cf0cbb0f62ff50772217d41ea8d3d9cc00279c45f8aabaa1b4025d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
path: flutter_inappwebview_ios
|
||||
ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
|
||||
resolved-ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
|
||||
url: "https://github.com/pichillilorenzo/flutter_inappwebview"
|
||||
source: git
|
||||
version: "1.2.0-beta.3"
|
||||
flutter_inappwebview_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_inappwebview_macos
|
||||
sha256: c1fbb86af1a3738e3541364d7d1866315ffb0468a1a77e34198c9be571287da1
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
path: flutter_inappwebview_macos
|
||||
ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
|
||||
resolved-ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
|
||||
url: "https://github.com/pichillilorenzo/flutter_inappwebview"
|
||||
source: git
|
||||
version: "1.2.0-beta.3"
|
||||
flutter_inappwebview_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_inappwebview_platform_interface
|
||||
sha256: cf5323e194096b6ede7a1ca808c3e0a078e4b33cc3f6338977d75b4024ba2500
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0+1"
|
||||
path: flutter_inappwebview_platform_interface
|
||||
ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
|
||||
resolved-ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
|
||||
url: "https://github.com/pichillilorenzo/flutter_inappwebview"
|
||||
source: git
|
||||
version: "1.4.0-beta.3"
|
||||
flutter_inappwebview_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_inappwebview_web
|
||||
sha256: "55f89c83b0a0d3b7893306b3bb545ba4770a4df018204917148ebb42dc14a598"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
path: flutter_inappwebview_web
|
||||
ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
|
||||
resolved-ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
|
||||
url: "https://github.com/pichillilorenzo/flutter_inappwebview"
|
||||
source: git
|
||||
version: "1.2.0-beta.3"
|
||||
flutter_inappwebview_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_inappwebview_windows
|
||||
sha256: "8b4d3a46078a2cdc636c4a3d10d10f2a16882f6be607962dbfff8874d1642055"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.0"
|
||||
path: flutter_inappwebview_windows
|
||||
ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
|
||||
resolved-ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
|
||||
url: "https://github.com/pichillilorenzo/flutter_inappwebview"
|
||||
source: git
|
||||
version: "0.7.0-beta.3"
|
||||
flutter_lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@@ -1100,4 +1107,4 @@ packages:
|
||||
version: "0.0.10"
|
||||
sdks:
|
||||
dart: ">=3.6.0 <4.0.0"
|
||||
flutter: ">=3.27.3"
|
||||
flutter: ">=3.27.4"
|
||||
|
12
pubspec.yaml
12
pubspec.yaml
@@ -2,11 +2,11 @@ name: venera
|
||||
description: "A comic app."
|
||||
publish_to: 'none'
|
||||
|
||||
version: 1.2.3+123
|
||||
version: 1.2.5+125
|
||||
|
||||
environment:
|
||||
sdk: '>=3.6.0 <4.0.0'
|
||||
flutter: 3.27.3
|
||||
flutter: 3.27.4
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
@@ -43,7 +43,11 @@ dependencies:
|
||||
git:
|
||||
url: https://github.com/wgh136/flutter_desktop_webview
|
||||
path: packages/desktop_webview_window
|
||||
flutter_inappwebview: ^6.1.5
|
||||
flutter_inappwebview:
|
||||
git:
|
||||
url: https://github.com/pichillilorenzo/flutter_inappwebview
|
||||
path: flutter_inappwebview
|
||||
ref: 0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676
|
||||
app_links: ^6.3.3
|
||||
sliver_tools: ^0.2.12
|
||||
flutter_file_dialog: ^3.0.2
|
||||
@@ -82,7 +86,7 @@ dev_dependencies:
|
||||
sdk: flutter
|
||||
flutter_lints: ^5.0.0
|
||||
flutter_to_arch: ^1.0.1
|
||||
flutter_to_debian:
|
||||
flutter_to_debian: ^2.0.2
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
|
@@ -3,11 +3,36 @@
|
||||
|
||||
#define MyAppName "Venera"
|
||||
#define MyAppVersion "{{version}}"
|
||||
#define MyAppPublisher "wgh136"
|
||||
#define MyAppPublisher "nyne"
|
||||
#define MyAppURL "https://github.com/venera-app/venera"
|
||||
#define MyAppExeName "venera.exe"
|
||||
#define RootPath "{{root_path}}"
|
||||
|
||||
[Code]
|
||||
procedure CurStepChanged(CurStep: TSetupStep);
|
||||
var
|
||||
OldVersionPath, ShortcutPath: string;
|
||||
begin
|
||||
if CurStep = ssInstall then
|
||||
begin
|
||||
OldVersionPath := 'C:\Program Files (x86)\Venera';
|
||||
if DirExists(OldVersionPath) then
|
||||
begin
|
||||
DelTree(OldVersionPath, True, True, True);
|
||||
ShortcutPath := GetEnv('USERPROFILE') + '\Desktop\Venera.lnk';
|
||||
if FileExists(ShortcutPath) then
|
||||
begin
|
||||
DeleteFile(ShortcutPath);
|
||||
end;
|
||||
ShortcutPath := 'C:\Users\Public\Desktop\Venera.lnk';
|
||||
if FileExists(ShortcutPath) then
|
||||
begin
|
||||
DeleteFile(ShortcutPath);
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
[Setup]
|
||||
; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
|
||||
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
|
||||
@@ -30,6 +55,8 @@ SetupIconFile={#RootPath}\windows\runner\resources\app_icon.ico
|
||||
Compression=lzma
|
||||
SolidCompression=yes
|
||||
WizardStyle=modern
|
||||
ArchitecturesInstallIn64BitMode=x64compatible
|
||||
ArchitecturesAllowed=x64compatible
|
||||
|
||||
[Languages]
|
||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||
|
@@ -29,7 +29,7 @@ file.close()
|
||||
|
||||
if not os.path.exists("windows/ChineseSimplified.isl"):
|
||||
# download ChineseSimplified.isl
|
||||
url = "https://raw.githubusercontent.com/kira-96/Inno-Setup-Chinese-Simplified-Translation/refs/heads/main/ChineseSimplified.isl"
|
||||
url = "https://cdn.jsdelivr.net/gh/kira-96/Inno-Setup-Chinese-Simplified-Translation@latest/ChineseSimplified.isl"
|
||||
response = httpx.get(url)
|
||||
with open('windows/ChineseSimplified.isl', 'wb') as file:
|
||||
file.write(response.content)
|
||||
|
Reference in New Issue
Block a user