45 Commits

Author SHA1 Message Date
fec1926774 Fix webview 2025-02-13 12:14:57 +08:00
nyne
7cd0a20785 Merge pull request #191 from venera-app/v1.2.5-dev
V1.2.5
2025-02-13 11:05:20 +08:00
ed124d0419 Fix calculation 2025-02-13 11:01:42 +08:00
14c3e9ea43 Fixed the storage of chapter read information. 2025-02-13 10:47:54 +08:00
d2aca7ce44 Improve sorting images when importing comic. 2025-02-13 10:09:08 +08:00
34194559f5 Improve chapters display 2025-02-13 10:05:38 +08:00
18c5d5d85a Fix image overflow 2025-02-13 09:49:05 +08:00
9b1bafcbe1 Improve gesture 2025-02-13 09:43:36 +08:00
dd7e2d6744 Improve aggregated_search_page 2025-02-11 21:13:57 +08:00
51c2bf0d6f [windows] Replace desktop_webview_window with flutter_inappwebview 2025-02-11 20:08:02 +08:00
53e5ebbbf6 Update version code 2025-02-11 19:21:44 +08:00
c600d99c58 Add Reverse Tap to Turn Page. Close #186 2025-02-11 19:02:16 +08:00
f4804faf52 Improve reader gesture. Close #185 2025-02-11 18:51:27 +08:00
c7d72347a9 typo 2025-02-11 17:55:17 +08:00
a4e2d4f6e4 Update js api 2025-02-11 13:58:17 +08:00
5c7cd7a304 Improve multi-folder favorites management. 2025-02-11 13:51:19 +08:00
9fb63e47ea Fix deleting comic in favorites page. 2025-02-11 13:23:51 +08:00
fc66e8ae2d Fix getLocale 2025-02-11 13:16:16 +08:00
d04c872491 Merge branch 'v1.3.0-dev' 2025-02-11 13:09:17 +08:00
426936082e Fix description overflow 2025-02-11 13:08:24 +08:00
5129530e56 Update issue template. 2025-02-11 11:07:55 +08:00
3735249de6 Fix the issue where page is not reloaded after changing search options in search results page. 2025-02-09 21:15:31 +08:00
nyne
8868a02a7e Merge pull request #183 from venera-app/v1.2.4-dev
V1.2.4
2025-02-09 19:59:26 +08:00
nyne
e1b95c9e23 Merge branch 'master' into v1.2.4-dev 2025-02-09 19:57:42 +08:00
0b65b4ab53 Update version code 2025-02-09 19:32:10 +08:00
df4263f969 Add ability to manage search sources. Close #174 2025-02-09 19:29:51 +08:00
17ef17ca5b Add a button for managing network folders. 2025-02-09 18:22:38 +08:00
nyne
e55c45a589 Support Linux arm64. Close #176 2025-02-09 15:11:46 +08:00
591f2836d4 Improve windows build script. 2025-02-09 13:45:30 +08:00
8ab4f7a34b Fix the issue where cache files are not deleted. 2025-02-09 11:38:19 +08:00
614c01872b Fix auto language filter. Close #171 2025-02-08 21:10:43 +08:00
6be258092a Remove confirmation prompt from deb. Close #177 2025-02-08 20:40:45 +08:00
ce50812857 Fix invalid image order when exporting comic as pdf. 2025-02-08 19:37:04 +08:00
f0b1135eb7 Allow batch export. Close #179 2025-02-08 18:23:49 +08:00
shenmo
cc0f070df5 Use Ubuntu 22.04 to run the workflow. (#178) 2025-02-07 19:19:39 +08:00
35429c132c Improve comic page performance 2025-02-07 18:15:36 +08:00
998d4c31d3 Improve importing comic: If the archive has only one directory, set working dir as it. 2025-02-07 17:32:51 +08:00
0122bb8f28 fix windows font 2025-02-07 17:28:03 +08:00
33a9fa062b flutter 3.27.4 2025-02-07 17:19:26 +08:00
13081332f2 Improve tags display 2025-02-07 17:19:04 +08:00
Pacalini
cdc6c95579 pre-search: enable suggestions for EN (#175) 2025-02-07 17:16:41 +08:00
buste
3aca3baafc Fix ensure searchTarget is properly initialized for aggregatedSearch mode (#173)
Set searchTarget = defaultSearchTarget when aggregatedSearch is enabled, ensuring correct initialization and preventing missing suggestions on first input.

Without this fix, when opening the search page for the first time with aggregatedSearch enabled by default, entering an ID that matches a comic source does not trigger the "Open comic" suggestion. However, after toggling aggregatedSearch off and then back on, the same ID input correctly displays the suggestion.
2025-02-07 17:03:52 +08:00
58d6ccdde1 Fix an issue where an application turns to a white screen after finishing cloudflare verification. Close #169 2025-02-05 21:21:20 +08:00
23404b86f6 Record the last state of the favorite pane. 2025-02-05 20:40:14 +08:00
UjuiUjuMandan
965187e9de replace raw.githubusercontent.com 2025-02-05 20:21:15 +08:00
49 changed files with 1135 additions and 592 deletions

View File

@@ -7,6 +7,10 @@ body:
attributes: attributes:
value: | value: |
Thank you for reporting a problem, please complete the title and fill in the following information. 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 - type: textarea
id: what-happened id: what-happened
attributes: attributes:
@@ -19,7 +23,8 @@ body:
attributes: attributes:
label: Version label: Version
description: | description: |
App version App version.
Please try to update if it is not the latest version Please try to update if it is not the latest version
validations: validations:
required: true required: true

View File

@@ -39,12 +39,18 @@ jobs:
ln -s /Applications dist/dmg_contents/Applications ln -s /Applications dist/dmg_contents/Applications
hdiutil create -volname "venera" -srcfolder dist/dmg_contents -ov -format UDZO "dist/venera.dmg" 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) # Step 4: Attach and upload artifacts (optional)
- name: Upload DMG - name: Upload DMG
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: venera.dmg name: macos_build
path: dist/venera.dmg path: result/
Build_IOS: Build_IOS:
runs-on: macos-15 runs-on: macos-15
steps: 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 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/ cd /Users/runner/work/venera/venera/build/ios/iphoneos/
zip -r venera-ios.ipa Payload 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 - uses: actions/upload-artifact@v4
with: with:
name: app-ios.ipa name: ios_build
path: /Users/runner/work/venera/venera/build/ios/iphoneos/venera-ios.ipa path: result/
Build_Android: Build_Android:
runs-on: ubuntu-latest runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: subosito/flutter-action@v2 - uses: subosito/flutter-action@v2
@@ -118,7 +129,7 @@ jobs:
name: windows_build name: windows_build
path: build/windows/Venera-* path: build/windows/Venera-*
Build_Linux: Build_Linux:
runs-on: ubuntu-latest runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: subosito/flutter-action@v2 - uses: subosito/flutter-action@v2
@@ -130,7 +141,7 @@ jobs:
sudo apt-get update -y sudo apt-get update -y
sudo apt-get install -y ninja-build libgtk-3-dev webkit2gtk-4.1 sudo apt-get install -y ninja-build libgtk-3-dev webkit2gtk-4.1
dart pub global activate flutter_to_debian 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: dart run flutter_to_arch
- run: | - run: |
sudo rm -rf build/linux/arch/app.tar.gz sudo rm -rf build/linux/arch/app.tar.gz
@@ -145,19 +156,43 @@ jobs:
with: with:
name: arch_build name: arch_build
path: build/linux/arch/ 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: Release:
runs-on: ubuntu-latest runs-on: ubuntu-22.04
needs: [Build_MacOS, Build_IOS, Build_Android, Build_Windows, Build_Linux] needs: [Build_MacOS, Build_IOS, Build_Android, Build_Windows, Build_Linux, Build_Linux_ARM64]
if: github.event_name == 'release' # 仅在 push 事件时执行 if: github.event_name == 'release' # 仅在 push 事件时执行
steps: steps:
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
name: venera.dmg name: macos_build
path: outputs path: outputs
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
name: app-ios.ipa name: ios_build
path: outputs path: outputs
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
@@ -175,6 +210,10 @@ jobs:
with: with:
name: arch_build name: arch_build
path: outputs path: outputs
- uses: actions/download-artifact@v4
with:
name: deb_arm64_build
path: outputs
- uses: softprops/action-gh-release@v2 - uses: softprops/action-gh-release@v2
with: with:
tag_name: ${{ github.ref_name }} tag_name: ${{ github.ref_name }}

View File

@@ -496,7 +496,7 @@ let Network = {
/** /**
* [fetch] function for sending HTTP requests. Same api as the browser fetch. * [fetch] function for sending HTTP requests. Same api as the browser fetch.
* @param url {string} * @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>)}>} * @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 * @since 1.2.0
*/ */
@@ -921,7 +921,7 @@ function Comic({id, title, subtitle, subTitle, cover, tags, description, maxPage
* @param description {string?} * @param description {string?}
* @param tags {Map<string, string[]> | {} | null | undefined} * @param tags {Map<string, string[]> | {} | null | undefined}
* @param chapters {Map<string, string> | {} | null | undefined} - key: chapter id, value: chapter title * @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 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 thumbnails {string[]?} - for multiple page thumbnails, set this to null, and use `loadThumbnails` api to load thumbnails
* @param recommend {Comic[]?} - related comics * @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() { } init() { }
static sources = {} static sources = {}

View File

@@ -139,8 +139,8 @@
"Block": "屏蔽", "Block": "屏蔽",
"Add new favorite to": "添加新收藏到", "Add new favorite to": "添加新收藏到",
"Move favorite after reading": "阅读后移动收藏", "Move favorite after reading": "阅读后移动收藏",
"Delete folder?" : "除文件夾?", "Delete folder?" : "除文件夹?",
"Delete folder '@f' ?" : "删除文件夹 '@f' ", "Delete folder '@f' ?" : "删除文件夹 '@f' ?",
"Import from file": "从文件导入", "Import from file": "从文件导入",
"Failed to import": "导入失败", "Failed to import": "导入失败",
"Cache Limit": "缓存限制", "Cache Limit": "缓存限制",
@@ -321,7 +321,18 @@
"Manage": "管理", "Manage": "管理",
"Verify": "验证", "Verify": "验证",
"Cloudflare verification required": "需要Cloudflare验证", "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": { "zh_TW": {
"Home": "首頁", "Home": "首頁",
@@ -645,6 +656,17 @@
"Manage": "管理", "Manage": "管理",
"Verify": "驗證", "Verify": "驗證",
"Cloudflare verification required": "需要Cloudflare驗證", "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
View File

@@ -1,5 +1,7 @@
import subprocess import subprocess
import sys
arch = sys.argv[1]
debianContent = '' debianContent = ''
desktopContent = '' desktopContent = ''
version = '' version = ''
@@ -12,7 +14,14 @@ with open('pubspec.yaml', 'r') as f:
version = str.split(str.split(f.read(), 'version: ')[1], '+')[0] version = str.split(str.split(f.read(), 'version: ')[1], '+')[0]
with open('debian/debian.yaml', 'w') as f: 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: with open('debian/gui/venera.desktop', 'w') as f:
f.write(desktopContent.replace('{{Version}}', version)) f.write(desktopContent.replace('{{Version}}', version))

6
debian/debian.yaml vendored
View File

@@ -1,13 +1,13 @@
flutter_app: flutter_app:
command: venera command: venera
arch: x64 arch: {{Arch}}
parent: /usr/local/lib parent: /usr/local/lib
nonInteractive: false nonInteractive: true
control: control:
Package: venera Package: venera
Version: {{Version}} Version: {{Version}}
Architecture: amd64 Architecture: {{Architecture}}
Priority: optional Priority: optional
Depends: libwebkit2gtk-4.1-0, libgtk-3-0 Depends: libwebkit2gtk-4.1-0, libgtk-3-0
Maintainer: nyne Maintainer: nyne

View File

@@ -550,7 +550,7 @@ class _ComicDescription extends StatelessWidget {
int cnt = (constraints.maxHeight - 22).toInt() ~/ 25; int cnt = (constraints.maxHeight - 22).toInt() ~/ 25;
return Container( return Container(
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
height: 22 + cnt * 25, height: 21 + cnt * 24,
width: double.infinity, width: double.infinity,
decoration: const BoxDecoration(), decoration: const BoxDecoration(),
child: Wrap( child: Wrap(
@@ -562,31 +562,30 @@ class _ComicDescription extends StatelessWidget {
children: [ children: [
for (var s in tags!) for (var s in tags!)
Container( Container(
height: 22, height: 21,
padding: const EdgeInsets.fromLTRB(3, 2, 3, 2), padding: const EdgeInsets.symmetric(horizontal: 4),
constraints: BoxConstraints( constraints: BoxConstraints(
maxWidth: constraints.maxWidth * 0.45, 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); ).toAlign(Alignment.topCenter);
@@ -607,23 +606,26 @@ class _ComicDescription extends StatelessWidget {
style: const TextStyle( style: const TextStyle(
fontSize: 12.0, fontSize: 12.0,
), ),
maxLines: 1,
overflow: TextOverflow.ellipsis,
), ),
], ],
), ),
), ),
if (badge != null) if (badge != null)
Container( Container(
padding: const EdgeInsets.fromLTRB(6, 4, 6, 4), padding: const EdgeInsets.fromLTRB(6, 4, 6, 4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.tertiaryContainer, color: Theme.of(context).colorScheme.tertiaryContainer,
borderRadius: const BorderRadius.all(Radius.circular(8)), 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 @override
void didUpdateWidget(covariant SliverGridComics oldWidget) { void didUpdateWidget(covariant SliverGridComics oldWidget) {
if (oldWidget.comics != widget.comics) { if (oldWidget.comics.isEqualTo(widget.comics)) {
comics.clear(); comics.clear();
for (var comic in widget.comics) { for (var comic in widget.comics) {
if (isBlocked(comic) == null) { if (isBlocked(comic) == null) {
@@ -1520,14 +1522,15 @@ class SimpleComicTile extends StatelessWidget {
return AnimatedTapRegion( return AnimatedTapRegion(
borderRadius: 8, borderRadius: 8,
onTap: onTap ?? () { onTap: onTap ??
context.to( () {
() => ComicPage( context.to(
id: comic.id, () => ComicPage(
sourceKey: comic.sourceKey, id: comic.id,
), sourceKey: comic.sourceKey,
); ),
}, );
},
child: Container( child: Container(
width: 92, width: 92,
height: 114, height: 114,

View File

@@ -148,3 +148,18 @@ class SliverGridDelegateWithComics extends SliverGridDelegate {
return false; 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,
]);
}
}

View File

@@ -168,7 +168,15 @@ Future<void> showConfirmDialog({
} }
class LoadingDialogController { 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; bool closed = false;
@@ -177,63 +185,86 @@ class LoadingDialogController {
return; return;
} }
closed = true; closed = true;
if (closeDialog == null) { if (_closeDialog == null) {
Future.microtask(closeDialog!); Future.microtask(_closeDialog!);
} else { } 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, LoadingDialogController showLoadingDialog(
{void Function()? onCancel, BuildContext context, {
bool barrierDismissible = true, void Function()? onCancel,
bool allowCancel = true, bool barrierDismissible = true,
String? message, bool allowCancel = true,
String cancelButtonText = "Cancel"}) { String? message,
String cancelButtonText = "Cancel",
bool withProgress = false,
}) {
var controller = LoadingDialogController(); var controller = LoadingDialogController();
controller._message = message;
if (withProgress) {
controller._progress = 0;
}
var loadingDialogRoute = DialogRoute( var loadingDialogRoute = DialogRoute(
context: context, context: context,
barrierDismissible: barrierDismissible, barrierDismissible: barrierDismissible,
builder: (BuildContext context) { builder: (BuildContext context) {
return Dialog( return StatefulBuilder(builder: (context, setState) {
child: Container( controller._serProgress = (value) {
width: 100, setState(() {
padding: const EdgeInsets.all(16.0), controller._progress = value;
child: Row( });
children: [ };
const SizedBox( controller._setMessage = (message) {
width: 30, setState(() {
height: 30, controller._message = message;
child: CircularProgressIndicator(), });
), };
const SizedBox( return ContentDialog(
width: 16, title: controller._message ?? 'Loading',
), content: LinearProgressIndicator(
Text( value: controller._progress,
message ?? 'Loading', backgroundColor: context.colorScheme.surfaceContainer,
style: const TextStyle(fontSize: 16), ).paddingHorizontal(16).paddingVertical(16),
), actions: [
const Spacer(), FilledButton(
if (allowCancel) onPressed: allowCancel
TextButton( ? () {
onPressed: () { controller.close();
controller.close(); onCancel?.call();
onCancel?.call(); }
}, : null,
child: Text(cancelButtonText.tl)) child: Text(cancelButtonText.tl),
], )
), ],
),
); );
}); });
},
);
var navigator = Navigator.of(context, rootNavigator: true); var navigator = Navigator.of(context, rootNavigator: true);
navigator.push(loadingDialogRoute).then((value) => controller.closed = true); navigator.push(loadingDialogRoute).then((value) => controller.closed = true);
controller.closeDialog = () { controller._closeDialog = () {
navigator.removeRoute(loadingDialogRoute); navigator.removeRoute(loadingDialogRoute);
}; };
@@ -444,9 +475,7 @@ Future<int?> showSelectDialog({
child: Text('Cancel'.tl), child: Text('Cancel'.tl),
), ),
FilledButton( FilledButton(
onPressed: current == null onPressed: current == null ? null : context.pop,
? null
: context.pop,
child: Text('Confirm'.tl), child: Text('Confirm'.tl),
), ),
], ],

View File

@@ -10,7 +10,7 @@ export "widget_utils.dart";
export "context.dart"; export "context.dart";
class _App { class _App {
final version = "1.2.3"; final version = "1.2.5";
bool get isAndroid => Platform.isAndroid; bool get isAndroid => Platform.isAndroid;
@@ -52,7 +52,7 @@ class _App {
BuildContext get rootContext => rootNavigatorKey.currentContext!; BuildContext get rootContext => rootNavigatorKey.currentContext!;
void rootPop() { void rootPop() {
rootNavigatorKey.currentState?.pop(); rootNavigatorKey.currentState?.maybePop();
} }
void pop() { void pop() {

View File

@@ -126,6 +126,7 @@ class _Settings with ChangeNotifier {
'explore_pages': [], 'explore_pages': [],
'categories': [], 'categories': [],
'favorites': [], 'favorites': [],
'searchSources': null,
'showFavoriteStatusOnTile': true, 'showFavoriteStatusOnTile': true,
'showHistoryStatusOnTile': false, 'showHistoryStatusOnTile': false,
'blockedWords': [], 'blockedWords': [],
@@ -134,6 +135,7 @@ class _Settings with ChangeNotifier {
'readerMode': 'galleryLeftToRight', // values of [ReaderMode] 'readerMode': 'galleryLeftToRight', // values of [ReaderMode]
'readerScreenPicNumber': 1, // 1 - 5 'readerScreenPicNumber': 1, // 1 - 5
'enableTapToTurnPages': true, 'enableTapToTurnPages': true,
'reverseTapToTurnPages': false,
'enablePageAnimation': true, 'enablePageAnimation': true,
'language': 'system', // system, zh-CN, zh-TW, en-US 'language': 'system', // system, zh-CN, zh-TW, en-US
'cacheSize': 2048, // in MB 'cacheSize': 2048, // in MB
@@ -155,7 +157,7 @@ class _Settings with ChangeNotifier {
'customImageProcessing': defaultCustomImageProcessing, 'customImageProcessing': defaultCustomImageProcessing,
'sni': true, 'sni': true,
'autoAddLanguageFilter': 'none', // none, chinese, english, japanese '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) { operator [](String key) {

View File

@@ -417,7 +417,7 @@ class SearchOptions {
const SearchOptions(this.options, this.label, this.type, this.defaultVal); 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( typedef CategoryComicsLoader = Future<Res<List<Comic>>> Function(

View File

@@ -37,6 +37,8 @@ class FavoriteData {
final AddOrDelFavFunc? addOrDelFavorite; final AddOrDelFavFunc? addOrDelFavorite;
final bool singleFolderForSingleComic;
const FavoriteData({ const FavoriteData({
required this.key, required this.key,
required this.title, required this.title,
@@ -49,6 +51,7 @@ class FavoriteData {
this.allFavoritesId, this.allFavoritesId,
this.addOrDelFavorite, this.addOrDelFavorite,
this.isOldToNewSort, this.isOldToNewSort,
this.singleFolderForSingleComic = false,
}); });
} }

View File

@@ -620,6 +620,7 @@ class ComicSourceParser {
final bool multiFolder = _getValue("favorites.multiFolder"); final bool multiFolder = _getValue("favorites.multiFolder");
final bool? isOldToNewSort = _getValue("favorites.isOldToNewSort"); final bool? isOldToNewSort = _getValue("favorites.isOldToNewSort");
final bool? singleFolderForSingleComic = _getValue("favorites.singleFolderForSingleComic");
Future<Res<T>> retryZone<T>(Future<Res<T>> Function() func) async { Future<Res<T>> retryZone<T>(Future<Res<T>> Function() func) async {
if (!ComicSource.find(_key!)!.isLogged) { if (!ComicSource.find(_key!)!.isLogged) {
@@ -773,6 +774,7 @@ class ComicSourceParser {
deleteFolder: deleteFolder, deleteFolder: deleteFolder,
addOrDelFavorite: addOrDelFavFunc, addOrDelFavorite: addOrDelFavFunc,
isOldToNewSort: isOldToNewSort, isOldToNewSort: isOldToNewSort,
singleFolderForSingleComic: singleFolderForSingleComic ?? false,
); );
} }

View File

@@ -156,7 +156,7 @@ class JsEngine with _JSEngineApi, JsUiApi {
case "UI": case "UI":
return handleUIMessage(Map.from(message)); return handleUIMessage(Map.from(message));
case "getLocale": case "getLocale":
return "${App.locale.languageCode}-${App.locale.countryCode}"; return "${App.locale.languageCode}_${App.locale.countryCode}";
case "getPlatform": case "getPlatform":
return Platform.operatingSystem; return Platform.operatingSystem;
} }

View File

@@ -42,11 +42,16 @@ Future<void> init() async {
await ComicSource.init().wait(); await ComicSource.init().wait();
await LocalManager().init().wait(); await LocalManager().init().wait();
CacheManager().setLimitSize(appdata.settings['cacheSize']); 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) { if (App.isAndroid) {
handleLinks(); handleLinks();
} }
FlutterError.onError = (details) { FlutterError.onError = (details) {
Log.error( Log.error("Unhandled Exception", "${details.exception}\n${details.stack}");
"Unhandled Exception", "${details.exception}\n${details.stack}");
}; };
} }

View File

@@ -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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget home; Widget home;
@@ -158,24 +190,9 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
return MaterialApp( return MaterialApp(
home: home, home: home,
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
theme: ThemeData( theme: getTheme(primary, secondary, tertiary, Brightness.light),
colorScheme: SeedColorScheme.fromSeeds(
primaryKey: primary,
secondaryKey: secondary,
tertiaryKey: tertiary,
tones: FlexTones.vividBackground(Brightness.light),
),
),
navigatorKey: App.rootNavigatorKey, navigatorKey: App.rootNavigatorKey,
darkTheme: ThemeData( darkTheme: getTheme(primary, secondary, tertiary, Brightness.dark),
colorScheme: SeedColorScheme.fromSeeds(
primaryKey: primary,
secondaryKey: secondary,
tertiaryKey: tertiary,
brightness: Brightness.dark,
tones: FlexTones.vividBackground(Brightness.dark),
),
),
themeMode: switch (appdata.settings['theme_mode']) { themeMode: switch (appdata.settings['theme_mode']) {
'light' => ThemeMode.light, 'light' => ThemeMode.light,
'dark' => ThemeMode.dark, 'dark' => ThemeMode.dark,

View File

@@ -118,7 +118,7 @@ void passCloudflare(CloudflareException e, void Function() onFinished) async {
// windows version of package `flutter_inappwebview` cannot get some cookies // windows version of package `flutter_inappwebview` cannot get some cookies
// Using DesktopWebview instead // Using DesktopWebview instead
if (App.isLinux || App.isWindows) { if (App.isLinux) {
var webview = DesktopWebview( var webview = DesktopWebview(
initialUrl: url, initialUrl: url,
onTitleChange: (title, controller) async { onTitleChange: (title, controller) async {
@@ -152,6 +152,7 @@ void passCloudflare(CloudflareException e, void Function() onFinished) async {
); );
webview.open(); webview.open();
} else { } else {
bool success = false;
void check(InAppWebViewController controller) async { void check(InAppWebViewController controller) async {
var head = await controller.evaluateJavascript( var head = await controller.evaluateJavascript(
source: "document.head.innerHTML") as String; source: "document.head.innerHTML") as String;
@@ -176,7 +177,10 @@ void passCloudflare(CloudflareException e, void Function() onFinished) async {
return; return;
} }
SingleInstanceCookieJar.instance?.saveFromResponse(uri, cookies); SingleInstanceCookieJar.instance?.saveFromResponse(uri, cookies);
App.rootPop(); if (!success) {
App.rootPop();
success = true;
}
} }
} }

View File

@@ -2,6 +2,7 @@ import "package:flutter/material.dart";
import 'package:shimmer_animation/shimmer_animation.dart'; import 'package:shimmer_animation/shimmer_animation.dart';
import "package:venera/components/components.dart"; import "package:venera/components/components.dart";
import "package:venera/foundation/app.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/foundation/comic_source/comic_source.dart";
import "package:venera/pages/search_result_page.dart"; import "package:venera/pages/search_result_page.dart";
import "package:venera/utils/translations.dart"; import "package:venera/utils/translations.dart";
@@ -24,7 +25,18 @@ class _AggregatedSearchPageState extends State<AggregatedSearchPage> {
@override @override
void initState() { 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; _keyword = widget.keyword;
controller = SearchBarController( controller = SearchBarController(
currentText: widget.keyword, currentText: widget.keyword,
@@ -46,7 +58,11 @@ class _AggregatedSearchPageState extends State<AggregatedSearchPage> {
delegate: SliverChildBuilderDelegate( delegate: SliverChildBuilderDelegate(
(context, index) { (context, index) {
final source = sources[index]; final source = sources[index];
return _SliverSearchResult(source: source, keyword: _keyword); return _SliverSearchResult(
key: ValueKey(source.key),
source: source,
keyword: _keyword,
);
}, },
childCount: sources.length, childCount: sources.length,
), ),
@@ -56,7 +72,11 @@ class _AggregatedSearchPageState extends State<AggregatedSearchPage> {
} }
class _SliverSearchResult extends StatefulWidget { 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; final ComicSource source;
@@ -78,6 +98,8 @@ class _SliverSearchResultState extends State<_SliverSearchResult>
List<Comic>? comics; List<Comic>? comics;
String? error;
void load() async { void load() async {
final data = widget.source.searchPageData!; final data = widget.source.searchPageData!;
var options = var options =
@@ -89,6 +111,11 @@ class _SliverSearchResultState extends State<_SliverSearchResult>
comics = res.data; comics = res.data;
isLoading = false; isLoading = false;
}); });
} else {
setState(() {
error = res.errorMessage ?? "Unknown error".tl;
isLoading = false;
});
} }
} else if (data.loadNext != null) { } else if (data.loadNext != null) {
var res = await data.loadNext!(widget.keyword, null, options); var res = await data.loadNext!(widget.keyword, null, options);
@@ -97,6 +124,11 @@ class _SliverSearchResultState extends State<_SliverSearchResult>
comics = res.data; comics = res.data;
isLoading = false; isLoading = false;
}); });
} else {
setState(() {
error = res.errorMessage ?? "Unknown error".tl;
isLoading = false;
});
} }
} }
} }
@@ -127,6 +159,9 @@ class _SliverSearchResultState extends State<_SliverSearchResult>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (error != null && error!.startsWith("CloudflareException")) {
error = "Cloudflare verification required".tl;
}
super.build(context); super.build(context);
return InkWell( return InkWell(
onTap: () { 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( SizedBox(
height: _kComicHeight, height: _kComicHeight,
child: Column( child: Column(
@@ -178,7 +213,13 @@ class _SliverSearchResultState extends State<_SliverSearchResult>
children: [ children: [
const Icon(Icons.error_outline), const Icon(Icons.error_outline),
const SizedBox(width: 8), 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(), const Spacer(),

View File

@@ -32,7 +32,7 @@ class _CategoriesPageState extends State<CategoriesPage> {
.toList(); .toList();
categories = categories =
categories.where((element) => allCategories.contains(element)).toList(); categories.where((element) => allCategories.contains(element)).toList();
if (!categories.isEqualsTo(this.categories)) { if (!categories.isEqualTo(this.categories)) {
setState(() { setState(() {
this.categories = categories; this.categories = categories;
}); });

View File

@@ -49,19 +49,19 @@ class ComicPage extends StatefulWidget {
class _ComicPageState extends LoadingState<ComicPage, ComicDetails> class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
with _ComicPageActions { with _ComicPageActions {
@override
History? history;
bool showAppbarTitle = false; bool showAppbarTitle = false;
var scrollController = ScrollController(); var scrollController = ScrollController();
bool isDownloaded = false; bool isDownloaded = false;
void updateHistory() async { @override
var newHistory = await HistoryManager() void onReadEnd() {
.find(widget.id, ComicType(widget.sourceKey.hashCode)); // The history is passed by reference, so it will be updated automatically.
if (newHistory?.ep != history?.ep || newHistory?.page != history?.page) { update();
history = newHistory;
update();
}
} }
@override @override
@@ -77,14 +77,12 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
@override @override
void initState() { void initState() {
scrollController.addListener(onScroll); scrollController.addListener(onScroll);
HistoryManager().addListener(updateHistory);
super.initState(); super.initState();
} }
@override @override
void dispose() { void dispose() {
scrollController.removeListener(onScroll); scrollController.removeListener(onScroll);
HistoryManager().removeListener(updateHistory);
super.dispose(); super.dispose();
} }
@@ -206,62 +204,64 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
yield const SliverPadding(padding: EdgeInsets.only(top: 8)); yield const SliverPadding(padding: EdgeInsets.only(top: 8));
yield Row( yield SliverLazyToBoxAdapter(
crossAxisAlignment: CrossAxisAlignment.start, child: Row(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
const SizedBox(width: 16), children: [
Hero( const SizedBox(width: 16),
tag: "cover${comic.id}${comic.sourceKey}", Hero(
child: Container( tag: "cover${comic.id}${comic.sourceKey}",
decoration: BoxDecoration( child: Container(
color: context.colorScheme.primaryContainer, decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8), color: context.colorScheme.primaryContainer,
boxShadow: [ borderRadius: BorderRadius.circular(8),
BoxShadow( boxShadow: [
color: context.colorScheme.outlineVariant, BoxShadow(
blurRadius: 1, color: context.colorScheme.outlineVariant,
offset: const Offset(0, 1), 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() { Widget buildActions() {
bool isMobile = context.width < changePoint; bool isMobile = context.width < changePoint;
bool hasHistory = history != null && (history!.ep > 1 || history!.page > 1); bool hasHistory = history != null && (history!.ep > 1 || history!.page > 1);
return SliverToBoxAdapter( return SliverLazyToBoxAdapter(
child: Column( child: Column(
children: [ children: [
ListView( ListView(
@@ -354,7 +354,7 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
if (comic.description == null || comic.description!.trim().isEmpty) { if (comic.description == null || comic.description!.trim().isEmpty) {
return const SliverPadding(padding: EdgeInsets.zero); return const SliverPadding(padding: EdgeInsets.zero);
} }
return SliverToBoxAdapter( return SliverLazyToBoxAdapter(
child: Column( child: Column(
children: [ children: [
ListTile( ListTile(
@@ -482,7 +482,7 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
bool enableTranslation = bool enableTranslation =
App.locale.languageCode == 'zh' && comicSource.enableTagsTranslate; App.locale.languageCode == 'zh' && comicSource.enableTagsTranslate;
return SliverToBoxAdapter( return SliverLazyToBoxAdapter(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -550,7 +550,7 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
if (comic.chapters == null) { if (comic.chapters == null) {
return const SliverPadding(padding: EdgeInsets.zero); return const SliverPadding(padding: EdgeInsets.zero);
} }
return const _ComicChapters(); return _ComicChapters(history);
} }
Widget buildThumbnails() { Widget buildThumbnails() {
@@ -592,7 +592,7 @@ abstract mixin class _ComicPageActions {
ComicSource get comicSource => ComicSource.find(comic.sourceKey)!; ComicSource get comicSource => ComicSource.find(comic.sourceKey)!;
History? history; History? get history;
bool isLiking = false; bool isLiking = false;
@@ -612,8 +612,10 @@ abstract mixin class _ComicPageActions {
update(); update();
} }
/// whether the comic is added to local favorite
bool isAddToLocalFav = false; bool isAddToLocalFav = false;
/// whether the comic is favorite on the server
bool isFavorite = false; bool isFavorite = false;
FavoriteItem _toFavoriteItem() { FavoriteItem _toFavoriteItem() {
@@ -684,11 +686,13 @@ abstract mixin class _ComicPageActions {
chapters: comic.chapters, chapters: comic.chapters,
initialChapter: ep, initialChapter: ep,
initialPage: page, initialPage: page,
history: History.fromModel(model: comic, ep: 0, page: 0), history: history ?? History.fromModel(model: comic, ep: 0, page: 0),
author: comic.findAuthor() ?? '', author: comic.findAuthor() ?? '',
tags: comic.plainTags, tags: comic.plainTags,
), ),
); ).then((_) {
onReadEnd();
});
} }
void continueRead() { void continueRead() {
@@ -697,6 +701,8 @@ abstract mixin class _ComicPageActions {
read(ep, page); read(ep, page);
} }
void onReadEnd();
void download() async { void download() async {
if (LocalManager().isDownloading(comic.id, comic.comicType)) { if (LocalManager().isDownloading(comic.id, comic.comicType)) {
App.rootContext.showMessage(message: "The comic is downloading".tl); App.rootContext.showMessage(message: "The comic is downloading".tl);
@@ -1079,7 +1085,9 @@ class _ActionButton extends StatelessWidget {
} }
class _ComicChapters extends StatefulWidget { class _ComicChapters extends StatefulWidget {
const _ComicChapters(); const _ComicChapters(this.history);
final History? history;
@override @override
State<_ComicChapters> createState() => _ComicChaptersState(); State<_ComicChapters> createState() => _ComicChaptersState();
@@ -1092,104 +1100,133 @@ class _ComicChaptersState extends State<_ComicChapters> {
bool showAll = false; bool showAll = false;
late History? history;
@override
void initState() {
super.initState();
history = widget.history;
}
@override @override
void didChangeDependencies() { void didChangeDependencies() {
state = context.findAncestorStateOfType<_ComicPageState>()!; state = context.findAncestorStateOfType<_ComicPageState>()!;
super.didChangeDependencies(); super.didChangeDependencies();
} }
@override
void didUpdateWidget(covariant _ComicChapters oldWidget) {
super.didUpdateWidget(oldWidget);
setState(() {
history = widget.history;
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final eps = state.comic.chapters!; 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) { return SliverMainAxisGroup(
length = math.min(length, 20); slivers: [
} SliverToBoxAdapter(
child: ListTile(
return SliverMainAxisGroup( title: Text("Chapters".tl),
slivers: [ trailing: Tooltip(
SliverToBoxAdapter( message: "Order".tl,
child: ListTile( child: IconButton(
title: Text("Chapters".tl), icon: Icon(reverse
trailing: Tooltip( ? Icons.vertical_align_top
message: "Order".tl, : Icons.vertical_align_bottom_outlined),
child: IconButton( onPressed: () {
icon: Icon(reverse setState(() {
? Icons.vertical_align_top reverse = !reverse;
: 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,
),
),
),
), ),
), ),
), ),
);
}),
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),
), ),
), SliverGrid(
const SliverToBoxAdapter( delegate: SliverChildBuilderDelegate(
child: Divider(), 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(); State<_FavoritePanel> createState() => _FavoritePanelState();
} }
class _FavoritePanelState extends State<_FavoritePanel> { class _FavoritePanelState extends State<_FavoritePanel>
with SingleTickerProviderStateMixin {
late ComicSource comicSource; late ComicSource comicSource;
late TabController tabController;
late bool hasNetwork;
@override @override
void initState() { void initState() {
comicSource = widget.type.comicSource!; comicSource = widget.type.comicSource!;
localFolders = LocalFavoritesManager().folderNames; localFolders = LocalFavoritesManager().folderNames;
added = LocalFavoritesManager().find(widget.cid, widget.type); 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(); super.initState();
} }
@override
void dispose() {
var currentIndex = tabController.index;
appdata.implicitData['favoritePanelIndex'] = currentIndex;
appdata.writeImplicitData();
tabController.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var hasNetwork = comicSource.favoriteData != null && comicSource.isLogged;
return Scaffold( return Scaffold(
appBar: Appbar( appBar: Appbar(
title: Text("Favorite".tl), title: Text("Favorite".tl),
), ),
body: DefaultTabController( body: Column(
length: hasNetwork ? 2 : 1, children: [
child: Column( TabBar(
children: [ controller: tabController,
TabBar(tabs: [ tabs: [
Tab(text: "Local".tl), Tab(text: "Local".tl),
if (hasNetwork) Tab(text: "Network".tl), if (hasNetwork) Tab(text: "Network".tl),
]), ],
Expanded( ),
child: TabBarView( Expanded(
children: [ child: TabBarView(
buildLocal(), controller: tabController,
if (hasNetwork) buildNetwork(), children: [
], buildLocal(),
), if (hasNetwork) buildNetwork(),
],
), ),
], ),
), ],
), ),
); );
} }
@@ -1645,6 +1707,42 @@ class _NetworkFavoritesState extends State<_NetworkFavorites> {
} }
Widget buildMultiFolder() { 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) { if (isLoadingFolders) {
loadFolders(); loadFolders();
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
@@ -1850,7 +1948,7 @@ class _CommentsPartState extends State<_CommentsPart> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MultiSliver( return MultiSliver(
children: [ children: [
SliverToBoxAdapter( SliverLazyToBoxAdapter(
child: ListTile( child: ListTile(
title: Text("Comments".tl), title: Text("Comments".tl),
trailing: Row( trailing: Row(

View File

@@ -1011,7 +1011,7 @@ class _LoginPageState extends State<_LoginPage> {
if (widget.config.loginWebsite != null) if (widget.config.loginWebsite != null)
TextButton( TextButton(
onPressed: () { onPressed: () {
if (App.isWindows || App.isLinux) { if (App.isLinux) {
loginWithWebview2(); loginWithWebview2();
} else { } else {
loginWithWebview(); loginWithWebview();
@@ -1127,7 +1127,7 @@ class _LoginPageState extends State<_LoginPage> {
} }
} }
// for windows and linux // for linux
void loginWithWebview2() async { void loginWithWebview2() async {
if (!await DesktopWebview.isAvailable()) { if (!await DesktopWebview.isAvailable()) {
context.showMessage(message: "Webview is not available".tl); context.showMessage(message: "Webview is not available".tl);

View File

@@ -42,7 +42,7 @@ class _CommentsPageState extends State<CommentsPage> {
_error = res.errorMessage; _error = res.errorMessage;
_loading = false; _loading = false;
}); });
} else { } else if (mounted) {
setState(() { setState(() {
_comments = res.data; _comments = res.data;
_loading = false; _loading = false;

View File

@@ -37,7 +37,7 @@ class _ExplorePageState extends State<ExplorePage>
.expand((e) => e.map((e) => e.title)) .expand((e) => e.map((e) => e.title))
.toList(); .toList();
explorePages = explorePages.where((e) => all.contains(e)).toList(); explorePages = explorePages.where((e) => all.contains(e)).toList();
if (!pages.isEqualsTo(explorePages)) { if (!pages.isEqualTo(explorePages)) {
setState(() { setState(() {
pages = explorePages; pages = explorePages;
controller = TabController( controller = TabController(

View File

@@ -16,6 +16,7 @@ import 'package:venera/foundation/res.dart';
import 'package:venera/network/download.dart'; import 'package:venera/network/download.dart';
import 'package:venera/pages/comic_page.dart'; import 'package:venera/pages/comic_page.dart';
import 'package:venera/pages/reader/reader.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/io.dart';
import 'package:venera/utils/translations.dart'; import 'package:venera/utils/translations.dart';

View File

@@ -476,55 +476,47 @@ class _CreateFolderDialogState extends State<_CreateFolderDialog> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SimpleDialog( return ContentDialog(
title: Text("Create a folder".tl), title: "Create a folder".tl,
children: [ content: Column(
Padding( children: [
padding: const EdgeInsets.fromLTRB(20, 0, 20, 0), Padding(
child: TextField( padding: const EdgeInsets.fromLTRB(16, 0, 16, 0),
controller: controller, child: TextField(
decoration: InputDecoration( controller: controller,
border: const OutlineInputBorder(), decoration: InputDecoration(
labelText: "name".tl, 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),
), ),
), ),
) ),
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),
)
], ],
); );
} }

View File

@@ -20,22 +20,35 @@ class _LeftBarState extends State<_LeftBar> implements FolderList {
var networkFolders = <String>[]; 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 @override
void initState() { void initState() {
favPage = widget.favPage ?? favPage = widget.favPage ??
context.findAncestorStateOfType<_FavoritesPageState>()!; context.findAncestorStateOfType<_FavoritesPageState>()!;
favPage.folderList = this; favPage.folderList = this;
folders = LocalFavoritesManager().folderNames; folders = LocalFavoritesManager().folderNames;
networkFolders = ComicSource.all() findNetworkFolders();
.where((e) => e.favoriteData != null && e.isLogged) appdata.settings.addListener(updateFolders);
.map((e) => e.favoriteData!.key)
.toList();
super.initState(); super.initState();
} }
@override @override
void dispose() { void dispose() {
super.dispose(); super.dispose();
appdata.settings.removeListener(updateFolders);
} }
@override @override
@@ -102,7 +115,8 @@ class _LeftBarState extends State<_LeftBar> implements FolderList {
onClick: () { onClick: () {
newFolder().then((value) { newFolder().then((value) {
setState(() { setState(() {
folders = LocalFavoritesManager().folderNames; folders =
LocalFavoritesManager().folderNames;
}); });
}); });
}, },
@@ -113,7 +127,8 @@ class _LeftBarState extends State<_LeftBar> implements FolderList {
onClick: () { onClick: () {
sortFolders().then((value) { sortFolders().then((value) {
setState(() { setState(() {
folders = LocalFavoritesManager().folderNames; folders =
LocalFavoritesManager().folderNames;
}); });
}); });
}, },
@@ -143,15 +158,24 @@ class _LeftBarState extends State<_LeftBar> implements FolderList {
), ),
child: Row( child: Row(
children: [ children: [
const SizedBox(width: 16),
Icon( Icon(
Icons.cloud, Icons.cloud,
color: context.colorScheme.secondary, color: context.colorScheme.secondary,
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
Text("Network".tl), Text("Network".tl),
const Spacer(),
IconButton(
icon: const Icon(Icons.settings),
onPressed: () {
showPopUpWidget(
App.rootContext,
setFavoritesPagesWidget(),
);
},
),
], ],
), ).paddingHorizontal(16),
); );
} }
index--; index--;
@@ -241,10 +265,7 @@ class _LeftBarState extends State<_LeftBar> implements FolderList {
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
folders = LocalFavoritesManager().folderNames; folders = LocalFavoritesManager().folderNames;
networkFolders = ComicSource.all() findNetworkFolders();
.where((e) => e.favoriteData != null)
.map((e) => e.favoriteData!.key)
.toList();
}); });
} }
} }

View File

@@ -12,6 +12,7 @@ import 'package:venera/utils/epub.dart';
import 'package:venera/utils/io.dart'; import 'package:venera/utils/io.dart';
import 'package:venera/utils/pdf.dart'; import 'package:venera/utils/pdf.dart';
import 'package:venera/utils/translations.dart'; import 'package:venera/utils/translations.dart';
import 'package:zip_flutter/zip_flutter.dart';
class LocalComicsPage extends StatefulWidget { class LocalComicsPage extends StatefulWidget {
const LocalComicsPage({super.key}); const LocalComicsPage({super.key});
@@ -147,13 +148,13 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
text: "View Detail".tl, text: "View Detail".tl,
onClick: () { onClick: () {
context.to(() => ComicPage( context.to(() => ComicPage(
id: selectedComics.keys.first.id, id: selectedComics.keys.first.id,
sourceKey: selectedComics.keys.first.sourceKey, sourceKey: selectedComics.keys.first.sourceKey,
)); ));
}, },
), ),
if (selectedComics.length == 1) if (selectedComics.isNotEmpty)
...exportActions(selectedComics.keys.first), ...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; return isDeleted;
} }
List<MenuEntry> exportActions(LocalComic c) { List<MenuEntry> exportActions(List<LocalComic> comics) {
return [ return [
MenuEntry( MenuEntry(
icon: Icons.outbox_outlined, icon: Icons.outbox_outlined,
text: "Export as cbz".tl, text: "Export as cbz".tl,
onClick: () async { onClick: () {
var controller = showLoadingDialog( exportComics(comics, CBZ.export, ".cbz");
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();
}),
MenuEntry( MenuEntry(
icon: Icons.picture_as_pdf_outlined, icon: Icons.picture_as_pdf_outlined,
text: "Export as pdf".tl, text: "Export as pdf".tl,
onClick: () async { onClick: () async {
var cache = FilePath.join(App.cachePath, 'temp.pdf'); exportComics(comics, createPdfFromComicIsolate, ".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();
}
}, },
), ),
MenuEntry( MenuEntry(
icon: Icons.import_contacts_outlined, icon: Icons.import_contacts_outlined,
text: "Export as epub".tl, text: "Export as epub".tl,
onClick: () async { onClick: () async {
var controller = showLoadingDialog( exportComics(comics, createEpubWithLocalComic, ".epub");
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();
}
}, },
) )
]; ];
} }
/// 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);

View File

@@ -24,6 +24,8 @@ class _ReaderGestureDetectorState extends State<_ReaderGestureDetector> {
int fingers = 0; int fingers = 0;
late _ReaderState reader;
@override @override
void initState() { void initState() {
_tapGestureRecognizer = TapGestureRecognizer() _tapGestureRecognizer = TapGestureRecognizer()
@@ -33,6 +35,7 @@ class _ReaderGestureDetectorState extends State<_ReaderGestureDetector> {
}; };
super.initState(); super.initState();
context.readerScaffold._gestureDetectorState = this; context.readerScaffold._gestureDetectorState = this;
reader = context.reader;
} }
@override @override
@@ -166,7 +169,9 @@ class _ReaderGestureDetectorState extends State<_ReaderGestureDetector> {
} }
void onTap(Offset location) { void onTap(Offset location) {
if (context.readerScaffold.isOpen) { if (reader._imageViewController!.handleOnTap(location)) {
return;
} else if (context.readerScaffold.isOpen) {
context.readerScaffold.openOrClose(); context.readerScaffold.openOrClose();
} else { } else {
if (appdata.settings['enableTapToTurnPages']) { if (appdata.settings['enableTapToTurnPages']) {
@@ -186,31 +191,37 @@ class _ReaderGestureDetectorState extends State<_ReaderGestureDetector> {
isBottom = true; isBottom = true;
} }
bool isCenter = false; 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) { switch (context.reader.mode) {
case ReaderMode.galleryLeftToRight: case ReaderMode.galleryLeftToRight:
case ReaderMode.continuousLeftToRight: case ReaderMode.continuousLeftToRight:
if (isLeft) { if (isLeft) {
context.reader.toPrevPage(); prev();
} else if (isRight) { } else if (isRight) {
context.reader.toNextPage(); next();
} else { } else {
isCenter = true; isCenter = true;
} }
case ReaderMode.galleryRightToLeft: case ReaderMode.galleryRightToLeft:
case ReaderMode.continuousRightToLeft: case ReaderMode.continuousRightToLeft:
if (isLeft) { if (isLeft) {
context.reader.toNextPage(); next();
} else if (isRight) { } else if (isRight) {
context.reader.toPrevPage(); prev();
} else { } else {
isCenter = true; isCenter = true;
} }
case ReaderMode.galleryTopToBottom: case ReaderMode.galleryTopToBottom:
case ReaderMode.continuousTopToBottom: case ReaderMode.continuousTopToBottom:
if (isTop) { if (isTop) {
context.reader.toPrevPage(); prev();
} else if (isBottom) { } else if (isBottom) {
context.reader.toNextPage(); next();
} else { } else {
isCenter = true; isCenter = true;
} }

View File

@@ -335,6 +335,11 @@ class _GalleryModeState extends State<_GalleryMode>
} }
} }
} }
@override
bool handleOnTap(Offset location) {
return false;
}
} }
const Set<PointerDeviceKind> _kTouchLikeDeviceTypes = <PointerDeviceKind>{ const Set<PointerDeviceKind> _kTouchLikeDeviceTypes = <PointerDeviceKind>{
@@ -366,6 +371,18 @@ class _ContinuousModeState extends State<_ContinuousMode>
var fingers = 0; var fingers = 0;
bool disableScroll = false; 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 @override
void initState() { void initState() {
reader = context.reader; reader = context.reader;
@@ -374,6 +391,12 @@ class _ContinuousModeState extends State<_ContinuousMode>
super.initState(); super.initState();
} }
@override
void dispose() {
itemPositionsListener.itemPositions.removeListener(onPositionChanged);
super.dispose();
}
void onPositionChanged() { void onPositionChanged() {
var page = itemPositionsListener.itemPositions.value.first.index; var page = itemPositionsListener.itemPositions.value.first.index;
page = page.clamp(1, reader.maxPage); 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) { onPointerPanZoomUpdate: (event) {
if (event.scale == 1.0) { if (event.scale == 1.0) {
smoothTo(0 - event.panDelta.dy); smoothTo(0 - event.panDelta.dy);
@@ -516,8 +547,14 @@ class _ContinuousModeState extends State<_ContinuousMode>
child: widget, child: widget,
); );
widget = NotificationListener<ScrollUpdateNotification>( widget = NotificationListener<ScrollNotification>(
onNotification: (notification) { onNotification: (notification) {
if (notification is ScrollStartNotification) {
delayedSetIsScrolling(true);
} else if (notification is ScrollEndNotification) {
delayedSetIsScrolling(false);
}
var length = reader.maxChapter; var length = reader.maxChapter;
if (!scrollController.hasClients) return false; if (!scrollController.hasClients) return false;
if (scrollController.position.pixels <= if (scrollController.position.pixels <=
@@ -592,7 +629,7 @@ class _ContinuousModeState extends State<_ContinuousMode>
@override @override
void handleLongPressDown(Offset location) { void handleLongPressDown(Offset location) {
if (!appdata.settings['enableLongPressToZoom']) { if (!appdata.settings['enableLongPressToZoom'] || delayedIsScrolling) {
return; return;
} }
double target = photoViewController.getInitialScale!.call()! * 1.75; 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( ImageProvider _createImageProviderFromKey(

View File

@@ -237,6 +237,7 @@ class _ReaderState extends State<Reader> with _ReaderLocation, _ReaderWindow {
history!.maxPage = maxPage; history!.maxPage = maxPage;
} }
history!.readEpisode.add(chapter); history!.readEpisode.add(chapter);
print(history!.readEpisode);
history!.time = DateTime.now(); history!.time = DateTime.now();
HistoryManager().addHistory(history!); HistoryManager().addHistory(history!);
} }
@@ -430,4 +431,7 @@ abstract interface class _ImageViewController {
void handleLongPressUp(Offset location); void handleLongPressUp(Offset location);
void handleKeyEvent(KeyEvent event); void handleKeyEvent(KeyEvent event);
/// Returns true if the event is handled.
bool handleOnTap(Offset location);
} }

View File

@@ -660,12 +660,16 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
App.rootContext.pop(); App.rootContext.pop();
}, },
child: Container( child: Container(
decoration: BoxDecoration( foregroundDecoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(16)), borderRadius: BorderRadius.circular(16),
border: Border.all( border: Border.all(
color: Theme.of(context).colorScheme.outline, color: Theme.of(context).colorScheme.outline,
), ),
), ),
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
),
width: double.infinity, width: double.infinity,
height: double.infinity, height: double.infinity,
child: Image( child: Image(

View File

@@ -10,12 +10,14 @@ import 'package:venera/foundation/comic_source/comic_source.dart';
import 'package:venera/foundation/state_controller.dart'; import 'package:venera/foundation/state_controller.dart';
import 'package:venera/pages/aggregated_search_page.dart'; import 'package:venera/pages/aggregated_search_page.dart';
import 'package:venera/pages/search_result_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/app_links.dart';
import 'package:venera/utils/ext.dart'; import 'package:venera/utils/ext.dart';
import 'package:venera/utils/tags_translation.dart'; import 'package:venera/utils/tags_translation.dart';
import 'package:venera/utils/translations.dart'; import 'package:venera/utils/translations.dart';
import 'comic_page.dart'; import 'comic_page.dart';
import 'comic_source_page.dart';
class SearchPage extends StatefulWidget { class SearchPage extends StatefulWidget {
const SearchPage({super.key}); const SearchPage({super.key});
@@ -27,8 +29,13 @@ class SearchPage extends StatefulWidget {
class _SearchPageState extends State<SearchPage> { class _SearchPageState extends State<SearchPage> {
late final SearchBarController controller; late final SearchBarController controller;
late List<String> searchSources;
String searchTarget = ""; String searchTarget = "";
SearchPageData get currentSearchPageData =>
ComicSource.find(searchTarget)!.searchPageData!;
bool aggregatedSearch = false; bool aggregatedSearch = false;
var focusNode = FocusNode(); var focusNode = FocusNode();
@@ -139,29 +146,85 @@ class _SearchPageState extends State<SearchPage> {
@override @override
void initState() { void initState() {
findSearchSources();
var defaultSearchTarget = appdata.settings['defaultSearchTarget']; var defaultSearchTarget = appdata.settings['defaultSearchTarget'];
if (defaultSearchTarget == "_aggregated_") { if (defaultSearchTarget == "_aggregated_") {
aggregatedSearch = true; aggregatedSearch = true;
} else if (defaultSearchTarget != null && } else if (defaultSearchTarget != null &&
ComicSource.find(defaultSearchTarget) != null) { searchSources.contains(defaultSearchTarget)) {
searchTarget = defaultSearchTarget; searchTarget = defaultSearchTarget;
} else {
searchTarget = ComicSource.all().first.key;
} }
controller = SearchBarController( controller = SearchBarController(
onSearch: search, onSearch: search,
); );
appdata.settings.addListener(updateSearchSourcesIfNeeded);
super.initState(); super.initState();
} }
@override @override
void dispose() { void dispose() {
focusNode.dispose(); focusNode.dispose();
appdata.settings.removeListener(updateSearchSourcesIfNeeded);
super.dispose(); 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (searchSources.isEmpty) {
return buildEmpty();
}
return Scaffold( return Scaffold(
body: SmoothCustomScrollView( body: SmoothCustomScrollView(
slivers: buildSlivers().toList(), slivers: buildSlivers().toList(),
@@ -190,8 +253,7 @@ class _SearchPageState extends State<SearchPage> {
} }
Widget buildSearchTarget() { Widget buildSearchTarget() {
var sources = var sources = searchSources.map((e) => ComicSource.find(e)!).toList();
ComicSource.all().where((e) => e.searchPageData != null).toList();
return SliverToBoxAdapter( return SliverToBoxAdapter(
child: Container( child: Container(
width: double.infinity, width: double.infinity,
@@ -203,6 +265,10 @@ class _SearchPageState extends State<SearchPage> {
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.search), leading: const Icon(Icons.search),
title: Text("Search in".tl), title: Text("Search in".tl),
trailing: IconButton(
icon: const Icon(Icons.settings),
onPressed: manageSearchSources,
),
), ),
Wrap( Wrap(
spacing: 8, spacing: 8,
@@ -229,11 +295,6 @@ class _SearchPageState extends State<SearchPage> {
onChanged: (value) { onChanged: (value) {
setState(() { setState(() {
aggregatedSearch = value ?? false; aggregatedSearch = value ?? false;
if (!aggregatedSearch &&
appdata.settings['defaultSearchTarget'] ==
"_aggregated_") {
searchTarget = sources.first.key;
}
}); });
}, },
), ),
@@ -245,9 +306,7 @@ class _SearchPageState extends State<SearchPage> {
} }
void useDefaultOptions() { void useDefaultOptions() {
final searchOptions = final searchOptions = currentSearchPageData.searchOptions ?? [];
ComicSource.find(searchTarget)!.searchPageData!.searchOptions ??
<SearchOptions>[];
options = searchOptions.map((e) => e.defaultValue).toList(); options = searchOptions.map((e) => e.defaultValue).toList();
} }
@@ -258,9 +317,7 @@ class _SearchPageState extends State<SearchPage> {
var children = <Widget>[]; var children = <Widget>[];
final searchOptions = final searchOptions = currentSearchPageData.searchOptions ?? [];
ComicSource.find(searchTarget)!.searchPageData!.searchOptions ??
<SearchOptions>[];
if (searchOptions.length != options.length) { if (searchOptions.length != options.length) {
useDefaultOptions(); useDefaultOptions();
} }
@@ -394,7 +451,9 @@ class _SearchPageState extends State<SearchPage> {
Text( Text(
subTitle, subTitle,
style: TextStyle( style: TextStyle(
fontSize: 14, color: Theme.of(context).colorScheme.outline), fontSize: 14,
color: Theme.of(context).colorScheme.outline,
),
) )
], ],
), ),

View File

@@ -116,13 +116,13 @@ class _SearchResultPageState extends State<SearchResultPage> {
@override @override
void initState() { void initState() {
sourceKey = widget.sourceKey; sourceKey = widget.sourceKey;
text = checkAutoLanguage(widget.text);
controller = SearchBarController( controller = SearchBarController(
currentText: checkAutoLanguage(widget.text), currentText: text,
onSearch: search, onSearch: search,
); );
options = widget.options ?? const []; options = widget.options ?? const [];
validateOptions(); validateOptions();
text = widget.text;
appdata.addSearchHistory(text); appdata.addSearchHistory(text);
suggestionsController = _SuggestionsController(controller); suggestionsController = _SuggestionsController(controller);
super.initState(); super.initState();
@@ -187,7 +187,7 @@ class _SearchResultPageState extends State<SearchResultPage> {
suggestionsController.remove(); suggestionsController.remove();
} }
var previousOptions = options; var previousOptions = List<String>.from(options);
var previousSourceKey = sourceKey; var previousSourceKey = sourceKey;
await showDialog( await showDialog(
context: context, context: context,
@@ -196,7 +196,8 @@ class _SearchResultPageState extends State<SearchResultPage> {
return _SearchSettingsDialog(state: this); return _SearchSettingsDialog(state: this);
}, },
); );
if (previousOptions != options || previousSourceKey != sourceKey) { if (!previousOptions.isEqualTo(options) ||
previousSourceKey != sourceKey) {
text = checkAutoLanguage(controller.text); text = checkAutoLanguage(controller.text);
controller.currentText = text; controller.currentText = text;
setState(() {}); setState(() {});

View File

@@ -86,7 +86,7 @@ class _AboutSettingsState extends State<AboutSettings> {
Future<bool> checkUpdate() async { Future<bool> checkUpdate() async {
var res = await AppDio().get( 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) { if (res.statusCode == 200) {
var data = loadYaml(res.data); var data = loadYaml(res.data);
if (data["version"] != null) { if (data["version"] != null) {

View File

@@ -38,19 +38,11 @@ class _ExploreSettingsState extends State<ExploreSettings> {
).toSliver(), ).toSliver(),
_PopupWindowSetting( _PopupWindowSetting(
title: "Network Favorite Pages".tl, title: "Network Favorite Pages".tl,
builder: () { builder: setFavoritesPagesWidget,
var pages = <String, String>{}; ).toSliver(),
for (var c in ComicSource.all()) { _PopupWindowSetting(
if (c.favoriteData != null) { title: "Search Sources".tl,
pages[c.favoriteData!.key] = c.favoriteData!.title; builder: setSearchSourcesWidget,
}
}
return _MultiPagesFilter(
title: "Network Favorite Pages".tl,
settingsIndex: "favorites",
pages: pages,
);
},
).toSliver(), ).toSliver(),
_SwitchSetting( _SwitchSetting(
title: "Show favorite status on comic tile".tl, title: "Show favorite status on comic tile".tl,
@@ -209,3 +201,31 @@ Widget setCategoryPagesWidget() {
pages: pages, 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,
);
}

View File

@@ -22,6 +22,13 @@ class _ReaderSettingsState extends State<ReaderSettings> {
widget.onChanged?.call("enableTapToTurnPages"); widget.onChanged?.call("enableTapToTurnPages");
}, },
).toSliver(), ).toSliver(),
_SwitchSetting(
title: "Reverse tap to turn Pages".tl,
settingKey: "reverseTapToTurnPages",
onChanged: () {
widget.onChanged?.call("reverseTapToTurnPages");
},
).toSliver(),
_SwitchSetting( _SwitchSetting(
title: "Page animation".tl, title: "Page animation".tl,
settingKey: "enablePageAnimation", settingKey: "enablePageAnimation",

View File

@@ -25,8 +25,13 @@ extension WebviewExtension on InAppWebViewController {
if (url[url.length - 1] == '/') { if (url[url.length - 1] == '/') {
url = url.substring(0, url.length - 1); url = url.substring(0, url.length - 1);
} }
CookieManager cookieManager = CookieManager.instance(); CookieManager cookieManager = CookieManager.instance(
final cookies = await cookieManager.getCookies(url: WebUri(url)); webViewEnvironment: AppWebview.webViewEnvironment,
);
final cookies = await cookieManager.getCookies(
url: WebUri(url),
webViewController: this,
);
var res = <io.Cookie>[]; var res = <io.Cookie>[];
for (var cookie in cookies) { for (var cookie in cookies) {
var c = io.Cookie(cookie.name, cookie.value); var c = io.Cookie(cookie.name, cookie.value);
@@ -86,11 +91,12 @@ class _AppWebviewState extends State<AppWebview> {
late var future = _createWebviewEnvironment(); late var future = _createWebviewEnvironment();
Future<WebViewEnvironment> _createWebviewEnvironment() async { Future<bool> _createWebviewEnvironment() async {
var proxy = appdata.settings['proxy'].toString(); var proxy = appdata.settings['proxy'].toString();
if (proxy != "system" && proxy != "direct") { if (proxy != "system" && proxy != "direct") {
var proxyAvailable = await WebViewFeature.isFeatureSupported( var proxyAvailable = await WebViewFeature.isFeatureSupported(
WebViewFeature.PROXY_OVERRIDE); WebViewFeature.PROXY_OVERRIDE,
);
if (proxyAvailable) { if (proxyAvailable) {
ProxyController proxyController = ProxyController.instance(); ProxyController proxyController = ProxyController.instance();
await proxyController.clearProxyOverride(); 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( settings: WebViewEnvironmentSettings(
userDataFolder: "${App.dataPath}\\webview", userDataFolder: "${App.dataPath}\\webview",
), ),
); );
return true;
} }
@override @override
@@ -147,22 +157,20 @@ class _AppWebviewState extends State<AppWebview> {
) )
]; ];
Widget body = (App.isWindows && AppWebview.webViewEnvironment == null) Widget body = FutureBuilder(
? FutureBuilder( future: future,
future: future, builder: (context, e) {
builder: (context, e) { if (e.error != null) {
if (e.error != null) { return Center(child: Text("Error: ${e.error}"));
return Center(child: Text("Error: ${e.error}")); }
} if (!e.hasData) {
if (e.data == null) { return const SizedBox();
return const Center(child: CircularProgressIndicator()); }
} return createWebviewWithEnvironment(
AppWebview.webViewEnvironment = e.data; AppWebview.webViewEnvironment,
return createWebviewWithEnvironment( );
AppWebview.webViewEnvironment); },
}, );
)
: createWebviewWithEnvironment(AppWebview.webViewEnvironment);
body = Stack( body = Stack(
children: [ children: [

View File

@@ -85,6 +85,10 @@ abstract class CBZ {
if (cache.existsSync()) cache.deleteSync(recursive: true); if (cache.existsSync()) cache.deleteSync(recursive: true);
cache.createSync(); cache.createSync();
await extractArchive(file, cache); 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')); var metaDataFile = File(FilePath.join(cache.path, 'metadata.json'));
ComicMetaData? metaData; ComicMetaData? metaData;
if (metaDataFile.existsSync()) { if (metaDataFile.existsSync()) {
@@ -111,7 +115,17 @@ abstract class CBZ {
cache.deleteSync(recursive: true); cache.deleteSync(recursive: true);
throw Exception('No images found in the archive'); 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( var coverFile = files.firstWhereOrNull(
(element) => (element) =>
element.path.endsWith('cover.${element.path.split('.').last}'), element.path.endsWith('cover.${element.path.split('.').last}'),
@@ -171,7 +185,7 @@ abstract class CBZ {
return comic; 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')); var cache = Directory(FilePath.join(App.cachePath, 'cbz_export'));
if (cache.existsSync()) cache.deleteSync(recursive: true); if (cache.existsSync()) cache.deleteSync(recursive: true);
cache.createSync(); cache.createSync();
@@ -230,7 +244,7 @@ abstract class CBZ {
).toJson(), ).toJson(),
), ),
); );
var cbz = File(FilePath.join(App.cachePath, sanitizeFileName('${comic.title}.cbz'))); var cbz = File(outFilePath);
if (cbz.existsSync()) cbz.deleteSync(); if (cbz.existsSync()) cbz.deleteSync();
await _compress(cache.path, cbz.path); await _compress(cache.path, cbz.path);
cache.deleteSync(recursive: true); cache.deleteSync(recursive: true);

View File

@@ -118,6 +118,7 @@ class DataSync with ChangeNotifier {
await client.remove(files.first.name!); await client.remove(files.first.name!);
} }
await client.write(filename, await data.readAsBytes()); await client.write(filename, await data.readAsBytes());
data.deleteIgnoreError();
Log.info("Upload Data", "Data uploaded successfully"); Log.info("Upload Data", "Data uploaded successfully");
return const Res(true); return const Res(true);
} catch (e, s) { } catch (e, s) {

View File

@@ -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')); final workingDir = Directory(FilePath.join(cacheDir, 'epub'));
if (workingDir.existsSync()) { if (workingDir.existsSync()) {
workingDir.deleteSync(recursive: true); workingDir.deleteSync(recursive: true);
@@ -109,8 +110,7 @@ ${images.map((e) => ' <img src="$e" alt="$e"/>').join('\n')}
} }
// content.opf // content.opf
final contentOpf = final contentOpf = File(FilePath.join(workingDir.path, 'content.opf'));
File(FilePath.join(workingDir.path, 'content.opf'));
final uuid = const Uuid().v4(); final uuid = const Uuid().v4();
var spineStrBuilder = StringBuffer(); var spineStrBuilder = StringBuffer();
for (var i = 0; i < chapterIndex; i++) { for (var i = 0; i < chapterIndex; i++) {
@@ -171,16 +171,15 @@ ${navMapStrBuilder.toString()}
</ncx> </ncx>
'''); ''');
// zip ZipFile.compressFolder(workingDir.path, outFilePath);
final zipPath = FilePath.join(cacheDir, '${data.title}.epub');
ZipFile.compressFolder(workingDir.path, zipPath);
workingDir.deleteSync(recursive: true); 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>>{}; var chapters = <String, List<File>>{};
if (comic.chapters == null) { if (comic.chapters == null) {
chapters[comic.title] = chapters[comic.title] =
@@ -188,11 +187,11 @@ Future<File> createEpubWithLocalComic(LocalComic comic) async {
.map((e) => File(e)) .map((e) => File(e))
.toList(); .toList();
} else { } else {
for (var chapter in comic.chapters!.keys) { for (var chapter in comic.downloadedChapters) {
chapters[comic.chapters![chapter]!] = (await LocalManager() chapters[comic.chapters![chapter]!] =
.getImages(comic.id, comic.comicType, chapter)) (await LocalManager().getImages(comic.id, comic.comicType, chapter))
.map((e) => File(e)) .map((e) => File(e))
.toList(); .toList();
} }
} }
var data = EpubData( var data = EpubData(
@@ -205,6 +204,6 @@ Future<File> createEpubWithLocalComic(LocalComic comic) async {
final cacheDir = App.cachePath; final cacheDir = App.cachePath;
return Isolate.run(() => overrideIO(() async { return Isolate.run(() => overrideIO(() async {
return createEpubComic(data, cacheDir); return createEpubComic(data, cacheDir, outFilePath);
})); }));
} }

View File

@@ -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){ if(length != list.length){
return false; return false;
} }
@@ -81,10 +83,6 @@ extension StringExt on String{
return '$before$to$after'; return '$before$to$after';
} }
static bool hasMatch(String? value, String pattern) {
return (value == null) ? false : RegExp(pattern).hasMatch(value);
}
bool _isURL(){ bool _isURL(){
final regex = RegExp( final regex = RegExp(
r'^((http|https|ftp)://)[\w-]+(\.[\w-]+)+([\w.,@?^=%&:/~+#-|]*[\w@?^=%&/~+#-])?$', r'^((http|https|ftp)://)[\w-]+(\.[\w-]+)+([\w.,@?^=%&:/~+#-|]*[\w@?^=%&/~+#-])?$',

View File

@@ -35,19 +35,9 @@ class FilePath {
} }
extension FileSystemEntityExt on FileSystemEntity { extension FileSystemEntityExt on FileSystemEntity {
/// Get the base name of the file or directory.
String get name { String get name {
var path = this.path; return p.basename(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);
} }
Future<void> deleteIgnoreError({bool recursive = false}) async { 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]. // Stream is not usable since [AndroidFile] does not support [openRead].
await newFile.writeAsBytes(await readAsBytes()); await newFile.writeAsBytes(await readAsBytes());
} }
String get basenameWithoutExt {
return p.basenameWithoutExtension(path);
}
} }
extension DirectoryExtension on Directory { extension DirectoryExtension on Directory {

View File

@@ -30,14 +30,14 @@ Future<void> _createPdfFromComic({
files.removeWhere( files.removeWhere(
(element) => element is! File || element.path.startsWith('cover')); (element) => element is! File || element.path.startsWith('cover'));
files.sort((a, b) { files.sort((a, b) {
var aName = (a as File).name; var aName = (a as File).basenameWithoutExt;
var bName = (b as File).name; var bName = (b as File).basenameWithoutExt;
var aNumber = int.tryParse(aName); var aNumber = int.tryParse(aName);
var bNumber = int.tryParse(bName); var bNumber = int.tryParse(bName);
if (aNumber != null && bNumber != null) { if (aNumber != null && bNumber != null) {
return aNumber.compareTo(bNumber); 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); images.add(file.path);
} }
} else { } else {
for (var chapter in comic.chapters!.keys) { for (var chapter in comic.downloadedChapters) {
var files = Directory(FilePath.join(baseDir, chapter)).listSync(); var files = Directory(FilePath.join(baseDir, chapter)).listSync();
reorderFiles(files); reorderFiles(files);
for (var file in files) { for (var file in files) {
@@ -112,10 +112,7 @@ Future<Isolate> _runIsolate(
); );
} }
Future<void> createPdfFromComicIsolate({ Future<File> createPdfFromComicIsolate(LocalComic comic, String savePath) async {
required LocalComic comic,
required String savePath,
}) async {
var receivePort = ReceivePort(); var receivePort = ReceivePort();
SendPort? sendPort; SendPort? sendPort;
Isolate? isolate; Isolate? isolate;
@@ -134,7 +131,8 @@ Future<void> createPdfFromComicIsolate({
} }
}); });
isolate = await _runIsolate(comic, savePath, receivePort.sendPort); isolate = await _runIsolate(comic, savePath, receivePort.sendPort);
return completer.future; await completer.future;
return File(savePath);
} }
class PdfGenerator { class PdfGenerator {

View File

@@ -15,9 +15,6 @@ extension TagsTranslation on String{
static final Map<String, Map<String, String>> _data = {}; static final Map<String, Map<String, String>> _data = {};
static Future<void> readData() async{ static Future<void> readData() async{
if(App.locale.languageCode != "zh"){
return;
}
var fileName = App.locale.countryCode == 'TW' var fileName = App.locale.countryCode == 'TW'
? "assets/tags_tw.json" ? "assets/tags_tw.json"
: "assets/tags.json"; : "assets/tags.json";

View File

@@ -307,19 +307,21 @@ packages:
flutter_inappwebview: flutter_inappwebview:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_inappwebview path: flutter_inappwebview
sha256: "80092d13d3e29b6227e25b67973c67c7210bd5e35c4b747ca908e31eb71a46d5" ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
url: "https://pub.dev" resolved-ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
source: hosted url: "https://github.com/pichillilorenzo/flutter_inappwebview"
version: "6.1.5" source: git
version: "6.2.0-beta.3"
flutter_inappwebview_android: flutter_inappwebview_android:
dependency: transitive dependency: transitive
description: description:
name: flutter_inappwebview_android path: flutter_inappwebview_android
sha256: "62557c15a5c2db5d195cb3892aab74fcaec266d7b86d59a6f0027abd672cddba" ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
url: "https://pub.dev" resolved-ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
source: hosted url: "https://github.com/pichillilorenzo/flutter_inappwebview"
version: "1.1.3" source: git
version: "1.2.0-beta.3"
flutter_inappwebview_internal_annotations: flutter_inappwebview_internal_annotations:
dependency: transitive dependency: transitive
description: description:
@@ -331,43 +333,48 @@ packages:
flutter_inappwebview_ios: flutter_inappwebview_ios:
dependency: transitive dependency: transitive
description: description:
name: flutter_inappwebview_ios path: flutter_inappwebview_ios
sha256: "5818cf9b26cf0cbb0f62ff50772217d41ea8d3d9cc00279c45f8aabaa1b4025d" ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
url: "https://pub.dev" resolved-ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
source: hosted url: "https://github.com/pichillilorenzo/flutter_inappwebview"
version: "1.1.2" source: git
version: "1.2.0-beta.3"
flutter_inappwebview_macos: flutter_inappwebview_macos:
dependency: transitive dependency: transitive
description: description:
name: flutter_inappwebview_macos path: flutter_inappwebview_macos
sha256: c1fbb86af1a3738e3541364d7d1866315ffb0468a1a77e34198c9be571287da1 ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
url: "https://pub.dev" resolved-ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
source: hosted url: "https://github.com/pichillilorenzo/flutter_inappwebview"
version: "1.1.2" source: git
version: "1.2.0-beta.3"
flutter_inappwebview_platform_interface: flutter_inappwebview_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: flutter_inappwebview_platform_interface path: flutter_inappwebview_platform_interface
sha256: cf5323e194096b6ede7a1ca808c3e0a078e4b33cc3f6338977d75b4024ba2500 ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
url: "https://pub.dev" resolved-ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
source: hosted url: "https://github.com/pichillilorenzo/flutter_inappwebview"
version: "1.3.0+1" source: git
version: "1.4.0-beta.3"
flutter_inappwebview_web: flutter_inappwebview_web:
dependency: transitive dependency: transitive
description: description:
name: flutter_inappwebview_web path: flutter_inappwebview_web
sha256: "55f89c83b0a0d3b7893306b3bb545ba4770a4df018204917148ebb42dc14a598" ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
url: "https://pub.dev" resolved-ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
source: hosted url: "https://github.com/pichillilorenzo/flutter_inappwebview"
version: "1.1.2" source: git
version: "1.2.0-beta.3"
flutter_inappwebview_windows: flutter_inappwebview_windows:
dependency: transitive dependency: transitive
description: description:
name: flutter_inappwebview_windows path: flutter_inappwebview_windows
sha256: "8b4d3a46078a2cdc636c4a3d10d10f2a16882f6be607962dbfff8874d1642055" ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
url: "https://pub.dev" resolved-ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
source: hosted url: "https://github.com/pichillilorenzo/flutter_inappwebview"
version: "0.6.0" source: git
version: "0.7.0-beta.3"
flutter_lints: flutter_lints:
dependency: "direct dev" dependency: "direct dev"
description: description:
@@ -1100,4 +1107,4 @@ packages:
version: "0.0.10" version: "0.0.10"
sdks: sdks:
dart: ">=3.6.0 <4.0.0" dart: ">=3.6.0 <4.0.0"
flutter: ">=3.27.3" flutter: ">=3.27.4"

View File

@@ -2,11 +2,11 @@ name: venera
description: "A comic app." description: "A comic app."
publish_to: 'none' publish_to: 'none'
version: 1.2.3+123 version: 1.2.5+125
environment: environment:
sdk: '>=3.6.0 <4.0.0' sdk: '>=3.6.0 <4.0.0'
flutter: 3.27.3 flutter: 3.27.4
dependencies: dependencies:
flutter: flutter:
@@ -43,7 +43,11 @@ dependencies:
git: git:
url: https://github.com/wgh136/flutter_desktop_webview url: https://github.com/wgh136/flutter_desktop_webview
path: packages/desktop_webview_window 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 app_links: ^6.3.3
sliver_tools: ^0.2.12 sliver_tools: ^0.2.12
flutter_file_dialog: ^3.0.2 flutter_file_dialog: ^3.0.2
@@ -82,7 +86,7 @@ dev_dependencies:
sdk: flutter sdk: flutter
flutter_lints: ^5.0.0 flutter_lints: ^5.0.0
flutter_to_arch: ^1.0.1 flutter_to_arch: ^1.0.1
flutter_to_debian: flutter_to_debian: ^2.0.2
flutter: flutter:
uses-material-design: true uses-material-design: true

View File

@@ -3,11 +3,36 @@
#define MyAppName "Venera" #define MyAppName "Venera"
#define MyAppVersion "{{version}}" #define MyAppVersion "{{version}}"
#define MyAppPublisher "wgh136" #define MyAppPublisher "nyne"
#define MyAppURL "https://github.com/venera-app/venera" #define MyAppURL "https://github.com/venera-app/venera"
#define MyAppExeName "venera.exe" #define MyAppExeName "venera.exe"
#define RootPath "{{root_path}}" #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] [Setup]
; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. ; 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.) ; (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 Compression=lzma
SolidCompression=yes SolidCompression=yes
WizardStyle=modern WizardStyle=modern
ArchitecturesInstallIn64BitMode=x64compatible
ArchitecturesAllowed=x64compatible
[Languages] [Languages]
Name: "english"; MessagesFile: "compiler:Default.isl" Name: "english"; MessagesFile: "compiler:Default.isl"

View File

@@ -29,7 +29,7 @@ file.close()
if not os.path.exists("windows/ChineseSimplified.isl"): if not os.path.exists("windows/ChineseSimplified.isl"):
# download 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) response = httpx.get(url)
with open('windows/ChineseSimplified.isl', 'wb') as file: with open('windows/ChineseSimplified.isl', 'wb') as file:
file.write(response.content) file.write(response.content)