23 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
32 changed files with 654 additions and 345 deletions

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

@@ -321,7 +321,10 @@
"Manage": "管理", "Manage": "管理",
"Verify": "验证", "Verify": "验证",
"Cloudflare verification required": "需要Cloudflare验证", "Cloudflare verification required": "需要Cloudflare验证",
"Success": "成功" "Success": "成功",
"Compressing": "压缩中",
"Exporting": "导出中",
"Search Sources": "搜索源"
}, },
"zh_TW": { "zh_TW": {
"Home": "首頁", "Home": "首頁",
@@ -645,6 +648,9 @@
"Manage": "管理", "Manage": "管理",
"Verify": "驗證", "Verify": "驗證",
"Cloudflare verification required": "需要Cloudflare驗證", "Cloudflare verification required": "需要Cloudflare驗證",
"Success": "成功" "Success": "成功",
"Compressing": "壓縮中",
"Exporting": "匯出中",
"Search Sources": "搜索源"
} }
} }

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);
@@ -1520,14 +1519,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.4";
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': [],
@@ -155,7 +156,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

@@ -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

@@ -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,

View File

@@ -206,62 +206,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 +356,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 +484,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: [
@@ -1388,42 +1390,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(),
],
), ),
], ),
), ],
), ),
); );
} }
@@ -1850,7 +1877,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

@@ -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

@@ -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

@@ -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

@@ -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.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 @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();

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

@@ -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()) {
@@ -171,7 +175,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 +234,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

@@ -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

@@ -1100,4 +1100,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.4+124
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:
@@ -82,7 +82,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)