38 Commits

Author SHA1 Message Date
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
nyne
24155746f2 Merge pull request #166 from venera-app/dev
v1.2.3
2025-02-01 16:35:34 +08:00
340496da30 Fix cloudflare bypass 2025-02-01 16:24:43 +08:00
28a56b4612 Update version code 2025-02-01 15:56:57 +08:00
4e6f71ef36 Merge account page and comic source page. 2025-02-01 15:54:52 +08:00
739685f60f Fix crash when using cbz export on iOS and macOS.
Close #164
2025-02-01 10:11:34 +08:00
8c5dae1e59 Fix empty page.
Close #160
2025-01-31 13:27:22 +08:00
e2c69d882f Fix image order.
Close #159
2025-01-31 13:11:04 +08:00
0b9f0b7d35 Improve downloading message.
Close #165
2025-01-31 13:08:24 +08:00
9ea749a84a login with webview on windows and linux.
fix #162, fix #141
2025-01-31 11:53:06 +08:00
d675af3fb4 fix cloudflare verification 2025-01-31 10:46:24 +08:00
d99a30b7d8 Update desktop file 2025-01-30 17:49:01 +08:00
nyne
3c3c07b6fb fix #163 2025-01-28 17:04:13 +08:00
nyne
e688ab759a Merge pull request #161 from UjuiUjuMandan/debug
move out applicationVariants.all
2025-01-27 16:34:18 +08:00
UjuiUjuMandan
64a3ef352f move out applicationVariants.all 2025-01-27 07:04:15 +00:00
ef8dc9e8d4 fix #158 2025-01-26 18:36:35 +08:00
45 changed files with 1417 additions and 1086 deletions

View File

@@ -39,12 +39,18 @@ jobs:
ln -s /Applications dist/dmg_contents/Applications
hdiutil create -volname "venera" -srcfolder dist/dmg_contents -ov -format UDZO "dist/venera.dmg"
- name: Add version to filename
run: |
APP_VERSION=$(grep "version:" pubspec.yaml | cut -d':' -f2 | tr -d ' ')
mkdir -p result
mv dist/venera.dmg result/venera-$APP_VERSION.dmg
# Step 4: Attach and upload artifacts (optional)
- name: Upload DMG
uses: actions/upload-artifact@v4
with:
name: venera.dmg
path: dist/venera.dmg
name: macos_build
path: result/
Build_IOS:
runs-on: macos-15
steps:
@@ -62,12 +68,17 @@ jobs:
mv /Users/runner/work/venera/venera/build/ios/iphoneos/Runner.app /Users/runner/work/venera/venera/build/ios/iphoneos/Payload
cd /Users/runner/work/venera/venera/build/ios/iphoneos/
zip -r venera-ios.ipa Payload
- name: Add version to filename
run: |
APP_VERSION=$(grep "version:" pubspec.yaml | cut -d':' -f2 | tr -d ' ')
mkdir -p result
mv build/ios/iphoneos/venera-ios.ipa result/venera-ios-$APP_VERSION.ipa
- uses: actions/upload-artifact@v4
with:
name: app-ios.ipa
path: /Users/runner/work/venera/venera/build/ios/iphoneos/venera-ios.ipa
name: ios_build
path: result/
Build_Android:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
@@ -118,7 +129,7 @@ jobs:
name: windows_build
path: build/windows/Venera-*
Build_Linux:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
@@ -130,7 +141,7 @@ jobs:
sudo apt-get update -y
sudo apt-get install -y ninja-build libgtk-3-dev webkit2gtk-4.1
dart pub global activate flutter_to_debian
- run: python3 debian/build.py
- run: python3 debian/build.py x64
- run: dart run flutter_to_arch
- run: |
sudo rm -rf build/linux/arch/app.tar.gz
@@ -145,19 +156,43 @@ jobs:
with:
name: arch_build
path: build/linux/arch/
Build_Linux_ARM64:
runs-on: ubuntu-22.04-arm
steps:
- uses: actions/checkout@v4
- name: Setup Flutter
run: |
FLUTTER_VERSION=$(grep " flutter:" pubspec.yaml | cut -d':' -f2 | tr -d ' ')
sudo apt-get update -y && sudo apt-get upgrade -y;
sudo apt-get install -y curl git unzip xz-utils zip libglu1-mesa clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev libstdc++-12-dev
git clone --depth 1 --branch $FLUTTER_VERSION https://github.com/flutter/flutter.git $RUNNER_TEMP/flutter
echo "$RUNNER_TEMP/flutter/bin" >> $GITHUB_PATH
- name: Install Flutter
run: flutter doctor
- name: Install dependencies
run: |
flutter pub get
sudo apt-get update -y
sudo apt-get install -y ninja-build libgtk-3-dev webkit2gtk-4.1
dart pub global activate flutter_to_debian
- run: python3 debian/build.py arm64
- uses: actions/upload-artifact@v4
with:
name: deb_arm64_build
path: build/linux/x64/release/debian # This is a bug related to flutter_to_debian, but it's not a big deal.
Release:
runs-on: ubuntu-latest
needs: [Build_MacOS, Build_IOS, Build_Android, Build_Windows, Build_Linux]
runs-on: ubuntu-22.04
needs: [Build_MacOS, Build_IOS, Build_Android, Build_Windows, Build_Linux, Build_Linux_ARM64]
if: github.event_name == 'release' # 仅在 push 事件时执行
steps:
- uses: actions/download-artifact@v4
with:
name: venera.dmg
name: macos_build
path: outputs
- uses: actions/download-artifact@v4
with:
name: app-ios.ipa
name: ios_build
path: outputs
- uses: actions/download-artifact@v4
with:
@@ -175,6 +210,10 @@ jobs:
with:
name: arch_build
path: outputs
- uses: actions/download-artifact@v4
with:
name: deb_arm64_build
path: outputs
- uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.ref_name }}

View File

@@ -1,5 +1,4 @@
# venera
[![flutter](https://img.shields.io/badge/flutter-3.27.1-blue)](https://flutter.dev/)
[![License](https://img.shields.io/github/license/venera-app/venera)](https://github.com/venera-app/venera/blob/master/LICENSE)
[![Download](https://img.shields.io/github/v/release/venera-app/venera)](https://github.com/venera-app/venera/releases)
@@ -13,7 +12,6 @@ A comic reader that support reading local and network comics.
height="75">](https://f-droid.org/packages/com.github.wgh136.venera/)
## Features
- Read local comics
- Use javascript to create comic sources
- Read comics from network sources
@@ -23,14 +21,12 @@ A comic reader that support reading local and network comics.
- Login to comment, rate, and other operations if the source supports
## Build from source
1. Clone the repository
2. Install flutter, see [flutter.dev](https://flutter.dev/docs/get-started/install)
3. Install rust, see [rustup.rs](https://rustup.rs/)
4. Build for your platform: e.g. `flutter build apk`
## Create a new comic source
See [Comic Source](doc/comic_source.md)
## Thanks

View File

@@ -83,31 +83,31 @@ android {
abiFilters "armeabi-v7a", "arm64-v8a", "x86_64"
}
signingConfig signingConfigs.release
applicationVariants.all { variant ->
variant.outputs.all { output ->
def abi = output.getFilter(com.android.build.OutputFile.ABI)
if (abi != null) {
outputFileName = "venera-${variant.versionName}-${abi}.apk"
def abiVersionCode = project.ext.abiCodes.get(abi)
if (abiVersionCode != null) {
versionCodeOverride = variant.versionCode * 10 + abiVersionCode
}
} else {
outputFileName = "venera-${variant.versionName}.apk"
versionCodeOverride = variant.versionCode * 10
}
}
}
}
debug {
ndk {
abiFilters "armeabi-v7a", "arm64-v8a", "x86_64"
}
signingConfig signingConfigs.debug
applicationVariants.all { variant ->
variant.outputs.all { output ->
versionCodeOverride = variant.versionCode * 10 + 4
}
}
applicationVariants.all { variant ->
variant.outputs.all { output ->
def abi = output.getFilter(com.android.build.OutputFile.ABI)
if (variant.buildType.name == "release") {
if (abi != null) {
outputFileName = "venera-${variant.versionName}-${abi}.apk"
def abiVersionCode = project.ext.abiCodes.get(abi)
if (abiVersionCode != null) {
versionCodeOverride = variant.versionCode * 10 + abiVersionCode
}
} else {
outputFileName = "venera-${variant.versionName}.apk"
versionCodeOverride = variant.versionCode * 10
}
} else if (variant.buildType.name == "debug") {
versionCodeOverride = variant.versionCode * 10 + 4
}
}
}

View File

@@ -318,7 +318,13 @@
"Deselect All": "取消全选",
"Add keyword": "添加关键词",
"Keyword": "关键词",
"Manage": "管理"
"Manage": "管理",
"Verify": "验证",
"Cloudflare verification required": "需要Cloudflare验证",
"Success": "成功",
"Compressing": "压缩中",
"Exporting": "导出中",
"Search Sources": "搜索源"
},
"zh_TW": {
"Home": "首頁",
@@ -639,6 +645,12 @@
"Deselect All": "取消全選",
"Add keyword": "添加關鍵詞",
"Keyword": "關鍵詞",
"Manage": "管理"
"Manage": "管理",
"Verify": "驗證",
"Cloudflare verification required": "需要Cloudflare驗證",
"Success": "成功",
"Compressing": "壓縮中",
"Exporting": "匯出中",
"Search Sources": "搜索源"
}
}

11
debian/build.py vendored
View File

@@ -1,5 +1,7 @@
import subprocess
import sys
arch = sys.argv[1]
debianContent = ''
desktopContent = ''
version = ''
@@ -12,7 +14,14 @@ with open('pubspec.yaml', 'r') as f:
version = str.split(str.split(f.read(), 'version: ')[1], '+')[0]
with open('debian/debian.yaml', 'w') as f:
f.write(debianContent.replace('{{Version}}', version))
content = debianContent.replace('{{Version}}', version)
if arch == 'x64':
content = content.replace('{{Arch}}', 'x64')
content = content.replace('{{Architecture}}', 'amd64')
elif arch == 'arm64':
content = content.replace('{{Arch}}', 'arm64')
content = content.replace('{{Architecture}}', 'arm64')
f.write(content)
with open('debian/gui/venera.desktop', 'w') as f:
f.write(desktopContent.replace('{{Version}}', version))

6
debian/debian.yaml vendored
View File

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

View File

@@ -5,4 +5,5 @@ Comment=venera
Terminal=false
Type=Application
Categories=Utility
Keywords=Flutter;comic;images;
Keywords=Flutter;comic;images;
Icon=venera

View File

@@ -550,7 +550,7 @@ class _ComicDescription extends StatelessWidget {
int cnt = (constraints.maxHeight - 22).toInt() ~/ 25;
return Container(
clipBehavior: Clip.antiAlias,
height: 22 + cnt * 25,
height: 21 + cnt * 24,
width: double.infinity,
decoration: const BoxDecoration(),
child: Wrap(
@@ -562,31 +562,30 @@ class _ComicDescription extends StatelessWidget {
children: [
for (var s in tags!)
Container(
height: 22,
padding: const EdgeInsets.fromLTRB(3, 2, 3, 2),
constraints: BoxConstraints(
maxWidth: constraints.maxWidth * 0.45,
height: 21,
padding: const EdgeInsets.symmetric(horizontal: 4),
constraints: BoxConstraints(
maxWidth: constraints.maxWidth * 0.45,
),
decoration: BoxDecoration(
color: s == "Unavailable"
? context.colorScheme.errorContainer
: context.colorScheme.secondaryContainer,
borderRadius: BorderRadius.circular(8),
),
child: Center(
widthFactor: 1,
child: Text(
enableTranslate
? TagsTranslation.translateTag(s)
: s.split(':').last,
style: const TextStyle(fontSize: 12),
softWrap: true,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
decoration: BoxDecoration(
color: s == "Unavailable"
? Theme.of(context).colorScheme.errorContainer
: Theme.of(context)
.colorScheme
.secondaryContainer,
borderRadius:
const BorderRadius.all(Radius.circular(8)),
),
child: Center(
widthFactor: 1,
child: Text(
enableTranslate
? TagsTranslation.translateTag(s)
: s.split(':').last,
style: const TextStyle(fontSize: 12),
softWrap: true,
overflow: TextOverflow.ellipsis,
maxLines: 1,
))),
),
),
],
),
).toAlign(Alignment.topCenter);
@@ -1520,14 +1519,15 @@ class SimpleComicTile extends StatelessWidget {
return AnimatedTapRegion(
borderRadius: 8,
onTap: onTap ?? () {
context.to(
() => ComicPage(
id: comic.id,
sourceKey: comic.sourceKey,
),
);
},
onTap: onTap ??
() {
context.to(
() => ComicPage(
id: comic.id,
sourceKey: comic.sourceKey,
),
);
},
child: Container(
width: 92,
height: 114,

View File

@@ -148,3 +148,18 @@ class SliverGridDelegateWithComics extends SliverGridDelegate {
return false;
}
}
class SliverLazyToBoxAdapter extends StatelessWidget {
/// Creates a sliver that contains a single box widget which can be lazy loaded.
const SliverLazyToBoxAdapter({super.key, required this.child});
final Widget child;
@override
Widget build(BuildContext context) {
return SliverList.list(children: [
SizedBox(),
child,
]);
}
}

View File

@@ -57,7 +57,9 @@ class NetworkError extends StatelessWidget {
if (cfe != null)
FilledButton(
onPressed: () => passCloudflare(
CloudflareException.fromString(message)!, retry!),
CloudflareException.fromString(message)!,
retry!,
),
child: Text('Verify'.tl),
)
else
@@ -130,7 +132,7 @@ abstract class LoadingState<T extends StatefulWidget, S extends Object>
if (res.success) {
return res;
} else {
if(!mounted) return res;
if (!mounted) return res;
if (retry >= 3) {
return res;
}
@@ -188,7 +190,7 @@ abstract class LoadingState<T extends StatefulWidget, S extends Object>
isLoading = true;
Future.microtask(() {
loadDataWithRetry().then((value) async {
if(!mounted) return;
if (!mounted) return;
if (value.success) {
data = value.data;
await onDataLoaded();
@@ -321,21 +323,11 @@ abstract class MultiPageLoadingState<T extends StatefulWidget, S extends Object>
}
Widget buildError(BuildContext context, String error) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(error, maxLines: 3),
const SizedBox(height: 12),
Button.outlined(
onPressed: () {
reset();
},
child: const Text("Retry"),
)
],
),
).paddingHorizontal(16);
return NetworkError(
withAppbar: false,
message: error,
retry: reset,
);
}
@override

View File

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

View File

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

View File

@@ -126,6 +126,7 @@ class _Settings with ChangeNotifier {
'explore_pages': [],
'categories': [],
'favorites': [],
'searchSources': null,
'showFavoriteStatusOnTile': true,
'showHistoryStatusOnTile': false,
'blockedWords': [],
@@ -155,7 +156,7 @@ class _Settings with ChangeNotifier {
'customImageProcessing': defaultCustomImageProcessing,
'sni': true,
'autoAddLanguageFilter': 'none', // none, chinese, english, japanese
'comicSourceListUrl': "https://raw.githubusercontent.com/venera-app/venera-configs/master/index.json",
'comicSourceListUrl': "https://cdn.jsdelivr.net/gh/venera-app/venera-configs@latest/index.json",
};
operator [](String key) {

View File

@@ -42,11 +42,16 @@ Future<void> init() async {
await ComicSource.init().wait();
await LocalManager().init().wait();
CacheManager().setLimitSize(appdata.settings['cacheSize']);
if (appdata.settings['searchSources'] == null) {
appdata.settings['searchSources'] = ComicSource.all()
.where((e) => e.searchPageData != null)
.map((e) => e.key)
.toList();
}
if (App.isAndroid) {
handleLinks();
}
FlutterError.onError = (details) {
Log.error(
"Unhandled Exception", "${details.exception}\n${details.stack}");
Log.error("Unhandled Exception", "${details.exception}\n${details.stack}");
};
}
}

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

View File

@@ -1,9 +1,11 @@
import 'dart:io' as io;
import 'package:dio/dio.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:venera/foundation/app.dart';
import 'package:venera/foundation/appdata.dart';
import 'package:venera/foundation/consts.dart';
import 'package:venera/foundation/log.dart';
import 'package:venera/pages/webview.dart';
import 'package:venera/utils/ext.dart';
@@ -58,7 +60,7 @@ class CloudflareException implements DioException {
class CloudflareInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
if(options.headers['cookie'].toString().contains('cf_clearance')) {
if (options.headers['cookie'].toString().contains('cf_clearance')) {
options.headers['user-agent'] = appdata.implicitData['ua'] ?? webUA;
}
handler.next(options);
@@ -120,16 +122,25 @@ void passCloudflare(CloudflareException e, void Function() onFinished) async {
var webview = DesktopWebview(
initialUrl: url,
onTitleChange: (title, controller) async {
var res = await controller.evaluateJavascript(
"document.head.innerHTML.includes('#challenge-success-text')");
if (res == 'false') {
var head =
await controller.evaluateJavascript("document.head.innerHTML") ??
"";
Log.info("Cloudflare", "Checking head: $head");
var isChallenging = head.contains('#challenge-success-text') ||
head.contains("#challenge-error-text") ||
head.contains("#challenge-form");
if (!isChallenging) {
Log.info(
"Cloudflare",
"Cloudflare is passed due to there is no challenge css",
);
var ua = controller.userAgent;
if (ua != null) {
appdata.implicitData['ua'] = ua;
appdata.writeImplicitData();
}
var cookiesMap = await controller.getCookies(url);
if(cookiesMap['cf_clearance'] == null) {
if (cookiesMap['cf_clearance'] == null) {
return;
}
saveCookies(cookiesMap);
@@ -137,30 +148,51 @@ void passCloudflare(CloudflareException e, void Function() onFinished) async {
onFinished();
}
},
onClose: onFinished,
);
webview.open();
} else {
bool success = false;
void check(InAppWebViewController controller) async {
var head = await controller.evaluateJavascript(
source: "document.head.innerHTML") as String;
Log.info("Cloudflare", "Checking head: $head");
var isChallenging = head.contains('#challenge-success-text') ||
head.contains("#challenge-error-text") ||
head.contains("#challenge-form");
if (!isChallenging) {
Log.info(
"Cloudflare",
"Cloudflare is passed due to there is no challenge css",
);
var ua = await controller.getUA();
if (ua != null) {
appdata.implicitData['ua'] = ua;
appdata.writeImplicitData();
}
var cookies = await controller.getCookies(url) ?? [];
if (cookies.firstWhereOrNull(
(element) => element.name == 'cf_clearance') ==
null) {
return;
}
SingleInstanceCookieJar.instance?.saveFromResponse(uri, cookies);
if (!success) {
App.rootPop();
success = true;
}
}
}
await App.rootContext.to(
() => AppWebview(
initialUrl: url,
singlePage: true,
onTitleChange: (title, controller) async {
check(controller);
},
onLoadStop: (controller) async {
var res = await controller.platform.evaluateJavascript(
source:
"document.head.innerHTML.includes('#challenge-success-text')");
if (res == false) {
var ua = await controller.getUA();
if (ua != null) {
appdata.implicitData['ua'] = ua;
appdata.writeImplicitData();
}
var cookies = await controller.getCookies(url) ?? [];
if(cookies.firstWhereOrNull((element) => element.name == 'cf_clearance') == null) {
return;
}
SingleInstanceCookieJar.instance?.saveFromResponse(uri, cookies);
App.rootPop();
}
check(controller);
},
onStarted: (controller) async {
var ua = await controller.getUA();

View File

@@ -59,6 +59,16 @@ abstract class DownloadTask with ChangeNotifier {
return null;
}
}
@override
bool operator ==(Object other) {
return other is DownloadTask &&
other.id == id &&
other.comicType == comicType;
}
@override
int get hashCode => Object.hash(id, comicType);
}
class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
@@ -220,7 +230,9 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
runRecorder();
if (comic == null) {
var res = await runWithRetry(() async {
_message = "Fetching comic info...";
notifyListeners();
var res = await _runWithRetry(() async {
var r = await source.loadComicInfo!(comicId);
if (r.error) {
throw r.errorMessage!;
@@ -260,7 +272,9 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
await LocalManager().saveCurrentDownloadingTasks();
if (_cover == null) {
var res = await runWithRetry(() async {
_message = "Downloading cover...";
notifyListeners();
var res = await _runWithRetry(() async {
Uint8List? data;
await for (var progress
in ImageDownloader.loadThumbnail(comic!.cover, source.key)) {
@@ -272,8 +286,7 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
throw "Failed to download cover";
}
var fileType = detectFileType(data);
var file =
File(FilePath.join(path!, "cover${fileType.ext}"));
var file = File(FilePath.join(path!, "cover${fileType.ext}"));
file.writeAsBytesSync(data);
return "file://${file.path}";
});
@@ -290,7 +303,9 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
if (_images == null) {
if (comic!.chapters == null) {
var res = await runWithRetry(() async {
_message = "Fetching image list...";
notifyListeners();
var res = await _runWithRetry(() async {
var r = await source.loadComicPages!(comicId, null);
if (r.error) {
throw r.errorMessage!;
@@ -312,6 +327,8 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
} else {
_images = {};
_totalCount = 0;
int cpCount = 0;
int totalCpCount = chapters?.length ?? comic!.chapters!.length;
for (var i in comic!.chapters!.keys) {
if (chapters != null && !chapters!.contains(i)) {
continue;
@@ -320,7 +337,9 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
_totalCount += _images![i]!.length;
continue;
}
var res = await runWithRetry(() async {
_message = "Fetching image list ($cpCount/$totalCpCount)...";
notifyListeners();
var res = await _runWithRetry(() async {
var r = await source.loadComicPages!(comicId, i);
if (r.error) {
throw r.errorMessage!;
@@ -458,8 +477,7 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
}).toList(),
directory: Directory(path!).name,
chapters: comic!.chapters,
cover:
File(_cover!.split("file://").last).name,
cover: File(_cover!.split("file://").last).name,
comicType: ComicType(source.key.hashCode),
downloadedChapters: chapters ?? [],
createdAt: DateTime.now(),
@@ -478,7 +496,7 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
int get hashCode => Object.hash(comicId, source.key);
}
Future<Res<T>> runWithRetry<T>(Future<T> Function() task,
Future<Res<T>> _runWithRetry<T>(Future<T> Function() task,
{int retry = 3}) async {
for (var i = 0; i < retry; i++) {
try {

View File

@@ -1,349 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:venera/components/components.dart';
import 'package:venera/foundation/app.dart';
import 'package:venera/foundation/comic_source/comic_source.dart';
import 'package:venera/foundation/state_controller.dart';
import 'package:venera/network/cookie_jar.dart';
import 'package:venera/pages/webview.dart';
import 'package:venera/utils/translations.dart';
class AccountsPageLogic extends StateController {
final _reLogin = <String, bool>{};
}
class AccountsPage extends StatelessWidget {
const AccountsPage({super.key});
AccountsPageLogic get logic => StateController.find<AccountsPageLogic>();
@override
Widget build(BuildContext context) {
var body = StateBuilder<AccountsPageLogic>(
init: AccountsPageLogic(),
builder: (logic) {
return CustomScrollView(
slivers: [
SliverAppbar(title: Text("Accounts".tl)),
SliverList(
delegate: SliverChildListDelegate(
buildContent(context).toList(),
),
),
SliverPadding(
padding: EdgeInsets.only(bottom: context.padding.bottom),
)
],
);
},
);
return Scaffold(
body: body,
);
}
Iterable<Widget> buildContent(BuildContext context) sync* {
var sources = ComicSource.all().where((element) => element.account != null);
if (sources.isEmpty) return;
for (var element in sources) {
final bool logged = element.isLogged;
yield Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
child: Text(
element.name,
style: const TextStyle(fontSize: 16),
),
);
if (!logged) {
yield ListTile(
title: Text("Log in".tl),
trailing: const Icon(Icons.arrow_right),
onTap: () async {
await context.to(
() => _LoginPage(
config: element.account!,
source: element,
),
);
element.saveData();
ComicSource.notifyListeners();
logic.update();
},
);
}
if (logged) {
for (var item in element.account!.infoItems) {
if (item.builder != null) {
yield item.builder!(context);
} else {
yield ListTile(
title: Text(item.title.tl),
subtitle: item.data == null ? null : Text(item.data!()),
onTap: item.onTap,
);
}
}
if (element.data["account"] is List) {
bool loading = logic._reLogin[element.key] == true;
yield ListTile(
title: Text("Re-login".tl),
subtitle: Text("Click if login expired".tl),
onTap: () async {
if (element.data["account"] == null) {
context.showMessage(message: "No data".tl);
return;
}
logic._reLogin[element.key] = true;
logic.update();
final List account = element.data["account"];
var res = await element.account!.login!(account[0], account[1]);
if (res.error) {
context.showMessage(message: res.errorMessage!);
} else {
context.showMessage(message: "Success".tl);
}
logic._reLogin[element.key] = false;
logic.update();
},
trailing: loading
? const SizedBox.square(
dimension: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
),
)
: const Icon(Icons.refresh),
);
}
yield ListTile(
title: Text("Log out".tl),
onTap: () {
element.data["account"] = null;
element.account?.logout();
element.saveData();
ComicSource.notifyListeners();
logic.update();
},
trailing: const Icon(Icons.logout),
);
}
yield const Divider(thickness: 0.6);
}
}
void setClipboard(String text) {
Clipboard.setData(ClipboardData(text: text));
showToast(
message: "Copied".tl,
icon: const Icon(Icons.check),
context: App.rootContext,
);
}
}
class _LoginPage extends StatefulWidget {
const _LoginPage({required this.config, required this.source});
final AccountConfig config;
final ComicSource source;
@override
State<_LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<_LoginPage> {
String username = "";
String password = "";
bool loading = false;
final Map<String, String> _cookies = {};
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: const Appbar(
title: Text(''),
),
body: Center(
child: Container(
padding: const EdgeInsets.all(16),
constraints: const BoxConstraints(maxWidth: 400),
child: AutofillGroup(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text("Login".tl, style: const TextStyle(fontSize: 24)),
const SizedBox(height: 32),
if (widget.config.cookieFields == null)
TextField(
decoration: InputDecoration(
labelText: "Username".tl,
border: const OutlineInputBorder(),
),
enabled: widget.config.login != null,
onChanged: (s) {
username = s;
},
autofillHints: const [AutofillHints.username],
).paddingBottom(16),
if (widget.config.cookieFields == null)
TextField(
decoration: InputDecoration(
labelText: "Password".tl,
border: const OutlineInputBorder(),
),
obscureText: true,
enabled: widget.config.login != null,
onChanged: (s) {
password = s;
},
onSubmitted: (s) => login(),
autofillHints: const [AutofillHints.password],
).paddingBottom(16),
for (var field in widget.config.cookieFields ?? <String>[])
TextField(
decoration: InputDecoration(
labelText: field,
border: const OutlineInputBorder(),
),
obscureText: true,
enabled: widget.config.validateCookies != null,
onChanged: (s) {
_cookies[field] = s;
},
).paddingBottom(16),
if (widget.config.login == null &&
widget.config.cookieFields == null)
Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.error_outline),
const SizedBox(width: 8),
Text("Login with password is disabled".tl),
],
)
else
Button.filled(
isLoading: loading,
onPressed: login,
child: Text("Continue".tl),
),
const SizedBox(height: 24),
if (widget.config.loginWebsite != null)
TextButton(
onPressed: loginWithWebview,
child: Text("Login with webview".tl),
),
const SizedBox(height: 8),
if (widget.config.registerWebsite != null)
TextButton(
onPressed: () =>
launchUrlString(widget.config.registerWebsite!),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.link),
const SizedBox(width: 8),
Text("Create Account".tl),
],
),
),
],
),
),
),
),
);
}
void login() {
if (widget.config.login != null) {
if (username.isEmpty || password.isEmpty) {
showToast(
message: "Cannot be empty".tl,
icon: const Icon(Icons.error_outline),
context: context,
);
return;
}
setState(() {
loading = true;
});
widget.config.login!(username, password).then((value) {
if (value.error) {
context.showMessage(message: value.errorMessage!);
setState(() {
loading = false;
});
} else {
if (mounted) {
context.pop();
}
}
});
} else if (widget.config.validateCookies != null) {
setState(() {
loading = true;
});
var cookies =
widget.config.cookieFields!.map((e) => _cookies[e] ?? '').toList();
widget.config.validateCookies!(cookies).then((value) {
if (value) {
widget.source.data['account'] = 'ok';
widget.source.saveData();
context.pop();
} else {
context.showMessage(message: "Invalid cookies".tl);
setState(() {
loading = false;
});
}
});
}
}
void loginWithWebview() async {
var url = widget.config.loginWebsite!;
var title = '';
bool success = false;
void validate(InAppWebViewController c) async {
if (widget.config.checkLoginStatus != null
&& widget.config.checkLoginStatus!(url, title)) {
var cookies = (await c.getCookies(url)) ?? [];
SingleInstanceCookieJar.instance?.saveFromResponse(
Uri.parse(url),
cookies,
);
success = true;
widget.config.onLoginWithWebviewSuccess?.call();
App.mainNavigatorKey?.currentContext?.pop();
}
}
await context.to(
() => AppWebview(
initialUrl: widget.config.loginWebsite!,
onNavigation: (u, c) {
url = u;
validate(c);
return false;
},
onTitleChange: (t, c) {
title = t;
validate(c);
},
),
);
if (success) {
widget.source.data['account'] = 'ok';
widget.source.saveData();
context.pop();
}
}
}

View File

@@ -2,6 +2,7 @@ import "package:flutter/material.dart";
import 'package:shimmer_animation/shimmer_animation.dart';
import "package:venera/components/components.dart";
import "package:venera/foundation/app.dart";
import "package:venera/foundation/appdata.dart";
import "package:venera/foundation/comic_source/comic_source.dart";
import "package:venera/pages/search_result_page.dart";
import "package:venera/utils/translations.dart";
@@ -24,7 +25,18 @@ class _AggregatedSearchPageState extends State<AggregatedSearchPage> {
@override
void initState() {
sources = ComicSource.all().where((e) => e.searchPageData != null).toList();
var all = ComicSource.all()
.where((e) => e.searchPageData != null)
.map((e) => e.key)
.toList();
var settings = appdata.settings['searchSources'] as List;
var sources = <String>[];
for (var source in settings) {
if (all.contains(source)) {
sources.add(source);
}
}
this.sources = sources.map((e) => ComicSource.find(e)!).toList();
_keyword = widget.keyword;
controller = SearchBarController(
currentText: widget.keyword,

View File

@@ -206,62 +206,64 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
yield const SliverPadding(padding: EdgeInsets.only(top: 8));
yield Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(width: 16),
Hero(
tag: "cover${comic.id}${comic.sourceKey}",
child: Container(
decoration: BoxDecoration(
color: context.colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: context.colorScheme.outlineVariant,
blurRadius: 1,
offset: const Offset(0, 1),
yield SliverLazyToBoxAdapter(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(width: 16),
Hero(
tag: "cover${comic.id}${comic.sourceKey}",
child: Container(
decoration: BoxDecoration(
color: context.colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: context.colorScheme.outlineVariant,
blurRadius: 1,
offset: const Offset(0, 1),
),
],
),
height: 144,
width: 144 * 0.72,
clipBehavior: Clip.antiAlias,
child: AnimatedImage(
image: CachedImageProvider(
widget.cover ?? comic.cover,
sourceKey: comic.sourceKey,
cid: comic.id,
),
width: double.infinity,
height: double.infinity,
),
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SelectableText(comic.title, style: ts.s18),
if (comic.subTitle != null)
SelectableText(comic.subTitle!, style: ts.s14)
.paddingVertical(4),
Text(
(ComicSource.find(comic.sourceKey)?.name) ?? '',
style: ts.s12,
),
],
),
height: 144,
width: 144 * 0.72,
clipBehavior: Clip.antiAlias,
child: AnimatedImage(
image: CachedImageProvider(
widget.cover ?? comic.cover,
sourceKey: comic.sourceKey,
cid: comic.id,
),
width: double.infinity,
height: double.infinity,
),
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SelectableText(comic.title, style: ts.s18),
if (comic.subTitle != null)
SelectableText(comic.subTitle!, style: ts.s14)
.paddingVertical(4),
Text(
(ComicSource.find(comic.sourceKey)?.name) ?? '',
style: ts.s12,
),
],
),
),
],
).toSliver();
],
),
);
}
Widget buildActions() {
bool isMobile = context.width < changePoint;
bool hasHistory = history != null && (history!.ep > 1 || history!.page > 1);
return SliverToBoxAdapter(
return SliverLazyToBoxAdapter(
child: Column(
children: [
ListView(
@@ -354,7 +356,7 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
if (comic.description == null || comic.description!.trim().isEmpty) {
return const SliverPadding(padding: EdgeInsets.zero);
}
return SliverToBoxAdapter(
return SliverLazyToBoxAdapter(
child: Column(
children: [
ListTile(
@@ -482,7 +484,7 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
bool enableTranslation =
App.locale.languageCode == 'zh' && comicSource.enableTagsTranslate;
return SliverToBoxAdapter(
return SliverLazyToBoxAdapter(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -1388,42 +1390,67 @@ class _FavoritePanel extends StatefulWidget {
State<_FavoritePanel> createState() => _FavoritePanelState();
}
class _FavoritePanelState extends State<_FavoritePanel> {
class _FavoritePanelState extends State<_FavoritePanel>
with SingleTickerProviderStateMixin {
late ComicSource comicSource;
late TabController tabController;
late bool hasNetwork;
@override
void initState() {
comicSource = widget.type.comicSource!;
localFolders = LocalFavoritesManager().folderNames;
added = LocalFavoritesManager().find(widget.cid, widget.type);
hasNetwork = comicSource.favoriteData != null && comicSource.isLogged;
var initIndex = 0;
if (appdata.implicitData['favoritePanelIndex'] is int) {
initIndex = appdata.implicitData['favoritePanelIndex'];
}
initIndex = initIndex.clamp(0, hasNetwork ? 1 : 0);
tabController = TabController(
initialIndex: initIndex,
length: hasNetwork ? 2 : 1,
vsync: this,
);
super.initState();
}
@override
void dispose() {
var currentIndex = tabController.index;
appdata.implicitData['favoritePanelIndex'] = currentIndex;
appdata.writeImplicitData();
tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
var hasNetwork = comicSource.favoriteData != null && comicSource.isLogged;
return Scaffold(
appBar: Appbar(
title: Text("Favorite".tl),
),
body: DefaultTabController(
length: hasNetwork ? 2 : 1,
child: Column(
children: [
TabBar(tabs: [
body: Column(
children: [
TabBar(
controller: tabController,
tabs: [
Tab(text: "Local".tl),
if (hasNetwork) Tab(text: "Network".tl),
]),
Expanded(
child: TabBarView(
children: [
buildLocal(),
if (hasNetwork) buildNetwork(),
],
),
],
),
Expanded(
child: TabBarView(
controller: tabController,
children: [
buildLocal(),
if (hasNetwork) buildNetwork(),
],
),
],
),
),
],
),
);
}
@@ -1850,7 +1877,7 @@ class _CommentsPartState extends State<_CommentsPart> {
Widget build(BuildContext context) {
return MultiSliver(
children: [
SliverToBoxAdapter(
SliverLazyToBoxAdapter(
child: ListTile(
title: Text("Comments".tl),
trailing: Row(

View File

@@ -1,5 +1,7 @@
import 'dart:convert';
import 'dart:io' as io;
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:venera/components/components.dart';
import 'package:venera/foundation/app.dart';
@@ -7,11 +9,13 @@ import 'package:venera/foundation/appdata.dart';
import 'package:venera/foundation/comic_source/comic_source.dart';
import 'package:venera/foundation/log.dart';
import 'package:venera/network/app_dio.dart';
import 'package:venera/network/cookie_jar.dart';
import 'package:venera/pages/webview.dart';
import 'package:venera/utils/ext.dart';
import 'package:venera/utils/io.dart';
import 'package:venera/utils/translations.dart';
class ComicSourcePage extends StatefulWidget {
class ComicSourcePage extends StatelessWidget {
const ComicSourcePage({super.key});
static Future<int> checkComicSourceUpdate() async {
@@ -44,11 +48,6 @@ class ComicSourcePage extends StatefulWidget {
return shouldUpdate.length;
}
@override
State<ComicSourcePage> createState() => _ComicSourcePageState();
}
class _ComicSourcePageState extends State<ComicSourcePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -92,167 +91,19 @@ class _BodyState extends State<_Body> {
style: AppbarStyle.shadow,
),
buildCard(context),
for (var source in ComicSource.all()) buildSource(context, source),
for (var source in ComicSource.all())
_SliverComicSource(
key: ValueKey(source.key),
source: source,
edit: edit,
update: update,
delete: delete,
),
SliverPadding(padding: EdgeInsets.only(bottom: context.padding.bottom)),
],
);
}
Widget buildSource(BuildContext context, ComicSource source) {
var newVersion = ComicSource.availableUpdates[source.key];
bool hasUpdate =
newVersion != null && compareSemVer(newVersion, source.version);
return SliverToBoxAdapter(
child: Column(
children: [
const Divider(),
ListTile(
title: Row(
children: [
Text(source.name),
const SizedBox(width: 6),
if (hasUpdate)
Tooltip(
message: newVersion,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: context.colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(8),
),
child: Text(
"New Version".tl,
style: const TextStyle(fontSize: 13),
),
),
)
],
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Tooltip(
message: "Edit".tl,
child: IconButton(
onPressed: () => edit(source),
icon: const Icon(Icons.edit_note)),
),
Tooltip(
message: "Update".tl,
child: IconButton(
onPressed: () => update(source),
icon: const Icon(Icons.update)),
),
Tooltip(
message: "Delete".tl,
child: IconButton(
onPressed: () => delete(source),
icon: const Icon(Icons.delete)),
),
],
),
),
ListTile(
title: const Text("Version"),
subtitle: Text(source.version),
),
...buildSourceSettings(source),
],
),
);
}
Iterable<Widget> buildSourceSettings(ComicSource source) sync* {
if (source.settings == null) {
return;
} else if (source.data['settings'] == null) {
source.data['settings'] = {};
}
for (var item in source.settings!.entries) {
var key = item.key;
String type = item.value['type'];
try {
if (type == "select") {
var current = source.data['settings'][key];
if (current == null) {
var d = item.value['default'];
for (var option in item.value['options']) {
if (option['value'] == d) {
current = option['text'] ?? option['value'];
break;
}
}
} else {
current = item.value['options']
.firstWhere((e) => e['value'] == current)['text'] ??
current;
}
yield ListTile(
title: Text((item.value['title'] as String).ts(source.key)),
trailing: Select(
current: (current as String).ts(source.key),
values: (item.value['options'] as List)
.map<String>((e) =>
((e['text'] ?? e['value']) as String).ts(source.key))
.toList(),
onTap: (i) {
source.data['settings'][key] =
item.value['options'][i]['value'];
source.saveData();
setState(() {});
},
),
);
} else if (type == "switch") {
var current = source.data['settings'][key] ?? item.value['default'];
yield ListTile(
title: Text((item.value['title'] as String).ts(source.key)),
trailing: Switch(
value: current,
onChanged: (v) {
source.data['settings'][key] = v;
source.saveData();
setState(() {});
},
),
);
} else if (type == "input") {
var current =
source.data['settings'][key] ?? item.value['default'] ?? '';
yield ListTile(
title: Text((item.value['title'] as String).ts(source.key)),
subtitle:
Text(current, maxLines: 1, overflow: TextOverflow.ellipsis),
trailing: IconButton(
icon: const Icon(Icons.edit),
onPressed: () {
showInputDialog(
context: context,
title: (item.value['title'] as String).ts(source.key),
initialValue: current,
inputValidator: item.value['validator'] == null
? null
: RegExp(item.value['validator']),
onConfirm: (value) {
source.data['settings'][key] = value;
source.saveData();
setState(() {});
return null;
},
);
},
),
);
} else if (type == "callback") {
yield _CallbackSetting(setting: item, sourceKey: source.key);
}
} catch (e, s) {
Log.error("ComicSourcePage", "Failed to build a setting\n$e\n$s");
}
}
}
void delete(ComicSource source) {
showConfirmDialog(
context: App.rootContext,
@@ -297,10 +148,12 @@ class _BodyState extends State<_Body> {
//
}
}
context.to(() => _EditFilePage(source.filePath, () async {
await ComicSource.reload();
setState(() {});
}));
context.to(
() => _EditFilePage(source.filePath, () async {
await ComicSource.reload();
setState(() {});
}),
);
}
static Future<void> update(ComicSource source) async {
@@ -764,3 +617,566 @@ class _CallbackSettingState extends State<_CallbackSetting> {
);
}
}
class _SliverComicSource extends StatefulWidget {
const _SliverComicSource({
super.key,
required this.source,
required this.edit,
required this.update,
required this.delete,
});
final ComicSource source;
final void Function(ComicSource source) edit;
final void Function(ComicSource source) update;
final void Function(ComicSource source) delete;
@override
State<_SliverComicSource> createState() => _SliverComicSourceState();
}
class _SliverComicSourceState extends State<_SliverComicSource> {
ComicSource get source => widget.source;
@override
Widget build(BuildContext context) {
var newVersion = ComicSource.availableUpdates[source.key];
bool hasUpdate =
newVersion != null && compareSemVer(newVersion, source.version);
return SliverMainAxisGroup(
slivers: [
SliverPadding(padding: const EdgeInsets.only(top: 16)),
SliverToBoxAdapter(
child: ListTile(
title: Row(
children: [
Text(
source.name,
style: ts.s18,
),
const SizedBox(width: 6),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
decoration: BoxDecoration(
color: context.colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(8),
),
child: Text(
source.version,
style: const TextStyle(fontSize: 13),
),
),
if (hasUpdate)
Tooltip(
message: newVersion,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color: context.colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(8),
),
child: Text(
"New Version".tl,
style: const TextStyle(fontSize: 13),
),
),
).paddingLeft(4)
],
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Tooltip(
message: "Edit".tl,
child: IconButton(
onPressed: () => widget.edit(source),
icon: const Icon(Icons.edit_note),
),
),
Tooltip(
message: "Update".tl,
child: IconButton(
onPressed: () => widget.update(source),
icon: const Icon(Icons.update),
),
),
Tooltip(
message: "Delete".tl,
child: IconButton(
onPressed: () => widget.delete(source),
icon: const Icon(Icons.delete),
),
),
],
),
),
),
SliverToBoxAdapter(
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: context.colorScheme.outlineVariant,
width: 0.6,
),
),
),
),
),
SliverToBoxAdapter(
child: Column(
children: buildSourceSettings().toList(),
),
),
SliverToBoxAdapter(
child: Column(
children: _buildAccount().toList(),
),
),
],
);
}
Iterable<Widget> buildSourceSettings() sync* {
if (source.settings == null) {
return;
} else if (source.data['settings'] == null) {
source.data['settings'] = {};
}
for (var item in source.settings!.entries) {
var key = item.key;
String type = item.value['type'];
try {
if (type == "select") {
var current = source.data['settings'][key];
if (current == null) {
var d = item.value['default'];
for (var option in item.value['options']) {
if (option['value'] == d) {
current = option['text'] ?? option['value'];
break;
}
}
} else {
current = item.value['options']
.firstWhere((e) => e['value'] == current)['text'] ??
current;
}
yield ListTile(
title: Text((item.value['title'] as String).ts(source.key)),
trailing: Select(
current: (current as String).ts(source.key),
values: (item.value['options'] as List)
.map<String>((e) =>
((e['text'] ?? e['value']) as String).ts(source.key))
.toList(),
onTap: (i) {
source.data['settings'][key] =
item.value['options'][i]['value'];
source.saveData();
setState(() {});
},
),
);
} else if (type == "switch") {
var current = source.data['settings'][key] ?? item.value['default'];
yield ListTile(
title: Text((item.value['title'] as String).ts(source.key)),
trailing: Switch(
value: current,
onChanged: (v) {
source.data['settings'][key] = v;
source.saveData();
setState(() {});
},
),
);
} else if (type == "input") {
var current =
source.data['settings'][key] ?? item.value['default'] ?? '';
yield ListTile(
title: Text((item.value['title'] as String).ts(source.key)),
subtitle:
Text(current, maxLines: 1, overflow: TextOverflow.ellipsis),
trailing: IconButton(
icon: const Icon(Icons.edit),
onPressed: () {
showInputDialog(
context: context,
title: (item.value['title'] as String).ts(source.key),
initialValue: current,
inputValidator: item.value['validator'] == null
? null
: RegExp(item.value['validator']),
onConfirm: (value) {
source.data['settings'][key] = value;
source.saveData();
setState(() {});
return null;
},
);
},
),
);
} else if (type == "callback") {
yield _CallbackSetting(setting: item, sourceKey: source.key);
}
} catch (e, s) {
Log.error("ComicSourcePage", "Failed to build a setting\n$e\n$s");
}
}
}
final _reLogin = <String, bool>{};
Iterable<Widget> _buildAccount() sync* {
if (source.account == null) return;
final bool logged = source.isLogged;
if (!logged) {
yield ListTile(
title: Text("Log in".tl),
trailing: const Icon(Icons.arrow_right),
onTap: () async {
await context.to(
() => _LoginPage(
config: source.account!,
source: source,
),
);
source.saveData();
setState(() {});
},
);
}
if (logged) {
for (var item in source.account!.infoItems) {
if (item.builder != null) {
yield item.builder!(context);
} else {
yield ListTile(
title: Text(item.title.tl),
subtitle: item.data == null ? null : Text(item.data!()),
onTap: item.onTap,
);
}
}
if (source.data["account"] is List) {
bool loading = _reLogin[source.key] == true;
yield ListTile(
title: Text("Re-login".tl),
subtitle: Text("Click if login expired".tl),
onTap: () async {
if (source.data["account"] == null) {
context.showMessage(message: "No data".tl);
return;
}
setState(() {
_reLogin[source.key] = true;
});
final List account = source.data["account"];
var res = await source.account!.login!(account[0], account[1]);
if (res.error) {
context.showMessage(message: res.errorMessage!);
} else {
context.showMessage(message: "Success".tl);
}
setState(() {
_reLogin[source.key] = false;
});
},
trailing: loading
? const SizedBox.square(
dimension: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
),
)
: const Icon(Icons.refresh),
);
}
yield ListTile(
title: Text("Log out".tl),
onTap: () {
source.data["account"] = null;
source.account?.logout();
source.saveData();
ComicSource.notifyListeners();
setState(() {});
},
trailing: const Icon(Icons.logout),
);
}
}
}
class _LoginPage extends StatefulWidget {
const _LoginPage({required this.config, required this.source});
final AccountConfig config;
final ComicSource source;
@override
State<_LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<_LoginPage> {
String username = "";
String password = "";
bool loading = false;
final Map<String, String> _cookies = {};
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: const Appbar(
title: Text(''),
),
body: Center(
child: Container(
padding: const EdgeInsets.all(16),
constraints: const BoxConstraints(maxWidth: 400),
child: AutofillGroup(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text("Login".tl, style: const TextStyle(fontSize: 24)),
const SizedBox(height: 32),
if (widget.config.cookieFields == null)
TextField(
decoration: InputDecoration(
labelText: "Username".tl,
border: const OutlineInputBorder(),
),
enabled: widget.config.login != null,
onChanged: (s) {
username = s;
},
autofillHints: const [AutofillHints.username],
).paddingBottom(16),
if (widget.config.cookieFields == null)
TextField(
decoration: InputDecoration(
labelText: "Password".tl,
border: const OutlineInputBorder(),
),
obscureText: true,
enabled: widget.config.login != null,
onChanged: (s) {
password = s;
},
onSubmitted: (s) => login(),
autofillHints: const [AutofillHints.password],
).paddingBottom(16),
for (var field in widget.config.cookieFields ?? <String>[])
TextField(
decoration: InputDecoration(
labelText: field,
border: const OutlineInputBorder(),
),
obscureText: true,
enabled: widget.config.validateCookies != null,
onChanged: (s) {
_cookies[field] = s;
},
).paddingBottom(16),
if (widget.config.login == null &&
widget.config.cookieFields == null)
Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.error_outline),
const SizedBox(width: 8),
Text("Login with password is disabled".tl),
],
)
else
Button.filled(
isLoading: loading,
onPressed: login,
child: Text("Continue".tl),
),
const SizedBox(height: 24),
if (widget.config.loginWebsite != null)
TextButton(
onPressed: () {
if (App.isWindows || App.isLinux) {
loginWithWebview2();
} else {
loginWithWebview();
}
},
child: Text("Login with webview".tl),
),
const SizedBox(height: 8),
if (widget.config.registerWebsite != null)
TextButton(
onPressed: () =>
launchUrlString(widget.config.registerWebsite!),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.link),
const SizedBox(width: 8),
Text("Create Account".tl),
],
),
),
],
),
),
),
),
);
}
void login() {
if (widget.config.login != null) {
if (username.isEmpty || password.isEmpty) {
showToast(
message: "Cannot be empty".tl,
icon: const Icon(Icons.error_outline),
context: context,
);
return;
}
setState(() {
loading = true;
});
widget.config.login!(username, password).then((value) {
if (value.error) {
context.showMessage(message: value.errorMessage!);
setState(() {
loading = false;
});
} else {
if (mounted) {
context.pop();
}
}
});
} else if (widget.config.validateCookies != null) {
setState(() {
loading = true;
});
var cookies =
widget.config.cookieFields!.map((e) => _cookies[e] ?? '').toList();
widget.config.validateCookies!(cookies).then((value) {
if (value) {
widget.source.data['account'] = 'ok';
widget.source.saveData();
context.pop();
} else {
context.showMessage(message: "Invalid cookies".tl);
setState(() {
loading = false;
});
}
});
}
}
void loginWithWebview() async {
var url = widget.config.loginWebsite!;
var title = '';
bool success = false;
void validate(InAppWebViewController c) async {
if (widget.config.checkLoginStatus != null &&
widget.config.checkLoginStatus!(url, title)) {
var cookies = (await c.getCookies(url)) ?? [];
SingleInstanceCookieJar.instance?.saveFromResponse(
Uri.parse(url),
cookies,
);
success = true;
widget.config.onLoginWithWebviewSuccess?.call();
App.mainNavigatorKey?.currentContext?.pop();
}
}
await context.to(
() => AppWebview(
initialUrl: widget.config.loginWebsite!,
onNavigation: (u, c) {
url = u;
validate(c);
return false;
},
onTitleChange: (t, c) {
title = t;
validate(c);
},
),
);
if (success) {
widget.source.data['account'] = 'ok';
widget.source.saveData();
context.pop();
}
}
// for windows and linux
void loginWithWebview2() async {
if (!await DesktopWebview.isAvailable()) {
context.showMessage(message: "Webview is not available".tl);
}
var url = widget.config.loginWebsite!;
var title = '';
bool success = false;
void onClose() {
if (success) {
widget.source.data['account'] = 'ok';
widget.source.saveData();
context.pop();
}
}
void validate(DesktopWebview webview) async {
if (widget.config.checkLoginStatus != null &&
widget.config.checkLoginStatus!(url, title)) {
var cookiesMap = await webview.getCookies(url);
var cookies = <io.Cookie>[];
cookiesMap.forEach((key, value) {
cookies.add(io.Cookie(key, value));
});
SingleInstanceCookieJar.instance?.saveFromResponse(
Uri.parse(url),
cookies,
);
success = true;
widget.config.onLoginWithWebviewSuccess?.call();
webview.close();
onClose();
}
}
var webview = DesktopWebview(
initialUrl: widget.config.loginWebsite!,
onTitleChange: (t, webview) {
title = t;
validate(webview);
},
onNavigation: (u, webview) {
url = u;
validate(webview);
},
onClose: onClose,
);
webview.open();
}
}

View File

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

View File

@@ -46,6 +46,7 @@ class _DownloadingPageState extends State<DownloadingPage> {
i--;
return _DownloadTaskTile(
key: ValueKey(LocalManager().downloadingTasks[i]),
task: LocalManager().downloadingTasks[i],
);
},
@@ -120,7 +121,7 @@ class _DownloadingPageState extends State<DownloadingPage> {
}
class _DownloadTaskTile extends StatefulWidget {
const _DownloadTaskTile({required this.task});
const _DownloadTaskTile({required this.task, super.key});
final DownloadTask task;
@@ -129,20 +130,33 @@ class _DownloadTaskTile extends StatefulWidget {
}
class _DownloadTaskTileState extends State<_DownloadTaskTile> {
late DownloadTask task;
@override
void initState() {
widget.task.addListener(update);
task = widget.task;
task.addListener(update);
super.initState();
}
@override
void dispose() {
widget.task.removeListener(update);
task.removeListener(update);
super.dispose();
}
@override
void didUpdateWidget(covariant _DownloadTaskTile oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.task != widget.task) {
task.removeListener(update);
task = widget.task;
task.addListener(update);
}
}
void update() {
context.findAncestorStateOfType<_DownloadingPageState>()?.update();
setState(() {});
}
@override

View File

@@ -16,6 +16,7 @@ import 'package:venera/foundation/res.dart';
import 'package:venera/network/download.dart';
import 'package:venera/pages/comic_page.dart';
import 'package:venera/pages/reader/reader.dart';
import 'package:venera/pages/settings/settings_page.dart';
import 'package:venera/utils/io.dart';
import 'package:venera/utils/translations.dart';

View File

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

View File

@@ -9,7 +9,6 @@ import 'package:venera/foundation/favorites.dart';
import 'package:venera/foundation/history.dart';
import 'package:venera/foundation/local.dart';
import 'package:venera/foundation/log.dart';
import 'package:venera/pages/accounts_page.dart';
import 'package:venera/pages/comic_page.dart';
import 'package:venera/pages/comic_source_page.dart';
import 'package:venera/pages/downloading_page.dart';
@@ -36,7 +35,6 @@ class HomePage extends StatelessWidget {
const _History(),
const _Local(),
const _ComicSourceWidget(),
const _AccountsWidget(),
const ImageFavorites(),
SliverPadding(padding: EdgeInsets.only(top: context.padding.bottom)),
],
@@ -698,115 +696,6 @@ class _ComicSourceWidgetState extends State<_ComicSourceWidget> {
}
}
class _AccountsWidget extends StatefulWidget {
const _AccountsWidget();
@override
State<_AccountsWidget> createState() => _AccountsWidgetState();
}
class _AccountsWidgetState extends State<_AccountsWidget> {
late List<String> accounts;
void onComicSourceChange() {
setState(() {
accounts.clear();
for (var c in ComicSource.all()) {
if (c.isLogged) {
accounts.add(c.name);
}
}
});
}
@override
void initState() {
accounts = [];
for (var c in ComicSource.all()) {
if (c.isLogged) {
accounts.add(c.name);
}
}
ComicSource.addListener(onComicSourceChange);
super.initState();
}
@override
void dispose() {
ComicSource.removeListener(onComicSourceChange);
super.dispose();
}
@override
Widget build(BuildContext context) {
return SliverToBoxAdapter(
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.outlineVariant,
width: 0.6,
),
borderRadius: BorderRadius.circular(8),
),
child: InkWell(
borderRadius: BorderRadius.circular(8),
onTap: () {
context.to(() => const AccountsPage());
},
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
height: 56,
child: Row(
children: [
Center(
child: Text('Accounts'.tl, style: ts.s18),
),
Container(
margin: const EdgeInsets.symmetric(horizontal: 8),
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.secondaryContainer,
borderRadius: BorderRadius.circular(8),
),
child: Text(accounts.length.toString(), style: ts.s12),
),
const Spacer(),
const Icon(Icons.arrow_right),
],
),
).paddingHorizontal(16),
SizedBox(
width: double.infinity,
child: Wrap(
runSpacing: 8,
spacing: 8,
children: accounts.map((e) {
return Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.secondaryContainer,
borderRadius: BorderRadius.circular(8),
),
child: Text(e),
);
}).toList(),
).paddingHorizontal(16).paddingBottom(16),
),
],
),
),
),
);
}
}
class _AnimatedDownloadingIcon extends StatefulWidget {
const _AnimatedDownloadingIcon();

View File

@@ -12,6 +12,7 @@ import 'package:venera/utils/epub.dart';
import 'package:venera/utils/io.dart';
import 'package:venera/utils/pdf.dart';
import 'package:venera/utils/translations.dart';
import 'package:zip_flutter/zip_flutter.dart';
class LocalComicsPage extends StatefulWidget {
const LocalComicsPage({super.key});
@@ -147,13 +148,13 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
text: "View Detail".tl,
onClick: () {
context.to(() => ComicPage(
id: selectedComics.keys.first.id,
sourceKey: selectedComics.keys.first.sourceKey,
));
id: selectedComics.keys.first.id,
sourceKey: selectedComics.keys.first.sourceKey,
));
},
),
if (selectedComics.length == 1)
...exportActions(selectedComics.keys.first),
if (selectedComics.isNotEmpty)
...exportActions(selectedComics.keys.toList()),
]);
}
@@ -322,7 +323,7 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
});
},
),
...exportActions(c as LocalComic),
...exportActions([c as LocalComic]),
];
},
),
@@ -390,79 +391,102 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
return isDeleted;
}
List<MenuEntry> exportActions(LocalComic c) {
List<MenuEntry> exportActions(List<LocalComic> comics) {
return [
MenuEntry(
icon: Icons.outbox_outlined,
text: "Export as cbz".tl,
onClick: () async {
var controller = showLoadingDialog(
context,
allowCancel: false,
);
try {
var file = await CBZ.export(c);
await saveFile(filename: file.name, file: file);
await file.delete();
} catch (e, s) {
context.showMessage(message: e.toString());
Log.error("CBZ Export", e, s);
}
controller.close();
}),
icon: Icons.outbox_outlined,
text: "Export as cbz".tl,
onClick: () {
exportComics(comics, CBZ.export, ".cbz");
},
),
MenuEntry(
icon: Icons.picture_as_pdf_outlined,
text: "Export as pdf".tl,
onClick: () async {
var cache = FilePath.join(App.cachePath, 'temp.pdf');
var controller = showLoadingDialog(
context,
allowCancel: false,
);
try {
await createPdfFromComicIsolate(
comic: c,
savePath: cache,
);
await saveFile(
file: File(cache),
filename: "${c.title}.pdf",
);
} catch (e, s) {
Log.error("PDF Export", e, s);
context.showMessage(message: e.toString());
} finally {
controller.close();
File(cache).deleteIgnoreError();
}
exportComics(comics, createPdfFromComicIsolate, ".pdf");
},
),
MenuEntry(
icon: Icons.import_contacts_outlined,
text: "Export as epub".tl,
onClick: () async {
var controller = showLoadingDialog(
context,
allowCancel: false,
);
File? file;
try {
file = await createEpubWithLocalComic(
c,
);
await saveFile(
file: file,
filename: "${c.title}.epub",
);
} catch (e, s) {
Log.error("EPUB Export", e, s);
context.showMessage(message: e.toString());
} finally {
controller.close();
file?.deleteIgnoreError();
}
exportComics(comics, createEpubWithLocalComic, ".epub");
},
)
];
}
/// Export given comics to a file
void exportComics(
List<LocalComic> comics, ExportComicFunc export, String ext) async {
var current = 0;
var cacheDir = FilePath.join(App.cachePath, 'comics_export');
var outFile = FilePath.join(App.cachePath, 'comics_export.zip');
bool canceled = false;
if (Directory(cacheDir).existsSync()) {
Directory(cacheDir).deleteSync(recursive: true);
}
Directory(cacheDir).createSync();
var loadingController = showLoadingDialog(
context,
allowCancel: true,
message: "${"Exporting".tl} $current/${comics.length}",
withProgress: comics.length > 1,
onCancel: () {
canceled = true;
},
);
try {
var fileName = "";
// For each comic, export it to a file
for (var comic in comics) {
fileName = FilePath.join(cacheDir, sanitizeFileName(comic.title) + ext);
await export(comic, fileName);
current++;
if (comics.length > 1) {
loadingController
.setMessage("${"Exporting".tl} $current/${comics.length}");
loadingController.setProgress(current / comics.length);
}
if (canceled) {
return;
}
}
// For single comic, just save the file
if (comics.length == 1) {
await saveFile(
file: File(fileName),
filename: File(fileName).name,
);
Directory(cacheDir).deleteSync(recursive: true);
loadingController.close();
return;
}
// For multiple comics, compress the folder
loadingController.setProgress(null);
loadingController.setMessage("Compressing".tl);
await ZipFile.compressFolderAsync(cacheDir, outFile);
if (canceled) {
File(outFile).deleteIgnoreError();
return;
}
} catch (e, s) {
Log.error("Export Comics", e, s);
context.showMessage(message: e.toString());
loadingController.close();
return;
} finally {
Directory(cacheDir).deleteIgnoreError(recursive: true);
}
await saveFile(
file: File(outFile),
filename: "comics_export.zip",
);
loadingController.close();
File(outFile).deleteIgnoreError();
}
}
typedef ExportComicFunc = Future<File> Function(
LocalComic comic, String outFilePath);

View File

@@ -111,9 +111,7 @@ class _GalleryModeState extends State<_GalleryMode>
late _ReaderState reader;
int get totalPages => ((reader.images!.length + reader.imagesPerPage - 1) /
reader.imagesPerPage)
.ceil();
int get totalPages => (reader.images!.length / reader.imagesPerPage).ceil();
@override
void initState() {
@@ -228,6 +226,8 @@ class _GalleryModeState extends State<_GalleryMode>
? Axis.vertical
: Axis.horizontal;
bool reverse = reader.mode == ReaderMode.galleryRightToLeft;
List<Widget> imageWidgets = images.map((imageKey) {
ImageProvider imageProvider =
_createImageProviderFromKey(imageKey, context);
@@ -239,6 +239,10 @@ class _GalleryModeState extends State<_GalleryMode>
);
}).toList();
if (reverse) {
imageWidgets = imageWidgets.reversed.toList();
}
return axis == Axis.vertical
? Column(children: imageWidgets)
: Row(children: imageWidgets);

View File

@@ -98,8 +98,7 @@ class _ReaderState extends State<Reader> with _ReaderLocation, _ReaderWindow {
}
@override
int get maxPage =>
((images?.length ?? 1) + imagesPerPage - 1) ~/ imagesPerPage;
int get maxPage => ((images?.length ?? 1) / imagesPerPage).ceil();
ComicType get type => widget.type;

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

View File

@@ -116,13 +116,13 @@ class _SearchResultPageState extends State<SearchResultPage> {
@override
void initState() {
sourceKey = widget.sourceKey;
text = checkAutoLanguage(widget.text);
controller = SearchBarController(
currentText: checkAutoLanguage(widget.text),
currentText: text,
onSearch: search,
);
options = widget.options ?? const [];
validateOptions();
text = widget.text;
appdata.addSearchHistory(text);
suggestionsController = _SuggestionsController(controller);
super.initState();

View File

@@ -86,7 +86,7 @@ class _AboutSettingsState extends State<AboutSettings> {
Future<bool> checkUpdate() async {
var res = await AppDio().get(
"https://raw.githubusercontent.com/venera-app/venera/refs/heads/master/pubspec.yaml");
"https://cdn.jsdelivr.net/gh/venera-app/venera@latest/pubspec.yaml");
if (res.statusCode == 200) {
var data = loadYaml(res.data);
if (data["version"] != null) {

View File

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

@@ -206,37 +206,41 @@ class _SettingsPageState extends State<SettingsPage> implements PopEntry {
],
);
} else {
return Stack(
children: [
Positioned.fill(child: buildLeft()),
Positioned(
left: offset,
width: MediaQuery.of(context).size.width,
top: 0,
bottom: 0,
child: Listener(
onPointerDown: handlePointerDown,
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
switchInCurve: Curves.fastOutSlowIn,
switchOutCurve: Curves.fastOutSlowIn,
transitionBuilder: (child, animation) {
var tween = Tween<Offset>(
begin: const Offset(1, 0), end: const Offset(0, 0));
return LayoutBuilder(
builder: (context, constrains) {
return Stack(
children: [
Positioned.fill(child: buildLeft()),
Positioned(
left: offset,
width: constrains.maxWidth,
top: 0,
bottom: 0,
child: Listener(
onPointerDown: handlePointerDown,
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
switchInCurve: Curves.fastOutSlowIn,
switchOutCurve: Curves.fastOutSlowIn,
transitionBuilder: (child, animation) {
var tween = Tween<Offset>(
begin: const Offset(1, 0), end: const Offset(0, 0));
return SlideTransition(
position: tween.animate(animation),
child: child,
);
},
child: Material(
key: ValueKey(currentPage),
child: buildRight(),
return SlideTransition(
position: tween.animate(animation),
child: child,
);
},
child: Material(
key: ValueKey(currentPage),
child: buildRight(),
),
),
),
),
),
)
],
)
],
);
},
);
}
}

View File

@@ -303,7 +303,10 @@ class DesktopWebview {
proxy: AppDio.proxy,
));
_webview!.addOnWebMessageReceivedCallback(onMessage);
_webview!.setOnNavigation((s) => onNavigation?.call(s, this));
_webview!.setOnNavigation((s) {
s = s.substring(1, s.length - 1);
return onNavigation?.call(s, this);
});
_webview!.launch(initialUrl, triggerOnUrlRequestEvent: false);
_runTimer();
_webview!.onClose.then((value) {

View File

@@ -85,6 +85,10 @@ abstract class CBZ {
if (cache.existsSync()) cache.deleteSync(recursive: true);
cache.createSync();
await extractArchive(file, cache);
var f = cache.listSync();
if (f.length == 1 && f.first is Directory) {
cache = f.first as Directory;
}
var metaDataFile = File(FilePath.join(cache.path, 'metadata.json'));
ComicMetaData? metaData;
if (metaDataFile.existsSync()) {
@@ -171,7 +175,7 @@ abstract class CBZ {
return comic;
}
static Future<File> export(LocalComic comic) async {
static Future<File> export(LocalComic comic, String outFilePath) async {
var cache = Directory(FilePath.join(App.cachePath, 'cbz_export'));
if (cache.existsSync()) cache.deleteSync(recursive: true);
cache.createSync();
@@ -230,7 +234,7 @@ abstract class CBZ {
).toJson(),
),
);
var cbz = File(FilePath.join(App.cachePath, sanitizeFileName('${comic.title}.cbz')));
var cbz = File(outFilePath);
if (cbz.existsSync()) cbz.deleteSync();
await _compress(cache.path, cbz.path);
cache.deleteSync(recursive: true);

View File

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

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

View File

@@ -35,19 +35,9 @@ class FilePath {
}
extension FileSystemEntityExt on FileSystemEntity {
/// Get the base name of the file or directory.
String get name {
var path = this.path;
if (path.endsWith('/') || path.endsWith('\\')) {
path = path.substring(0, path.length - 1);
}
int i = path.length - 1;
while (i >= 0 && path[i] != '\\' && path[i] != '/') {
i--;
}
return path.substring(i + 1);
return p.basename(path);
}
Future<void> deleteIgnoreError({bool recursive = false}) async {
@@ -83,6 +73,10 @@ extension FileExtension on File {
// Stream is not usable since [AndroidFile] does not support [openRead].
await newFile.writeAsBytes(await readAsBytes());
}
String get basenameWithoutExt {
return p.basenameWithoutExtension(path);
}
}
extension DirectoryExtension on Directory {

View File

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

View File

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

View File

@@ -1083,21 +1083,21 @@ packages:
source: hosted
version: "6.5.0"
yaml:
dependency: transitive
dependency: "direct main"
description:
name: yaml
sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5"
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
source: hosted
version: "3.1.2"
version: "3.1.3"
zip_flutter:
dependency: "direct main"
description:
name: zip_flutter
sha256: fe63ef9098bb2426b001adba2e28029820d71ce80cce957a36676bd6b3227245
sha256: bbf3160062610a43901b7ebbc6f6dd46519540f03a84027dc7b1fff399dda1ac
url: "https://pub.dev"
source: hosted
version: "0.0.9"
version: "0.0.10"
sdks:
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."
publish_to: 'none'
version: 1.2.2+122
version: 1.2.4+124
environment:
sdk: '>=3.6.0 <4.0.0'
flutter: 3.27.3
flutter: 3.27.4
dependencies:
flutter:
@@ -48,7 +48,7 @@ dependencies:
sliver_tools: ^0.2.12
flutter_file_dialog: ^3.0.2
file_selector: ^1.0.3
zip_flutter: ^0.0.9
zip_flutter: ^0.0.10
lodepng_flutter:
git:
url: https://github.com/venera-app/lodepng_flutter
@@ -75,13 +75,14 @@ dependencies:
flex_seed_scheme: ^3.5.0
flutter_localizations:
sdk: flutter
yaml: ^3.1.3
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
flutter_to_arch: ^1.0.1
flutter_to_debian:
flutter_to_debian: ^2.0.2
flutter:
uses-material-design: true

View File

@@ -3,11 +3,36 @@
#define MyAppName "Venera"
#define MyAppVersion "{{version}}"
#define MyAppPublisher "wgh136"
#define MyAppPublisher "nyne"
#define MyAppURL "https://github.com/venera-app/venera"
#define MyAppExeName "venera.exe"
#define RootPath "{{root_path}}"
[Code]
procedure CurStepChanged(CurStep: TSetupStep);
var
OldVersionPath, ShortcutPath: string;
begin
if CurStep = ssInstall then
begin
OldVersionPath := 'C:\Program Files (x86)\Venera';
if DirExists(OldVersionPath) then
begin
DelTree(OldVersionPath, True, True, True);
ShortcutPath := GetEnv('USERPROFILE') + '\Desktop\Venera.lnk';
if FileExists(ShortcutPath) then
begin
DeleteFile(ShortcutPath);
end;
ShortcutPath := 'C:\Users\Public\Desktop\Venera.lnk';
if FileExists(ShortcutPath) then
begin
DeleteFile(ShortcutPath);
end;
end;
end;
end;
[Setup]
; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
@@ -30,6 +55,8 @@ SetupIconFile={#RootPath}\windows\runner\resources\app_icon.ico
Compression=lzma
SolidCompression=yes
WizardStyle=modern
ArchitecturesInstallIn64BitMode=x64compatible
ArchitecturesAllowed=x64compatible
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"

View File

@@ -29,7 +29,7 @@ file.close()
if not os.path.exists("windows/ChineseSimplified.isl"):
# download ChineseSimplified.isl
url = "https://raw.githubusercontent.com/kira-96/Inno-Setup-Chinese-Simplified-Translation/refs/heads/main/ChineseSimplified.isl"
url = "https://cdn.jsdelivr.net/gh/kira-96/Inno-Setup-Chinese-Simplified-Translation@latest/ChineseSimplified.isl"
response = httpx.get(url)
with open('windows/ChineseSimplified.isl', 'wb') as file:
file.write(response.content)