19 Commits

Author SHA1 Message Date
nyne
7fa48cec29 Merge pull request #515 from venera-app/v1.5.1-dev
V1.5.1
2025-09-14 18:56:12 +08:00
e549a18dbf flutter 3.35.3 2025-09-14 18:54:26 +08:00
c17c4abb5b Reduce size of scroll bar. 2025-09-14 18:43:11 +08:00
af57bc31b1 Update version code. 2025-09-14 18:33:19 +08:00
16449a1440 Change page transition animation for Android. 2025-09-14 18:30:54 +08:00
a7c1983f35 Fallback to local cover if loading fails for favorite comic. 2025-09-14 17:19:23 +08:00
4c257d7178 Show read button if loading fails. 2025-09-14 17:05:45 +08:00
3a9d634edf Update android build script. 2025-09-14 10:21:14 +08:00
nyne
e179c8f67f Change padding check condition for Android platform (#503) 2025-09-05 17:52:33 +08:00
nyne
c4b85471c1 Merge pull request #499 from KarlZeo/fix-ios-padding-check
fix padding check error on ios
2025-09-05 17:42:49 +08:00
KarlZeo
a898b57d96 fix padding check error on ios 2025-09-04 20:04:28 +08:00
50c6bec4cd Disable minify 2025-09-04 00:30:01 +08:00
nyne
8c44f83d6c Update Xcode version in GitHub Actions workflow 2025-09-03 22:50:32 +08:00
nyne
103b6b2832 Merge pull request #497 from venera-app/v1.5.0-dev
V1.5.0
2025-09-03 22:12:00 +08:00
4129349c70 Improve js api onResponse 2025-09-03 22:09:07 +08:00
77a9aa5457 Update version code. 2025-09-03 22:05:04 +08:00
97940b9492 Refactor category options. 2025-09-03 22:03:54 +08:00
7945c0e54f Improve compute api. 2025-09-03 20:31:42 +08:00
ᡠᠵᡠᡳ ᡠᠵᡠ ᠮᠠᠨᡩ᠋ᠠᠨ
d0b76de465 Use badge from shields.io (#455)
* Use badge from shields.io

* AUR
2025-09-01 20:55:45 +08:00
21 changed files with 704 additions and 412 deletions

View File

@@ -15,7 +15,7 @@ jobs:
channel: "stable"
flutter-version-file: pubspec.yaml
architecture: x64
- run: sudo xcode-select --switch /Applications/Xcode_16.0.app
- run: sudo xcode-select --switch /Applications/Xcode_16.4.app
- run: flutter pub get
# Step 1: Decode and install the certificate
- name: Decode and install certificate
@@ -63,7 +63,7 @@ jobs:
channel: "stable"
flutter-version-file: pubspec.yaml
architecture: x64
- run: sudo xcode-select --switch /Applications/Xcode_16.0.app
- run: sudo xcode-select --switch /Applications/Xcode_16.4.app
- run: flutter pub get
- run: flutter build ios --release --no-codesign
- run: |

View File

@@ -1,15 +1,14 @@
# 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)
[![stars](https://img.shields.io/github/stars/venera-app/venera?style=flat)](https://github.com/venera-app/venera/stargazers)
[![Telegram](https://img.shields.io/badge/Telegram-2CA5E0?style=flat&logo=telegram&logoColor=white)](https://t.me/venera_release)
A comic reader that support reading local and network comics.
[![Download](https://img.shields.io/github/v/release/venera-app/venera)](https://github.com/venera-app/venera/releases)
[![AUR Version](https://img.shields.io/aur/version/venera-bin)](https://aur.archlinux.org/packages/venera-bin)
[![F-Droid Version](https://img.shields.io/f-droid/v/com.github.wgh136.venera)](https://f-droid.org/packages/com.github.wgh136.venera/)
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
alt="Get it on F-Droid"
height="75">](https://f-droid.org/packages/com.github.wgh136.venera/)
A comic reader that support reading local and network comics.
## Features
- Read local comics

View File

@@ -34,6 +34,12 @@ android {
compileSdk = flutter.compileSdkVersion
ndkVersion "28.0.13004108"
packaging {
jniLibs {
useLegacyPackaging true
}
}
splits{
abi {
reset()
@@ -78,6 +84,9 @@ android {
buildTypes {
release {
// Temporarily solution to fix crash
minifyEnabled false
shrinkResources false
ndk {
abiFilters "armeabi-v7a", "arm64-v8a", "x86_64"
}

View File

@@ -16,6 +16,7 @@
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:enableOnBackInvokedCallback="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
@@ -58,8 +59,6 @@
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<!-- [flutter 3.27.1] Impeller is still worse than skia, disable it -->
<meta-data android:name="io.flutter.embedding.android.EnableImpeller" android:value="false"/>
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and

View File

@@ -19,7 +19,7 @@ pluginManagement {
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version '8.9.0' apply false
id "org.jetbrains.kotlin.android" version "1.8.10" apply false
id "org.jetbrains.kotlin.android" version "2.1.0" apply false
}
include ":app"

View File

@@ -1428,11 +1428,11 @@ function getClipboard() {
/**
* Compute a function with arguments. The function will be executed in the engine pool which is not in the main thread.
* @param func {string} - A js code string which can be evaluated to a function. The function will receive the args as its only argument.
* @param args {any[] | null | undefined} - The arguments to pass to the function.
* @param args {any[]} - The arguments to pass to the function.
* @returns {Promise<any>} - The result of the function.
* @since 1.5.0
*/
function compute(func, args) {
function compute(func, ...args) {
return sendMessage({
method: 'compute',
function: func,

View File

@@ -17,6 +17,7 @@ ImageProvider? _findImageProvider(Comic comic) {
comic.cover,
sourceKey: comic.sourceKey,
cid: comic.id,
fallbackToLocalCover: comic is FavoriteItem,
);
}
return image;

View File

@@ -7,6 +7,7 @@ class NetworkError extends StatelessWidget {
this.retry,
this.withAppbar = true,
this.buttonText,
this.action,
});
final String message;
@@ -17,6 +18,8 @@ class NetworkError extends StatelessWidget {
final String? buttonText;
final Widget? action;
@override
Widget build(BuildContext context) {
var cfe = CloudflareException.fromString(message);
@@ -67,12 +70,19 @@ class NetworkError extends StatelessWidget {
child: Text('Verify'.tl),
)
else
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (action != null)
action!.paddingRight(8),
FilledButton(
onPressed: retry,
child: Text(buttonText ?? 'Retry'.tl),
),
],
),
],
),
);
if (withAppbar) {
body = Column(

View File

@@ -7,8 +7,11 @@ class PaneItemEntry {
IconData activeIcon;
PaneItemEntry(
{required this.label, required this.icon, required this.activeIcon});
PaneItemEntry({
required this.label,
required this.icon,
required this.activeIcon,
});
}
class PaneActionEntry {
@@ -18,20 +21,24 @@ class PaneActionEntry {
VoidCallback onTap;
PaneActionEntry(
{required this.label, required this.icon, required this.onTap});
PaneActionEntry({
required this.label,
required this.icon,
required this.onTap,
});
}
class NaviPane extends StatefulWidget {
const NaviPane(
{required this.paneItems,
const NaviPane({
required this.paneItems,
required this.paneActions,
required this.pageBuilder,
this.initialPage = 0,
this.onPageChanged,
required this.observer,
required this.navigatorKey,
super.key});
super.key,
});
final List<PaneItemEntry> paneItems;
@@ -187,7 +194,8 @@ class NaviPaneState extends State<NaviPane>
child: buildLeft(),
),
Positioned.fill(
left: _kFoldedSideBarWidth * ((value - 1).clamp(0, 1)) +
left:
_kFoldedSideBarWidth * ((value - 1).clamp(0, 1)) +
(_kSideBarWidth - _kFoldedSideBarWidth) *
((value - 2).clamp(0, 1)),
child: buildMainView(),
@@ -202,6 +210,10 @@ class NaviPaneState extends State<NaviPane>
Widget buildMainView() {
return HeroControllerScope(
controller: MaterialApp.createMaterialHeroController(),
child: NavigatorPopHandler(
onPopWithResult: (result) {
widget.navigatorKey.currentState?.maybePop(result);
},
child: Navigator(
observers: [widget.observer],
key: widget.navigatorKey,
@@ -212,6 +224,7 @@ class NaviPaneState extends State<NaviPane>
},
),
),
),
);
}
@@ -239,7 +252,7 @@ class NaviPaneState extends State<NaviPane>
icon: Icon(action.icon),
onPressed: action.onTap,
),
)
),
],
),
),
@@ -261,9 +274,7 @@ class NaviPaneState extends State<NaviPane>
),
),
child: Row(
children: List<Widget>.generate(
widget.paneItems.length,
(index) {
children: List<Widget>.generate(widget.paneItems.length, (index) {
return Expanded(
child: _SingleBottomNaviWidget(
enabled: currentPage == index,
@@ -274,8 +285,7 @@ class NaviPaneState extends State<NaviPane>
key: ValueKey(index),
),
);
},
),
}),
),
),
);
@@ -286,7 +296,8 @@ class NaviPaneState extends State<NaviPane>
const paddingHorizontal = 12.0;
return Material(
child: Container(
width: _kFoldedSideBarWidth +
width:
_kFoldedSideBarWidth +
(_kSideBarWidth - _kFoldedSideBarWidth) * ((value - 2).clamp(0, 1)),
height: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: paddingHorizontal),
@@ -323,9 +334,7 @@ class NaviPaneState extends State<NaviPane>
key: ValueKey(index + widget.paneItems.length),
),
),
const SizedBox(
height: 16,
)
const SizedBox(height: 16),
],
),
),
@@ -334,12 +343,13 @@ class NaviPaneState extends State<NaviPane>
}
class _SideNaviWidget extends StatelessWidget {
const _SideNaviWidget(
{required this.enabled,
const _SideNaviWidget({
required this.enabled,
required this.entry,
required this.onTap,
required this.showTitle,
super.key});
super.key,
});
final bool enabled;
@@ -368,18 +378,18 @@ class _SideNaviWidget extends StatelessWidget {
? Row(
children: [icon, const SizedBox(width: 12), Text(entry.label)],
)
: Align(
alignment: Alignment.centerLeft,
child: icon,
),
: Align(alignment: Alignment.centerLeft, child: icon),
),
).paddingVertical(4);
}
}
class _PaneActionWidget extends StatelessWidget {
const _PaneActionWidget(
{required this.entry, required this.showTitle, super.key});
const _PaneActionWidget({
required this.entry,
required this.showTitle,
super.key,
});
final PaneActionEntry entry;
@@ -399,21 +409,19 @@ class _PaneActionWidget extends StatelessWidget {
? Row(
children: [icon, const SizedBox(width: 12), Text(entry.label)],
)
: Align(
alignment: Alignment.centerLeft,
child: icon,
),
: Align(alignment: Alignment.centerLeft, child: icon),
),
).paddingVertical(4);
}
}
class _SingleBottomNaviWidget extends StatefulWidget {
const _SingleBottomNaviWidget(
{required this.enabled,
const _SingleBottomNaviWidget({
required this.enabled,
required this.entry,
required this.onTap,
super.key});
super.key,
});
final bool enabled;
@@ -482,8 +490,9 @@ class _SingleBottomNaviWidgetState extends State<_SingleBottomNaviWidget>
Widget buildContent() {
final value = controller.value;
final colorScheme = Theme.of(context).colorScheme;
final icon =
Icon(widget.enabled ? widget.entry.activeIcon : widget.entry.icon);
final icon = Icon(
widget.enabled ? widget.entry.activeIcon : widget.entry.icon,
);
return Center(
child: Container(
width: 64,
@@ -570,8 +579,11 @@ class NaviObserver extends NavigatorObserver implements Listenable {
}
class _NaviPopScope extends StatelessWidget {
const _NaviPopScope(
{required this.child, this.popGesture = false, required this.action});
const _NaviPopScope({
required this.child,
this.popGesture = false,
required this.action,
});
final Widget child;
final bool popGesture;
@@ -581,15 +593,7 @@ class _NaviPopScope extends StatelessWidget {
@override
Widget build(BuildContext context) {
Widget res = App.isIOS
? child
: PopScope(
canPop: App.isAndroid ? false : true,
onPopInvokedWithResult: (value, result) {
action();
},
child: child,
);
Widget res = child;
if (popGesture) {
res = GestureDetector(
onPanStart: (details) {
@@ -606,7 +610,8 @@ class _NaviPopScope extends StatelessWidget {
}
panStartAtEdge = false;
},
child: res);
child: res,
);
}
return res;
}

View File

@@ -237,7 +237,7 @@ class _AppScrollBarState extends State<AppScrollBar> {
double viewHeight = 0;
final _scrollIndicatorSize = App.isDesktop ? 42.0 : 64.0;
final _scrollIndicatorSize = App.isDesktop ? 36.0 : 54.0;
late final VerticalDragGestureRecognizer _dragGestureRecognizer;
@@ -354,7 +354,7 @@ class _ScrollIndicatorPainter extends CustomPainter {
Offset(size.width, 0),
radius: Radius.circular(size.width),
);
canvas.drawShadow(path, shadowColor, 4, true);
canvas.drawShadow(path, shadowColor, 2, true);
var backgroundPaint = Paint()
..color = backgroundColor
..style = PaintingStyle.fill;

View File

@@ -13,7 +13,7 @@ export "widget_utils.dart";
export "context.dart";
class _App {
final version = "1.4.6";
final version = "1.5.1";
bool get isAndroid => Platform.isAndroid;

View File

@@ -2,6 +2,7 @@ import 'dart:math';
import 'dart:ui';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:venera/foundation/app.dart';
const double _kBackGestureWidth = 20.0;
const int _kMaxDroppedSwipePageForwardAnimationTime = 800;
@@ -115,7 +116,14 @@ mixin _AppRouteTransitionMixin<T> on PageRoute<T> {
@override
Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
return SlidePageTransitionBuilder().buildTransitions(
PageTransitionsBuilder builder;
if (App.isAndroid) {
builder = PredictiveBackPageTransitionsBuilder();
} else {
builder = SlidePageTransitionBuilder();
}
return builder.buildTransitions(
this,
context,
animation,

View File

@@ -401,9 +401,14 @@ class SearchOptions {
typedef CategoryComicsLoader = Future<Res<List<Comic>>> Function(
String category, String? param, List<String> options, int page);
typedef CategoryOptionsLoader = Future<Res<List<CategoryComicsOptions>>> Function(
String category, String? param);
class CategoryComicsData {
/// options
final List<CategoryComicsOptions> options;
final List<CategoryComicsOptions>? options;
final CategoryOptionsLoader? optionsLoader;
/// [category] is the one clicked by the user on the category page.
///
@@ -414,7 +419,7 @@ class CategoryComicsData {
final RankingData? rankingData;
const CategoryComicsData(this.options, this.load, {this.rankingData});
const CategoryComicsData({this.options, this.optionsLoader, required this.load, this.rankingData});
}
class RankingData {
@@ -429,6 +434,9 @@ class RankingData {
}
class CategoryComicsOptions {
// The label will not be displayed if it is empty.
final String label;
/// Use a [LinkedHashMap] to describe an option list.
/// key is for loading comics, value is the name displayed on screen.
/// Default value will be the first of the Map.
@@ -439,7 +447,7 @@ class CategoryComicsOptions {
final List<String>? showWhen;
const CategoryComicsOptions(this.options, this.notShowWhen, this.showWhen);
const CategoryComicsOptions(this.label, this.options, this.notShowWhen, this.showWhen);
}
class LinkHandler {

View File

@@ -64,8 +64,13 @@ class ComicSourceParser {
if (file.existsSync()) {
int i = 0;
while (file.existsSync()) {
file = File(FilePath.join(App.dataPath, "comic_source",
"${fileName.split('.').first}($i).js"));
file = File(
FilePath.join(
App.dataPath,
"comic_source",
"${fileName.split('.').first}($i).js",
),
);
i++;
}
}
@@ -80,8 +85,9 @@ class ComicSourceParser {
Future<ComicSource> parse(String js, String filePath) async {
js = js.replaceAll("\r\n", "\n");
var line1 =
js.split('\n').firstWhereOrNull((e) => e.trim().startsWith("class "));
var line1 = js
.split('\n')
.firstWhereOrNull((e) => e.trim().startsWith("class "));
if (line1 == null ||
!line1.startsWith("class ") ||
!line1.contains("extends ComicSource")) {
@@ -93,19 +99,23 @@ class ComicSourceParser {
this['temp'] = new $className()
}).call()
""", className);
_name = JsEngine().runCode("this['temp'].name") ??
_name =
JsEngine().runCode("this['temp'].name") ??
(throw ComicSourceParseException('name is required'));
var key = JsEngine().runCode("this['temp'].key") ??
var key =
JsEngine().runCode("this['temp'].key") ??
(throw ComicSourceParseException('key is required'));
var version = JsEngine().runCode("this['temp'].version") ??
var version =
JsEngine().runCode("this['temp'].version") ??
(throw ComicSourceParseException('version is required'));
var minAppVersion = JsEngine().runCode("this['temp'].minAppVersion");
var url = JsEngine().runCode("this['temp'].url");
if (minAppVersion != null) {
if (compareSemVer(minAppVersion, App.version.split('-').first)) {
throw ComicSourceParseException(
"minAppVersion @version is required"
.tlParams({"version": minAppVersion}),
"minAppVersion @version is required".tlParams({
"version": minAppVersion,
}),
);
}
}
@@ -174,8 +184,10 @@ class ComicSourceParser {
}
bool _checkExists(String index) {
return JsEngine().runCode("ComicSource.sources.$_key.$index !== null "
"&& ComicSource.sources.$_key.$index !== undefined");
return JsEngine().runCode(
"ComicSource.sources.$_key.$index !== null "
"&& ComicSource.sources.$_key.$index !== undefined",
);
}
dynamic _getValue(String index) {
@@ -276,16 +288,24 @@ class ComicSourceParser {
if (type == "singlePageWithMultiPart") {
loadMultiPart = () async {
try {
var res = await JsEngine()
.runCode("ComicSource.sources.$_key.explore[$i].load()");
return Res(List.from(res.keys
.map((e) => ExplorePagePart(
var res = await JsEngine().runCode(
"ComicSource.sources.$_key.explore[$i].load()",
);
return Res(
List.from(
res.keys
.map(
(e) => ExplorePagePart(
e,
(res[e] as List)
.map<Comic>((e) => Comic.fromJson(e, _key!))
.toList(),
null))
.toList()));
null,
),
)
.toList(),
),
);
} catch (e, s) {
Log.error("Data Analysis", "$e\n$s");
return Res.error(e.toString());
@@ -296,11 +316,15 @@ class ComicSourceParser {
loadPage = (int page) async {
try {
var res = await JsEngine().runCode(
"ComicSource.sources.$_key.explore[$i].load(${jsonEncode(page)})");
"ComicSource.sources.$_key.explore[$i].load(${jsonEncode(page)})",
);
return Res(
List.generate(res["comics"].length,
(index) => Comic.fromJson(res["comics"][index], _key!)),
subData: res["maxPage"]);
List.generate(
res["comics"].length,
(index) => Comic.fromJson(res["comics"][index], _key!),
),
subData: res["maxPage"],
);
} catch (e, s) {
Log.error("Network", "$e\n$s");
return Res.error(e.toString());
@@ -310,10 +334,13 @@ class ComicSourceParser {
loadNext = (next) async {
try {
var res = await JsEngine().runCode(
"ComicSource.sources.$_key.explore[$i].loadNext(${jsonEncode(next)})");
"ComicSource.sources.$_key.explore[$i].loadNext(${jsonEncode(next)})",
);
return Res(
List.generate(res["comics"].length,
(index) => Comic.fromJson(res["comics"][index], _key!)),
List.generate(
res["comics"].length,
(index) => Comic.fromJson(res["comics"][index], _key!),
),
subData: res["next"],
);
} catch (e, s) {
@@ -325,8 +352,9 @@ class ComicSourceParser {
} else if (type == "multiPartPage") {
loadMultiPart = () async {
try {
var res = await JsEngine()
.runCode("ComicSource.sources.$_key.explore[$i].load()");
var res = await JsEngine().runCode(
"ComicSource.sources.$_key.explore[$i].load()",
);
return Res(
List.from(
(res as List).map((e) {
@@ -349,19 +377,22 @@ class ComicSourceParser {
loadMixed = (index) async {
try {
var res = await JsEngine().runCode(
"ComicSource.sources.$_key.explore[$i].load(${jsonEncode(index)})");
"ComicSource.sources.$_key.explore[$i].load(${jsonEncode(index)})",
);
var list = <Object>[];
for (var data in (res['data'] as List)) {
if (data is List) {
list.add(data.map((e) => Comic.fromJson(e, _key!)).toList());
} else if (data is Map) {
list.add(ExplorePagePart(
list.add(
ExplorePagePart(
data['title'],
(data['comics'] as List).map((e) {
return Comic.fromJson(e, _key!);
}).toList(),
data['viewMore'],
));
),
);
}
}
return Res(list, subData: res['maxPage']);
@@ -371,21 +402,25 @@ class ComicSourceParser {
}
};
}
pages.add(ExplorePageData(
pages.add(
ExplorePageData(
title,
switch (type) {
"singlePageWithMultiPart" => ExplorePageType.singlePageWithMultiPart,
"singlePageWithMultiPart" =>
ExplorePageType.singlePageWithMultiPart,
"multiPartPage" => ExplorePageType.singlePageWithMultiPart,
"multiPageComicList" => ExplorePageType.multiPageComicList,
"mixed" => ExplorePageType.mixed,
_ =>
throw ComicSourceParseException("Unknown explore page type $type")
_ => throw ComicSourceParseException(
"Unknown explore page type $type",
),
},
loadPage,
loadNext,
loadMultiPart,
loadMixed,
));
),
);
}
return pages;
}
@@ -425,18 +460,17 @@ class ComicSourceParser {
if (type == "fixed") {
categoryParts.add(FixedCategoryPart(name, cs!));
} else if (type == "random") {
categoryParts
.add(RandomCategoryPart(name, cs!, c["randomNumber"] ?? 1));
categoryParts.add(
RandomCategoryPart(name, cs!, c["randomNumber"] ?? 1),
);
} else if (type == "dynamic" && categories == null) {
var loader = c["loader"];
if (loader is! JSInvokable) {
throw "DynamicCategoryPart loader must be a function";
}
categoryParts.add(DynamicCategoryPart(
name,
JSAutoFreeFunction(loader),
_key!,
));
categoryParts.add(
DynamicCategoryPart(name, JSAutoFreeFunction(loader), _key!),
);
}
} else {
// old format
@@ -453,30 +487,16 @@ class ComicSourceParser {
for (int i = 0; i < tags.length; i++) {
PageJumpTarget target;
if (itemType == 'category') {
target = PageJumpTarget(
_key!,
'category',
{
target = PageJumpTarget(_key!, 'category', {
"category": tags[i],
"param": categoryParams?.elementAtOrNull(i),
},
);
});
} else if (itemType == 'search') {
target = PageJumpTarget(
_key!,
'search',
{
"keyword": tags[i],
},
);
target = PageJumpTarget(_key!, 'search', {"keyword": tags[i]});
} else if (itemType == 'search_with_namespace') {
target = PageJumpTarget(
_key!,
'search',
{
target = PageJumpTarget(_key!, 'search', {
"keyword": "$name:$tags[i]",
},
);
});
} else {
target = PageJumpTarget(_key!, itemType, null);
}
@@ -485,8 +505,9 @@ class ComicSourceParser {
if (type == "fixed") {
categoryParts.add(FixedCategoryPart(name, cs));
} else if (type == "random") {
categoryParts
.add(RandomCategoryPart(name, cs, c["randomNumber"] ?? 1));
categoryParts.add(
RandomCategoryPart(name, cs, c["randomNumber"] ?? 1),
);
}
}
}
@@ -495,12 +516,16 @@ class ComicSourceParser {
title: title,
categories: categoryParts,
enableRankingPage: enableRankingPage ?? false,
key: title);
key: title,
);
}
CategoryComicsData? _loadCategoryComicsData() {
if (!_checkExists("categoryComics")) return null;
var options = <CategoryComicsOptions>[];
List<CategoryComicsOptions>? options;
if (_checkExists("categoryComics.optionList")) {
options = <CategoryComicsOptions>[];
for (var element in _getValue("categoryComics.optionList") ?? []) {
LinkedHashMap<String, String> map = LinkedHashMap<String, String>();
for (var option in element["options"]) {
@@ -512,11 +537,64 @@ class ComicSourceParser {
var value = split.join("-");
map[key] = value;
}
options.add(CategoryComicsOptions(
options.add(
CategoryComicsOptions(
element["label"] ?? "",
map,
List.from(element["notShowWhen"] ?? []),
element["showWhen"] == null ? null : List.from(element["showWhen"])));
element["showWhen"] == null ? null : List.from(element["showWhen"]),
),
);
}
}
CategoryOptionsLoader? optionLoader;
if (_checkExists("categoryComics.optionLoader")) {
optionLoader = (category, param) async {
try {
dynamic res = JsEngine().runCode("""
ComicSource.sources.$_key.categoryComics.optionLoader(
${jsonEncode(category)}, ${jsonEncode(param)})
""");
if (res is Future) {
res = await res;
}
if (res is! List) {
return Res.error("Invalid data:\nExpected: List\nGot: ${res.runtimeType}");
}
var options = <CategoryComicsOptions>[];
for (var element in res) {
if (element is! Map) {
return Res.error("Invalid option data:\nExpected: Map\nGot: ${element.runtimeType}");
}
LinkedHashMap<String, String> map = LinkedHashMap<String, String>();
for (var option in element["options"] ?? []) {
if (option.isEmpty || !option.contains("-")) {
continue;
}
var split = option.split("-");
var key = split.removeAt(0);
var value = split.join("-");
map[key] = value;
}
options.add(
CategoryComicsOptions(
element["label"] ?? "",
map,
List.from(element["notShowWhen"] ?? []),
element["showWhen"] == null ? null : List.from(element["showWhen"]),
),
);
}
return Res(options);
}
catch(e) {
Log.error("Data Analysis", "Failed to load category options.\n$e");
return Res.error(e.toString());
}
};
}
RankingData? rankingData;
if (_checkExists("categoryComics.ranking")) {
var options = <String, String>{};
@@ -540,9 +618,12 @@ class ComicSourceParser {
${jsonEncode(option)}, ${jsonEncode(page)})
""");
return Res(
List.generate(res["comics"].length,
(index) => Comic.fromJson(res["comics"][index], _key!)),
subData: res["maxPage"]);
List.generate(
res["comics"].length,
(index) => Comic.fromJson(res["comics"][index], _key!),
),
subData: res["maxPage"],
);
} catch (e, s) {
Log.error("Network", "$e\n$s");
return Res.error(e.toString());
@@ -556,8 +637,10 @@ class ComicSourceParser {
${jsonEncode(option)}, ${jsonEncode(next)})
""");
return Res(
List.generate(res["comics"].length,
(index) => Comic.fromJson(res["comics"][index], _key!)),
List.generate(
res["comics"].length,
(index) => Comic.fromJson(res["comics"][index], _key!),
),
subData: res["next"],
);
} catch (e, s) {
@@ -568,7 +651,15 @@ class ComicSourceParser {
}
rankingData = RankingData(options, load, loadWithNext);
}
return CategoryComicsData(options, (category, param, options, page) async {
if (options == null && optionLoader == null) {
options = [];
}
return CategoryComicsData(
options: options,
optionsLoader: optionLoader,
load: (category, param, options, page) async {
try {
var res = await JsEngine().runCode("""
ComicSource.sources.$_key.categoryComics.load(
@@ -579,14 +670,19 @@ class ComicSourceParser {
)
""");
return Res(
List.generate(res["comics"].length,
(index) => Comic.fromJson(res["comics"][index], _key!)),
subData: res["maxPage"]);
List.generate(
res["comics"].length,
(index) => Comic.fromJson(res["comics"][index], _key!),
),
subData: res["maxPage"],
);
} catch (e, s) {
Log.error("Network", "$e\n$s");
return Res.error(e.toString());
}
}, rankingData: rankingData);
},
rankingData: rankingData,
);
}
SearchPageData? _loadSearchData() {
@@ -603,12 +699,14 @@ class ComicSourceParser {
var value = split.join("-");
map[key] = value;
}
options.add(SearchOptions(
options.add(
SearchOptions(
map,
element["label"],
element['type'] ?? 'select',
element['default'] == null ? null : jsonEncode(element['default']),
));
),
);
}
SearchFunction? loadPage;
@@ -623,9 +721,12 @@ class ComicSourceParser {
${jsonEncode(keyword)}, ${jsonEncode(searchOption)}, ${jsonEncode(page)})
""");
return Res(
List.generate(res["comics"].length,
(index) => Comic.fromJson(res["comics"][index], _key!)),
subData: res["maxPage"]);
List.generate(
res["comics"].length,
(index) => Comic.fromJson(res["comics"][index], _key!),
),
subData: res["maxPage"],
);
} catch (e, s) {
Log.error("Network", "$e\n$s");
return Res.error(e.toString());
@@ -639,8 +740,10 @@ class ComicSourceParser {
${jsonEncode(keyword)}, ${jsonEncode(searchOption)}, ${jsonEncode(next)})
""");
return Res(
List.generate(res["comics"].length,
(index) => Comic.fromJson(res["comics"][index], _key!)),
List.generate(
res["comics"].length,
(index) => Comic.fromJson(res["comics"][index], _key!),
),
subData: res["next"],
);
} catch (e, s) {
@@ -689,8 +792,9 @@ class ComicSourceParser {
final bool multiFolder = _getValue("favorites.multiFolder");
final bool? isOldToNewSort = _getValue("favorites.isOldToNewSort");
final bool? singleFolderForSingleComic =
_getValue("favorites.singleFolderForSingleComic");
final bool? singleFolderForSingleComic = _getValue(
"favorites.singleFolderForSingleComic",
);
Future<Res<T>> retryZone<T>(Future<Res<T>> Function() func) async {
if (!ComicSource.find(_key!)!.isLogged) {
@@ -743,9 +847,12 @@ class ComicSourceParser {
${jsonEncode(page)}, ${jsonEncode(folder)})
""");
return Res(
List.generate(res["comics"].length,
(index) => Comic.fromJson(res["comics"][index], _key!)),
subData: res["maxPage"]);
List.generate(
res["comics"].length,
(index) => Comic.fromJson(res["comics"][index], _key!),
),
subData: res["maxPage"],
);
} catch (e, s) {
Log.error("Network", "$e\n$s");
return Res.error(e.toString());
@@ -765,8 +872,10 @@ class ComicSourceParser {
${jsonEncode(next)}, ${jsonEncode(folder)})
""");
return Res(
List.generate(res["comics"].length,
(index) => Comic.fromJson(res["comics"][index], _key!)),
List.generate(
res["comics"].length,
(index) => Comic.fromJson(res["comics"][index], _key!),
),
subData: res["next"],
);
} catch (e, s) {
@@ -858,7 +967,8 @@ class ComicSourceParser {
""");
return Res(
(res["comments"] as List).map((e) => Comment.fromJson(e)).toList(),
subData: res["maxPage"]);
subData: res["maxPage"],
);
} catch (e, s) {
Log.error("Network", "$e\n$s");
return Res.error(e.toString());
@@ -1113,7 +1223,8 @@ class ComicSourceParser {
ComicSource.sources.$_key.comic.archive.getArchives(${jsonEncode(cid)})
""");
return Res(
(res as List).map((e) => ArchiveInfo.fromJson(e)).toList());
(res as List).map((e) => ArchiveInfo.fromJson(e)).toList(),
);
} catch (e, s) {
Log.error("Network", "$e\n$s");
return Res.error(e.toString());

View File

@@ -1,6 +1,8 @@
import 'dart:async' show Future;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:venera/foundation/comic_type.dart';
import 'package:venera/foundation/local.dart';
import 'package:venera/network/images.dart';
import 'package:venera/utils/io.dart';
import 'base_image_provider.dart';
@@ -11,7 +13,12 @@ class CachedImageProvider
/// Image provider for normal image.
///
/// [url] is the url of the image. Local file path is also supported.
const CachedImageProvider(this.url, {this.headers, this.sourceKey, this.cid});
const CachedImageProvider(this.url, {
this.headers,
this.sourceKey,
this.cid,
this.fallbackToLocalCover = false,
});
final String url;
@@ -21,6 +28,9 @@ class CachedImageProvider
final String? cid;
// Use local cover if network image fails to load.
final bool fallbackToLocalCover;
static int loadingCount = 0;
static const _kMaxLoadingCount = 8;
@@ -49,6 +59,24 @@ class CachedImageProvider
}
throw "Error: Empty response body.";
}
catch(e) {
if (fallbackToLocalCover && sourceKey != null && cid != null) {
final localComic = LocalManager().find(
cid!,
ComicType.fromKey(sourceKey!),
);
if (localComic != null) {
var file = localComic.coverFile;
if (await file.exists()) {
var data = await file.readAsBytes();
if (data.isNotEmpty) {
return data;
}
}
}
}
rethrow;
}
finally {
loadingCount--;
}

View File

@@ -248,7 +248,7 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
MediaQuery.of(context).viewPadding.top <= 0 ||
MediaQuery.of(context).viewPadding.top > 50;
if (isPaddingCheckError) {
if (isPaddingCheckError && Platform.isAndroid) {
widget = MediaQuery(
data: MediaQuery.of(context).copyWith(
viewPadding: const EdgeInsets.only(

View File

@@ -181,12 +181,17 @@ abstract class ImageDownloader {
}
if (configs['onResponse'] is JSInvokable) {
buffer = (configs['onResponse'] as JSInvokable)([buffer]);
buffer = (configs['onResponse'] as JSInvokable)([Uint8List.fromList(buffer)]);
(configs['onResponse'] as JSInvokable).free();
}
var data = Uint8List.fromList(buffer);
Uint8List data;
if (buffer is Uint8List) {
data = buffer;
} else {
data = Uint8List.fromList(buffer);
buffer.clear();
}
if (configs['modifyImage'] != null) {
var newData = await modifyImageWithScript(

View File

@@ -27,9 +27,11 @@ class CategoryComicsPage extends StatefulWidget {
class _CategoryComicsPageState extends State<CategoryComicsPage> {
late final CategoryComicsData data;
late final List<CategoryComicsOptions> options;
late List<CategoryComicsOptions>? options;
late final CategoryOptionsLoader? optionsLoader;
late List<String> optionsValue;
late String sourceKey;
String? error;
void findData() {
for (final source in ComicSource.all()) {
@@ -38,7 +40,8 @@ class _CategoryComicsPageState extends State<CategoryComicsPage> {
throw "The comic source ${source.name} does not support category comics";
}
data = source.categoryComicsData!;
options = data.options.where((element) {
if (data.options != null) {
options = data.options!.where((element) {
if (element.notShowWhen.contains(widget.category)) {
return false;
} else if (element.showWhen != null) {
@@ -46,16 +49,14 @@ class _CategoryComicsPageState extends State<CategoryComicsPage> {
}
return true;
}).toList();
var defaultOptionsValue =
options.map((e) => e.options.keys.first).toList();
if (optionsValue.length != options.length) {
var newOptionsValue = List<String>.filled(options.length, "");
for (var i = 0; i < options.length; i++) {
newOptionsValue[i] =
optionsValue.elementAtOrNull(i) ?? defaultOptionsValue[i];
} else {
options = null;
}
optionsValue = newOptionsValue;
if (data.optionsLoader != null) {
optionsLoader = data.optionsLoader;
loadOptions();
}
resetOptionsValue();
sourceKey = source.key;
return;
}
@@ -63,6 +64,36 @@ class _CategoryComicsPageState extends State<CategoryComicsPage> {
throw "${widget.categoryKey} Not found";
}
void resetOptionsValue() {
if (options == null) return;
var defaultOptionsValue = options!
.map((e) => e.options.keys.first)
.toList();
if (optionsValue.length != options!.length) {
var newOptionsValue = List<String>.filled(options!.length, "");
for (var i = 0; i < options!.length; i++) {
newOptionsValue[i] =
optionsValue.elementAtOrNull(i) ?? defaultOptionsValue[i];
}
optionsValue = newOptionsValue;
}
}
void loadOptions() async {
final res = await optionsLoader!(widget.category, widget.param);
if (res.error) {
setState(() {
error = res.errorMessage;
});
} else {
setState(() {
options = res.data;
resetOptionsValue();
error = null;
});
}
}
@override
void initState() {
if (widget.options != null) {
@@ -77,27 +108,44 @@ class _CategoryComicsPageState extends State<CategoryComicsPage> {
@override
Widget build(BuildContext context) {
var topPadding = context.padding.top + 56.0;
Widget body;
if (options == null) {
body = Center(child: CircularProgressIndicator());
} else if (error != null) {
body = NetworkError(
message: error!,
retry: () {
setState(() {
error = null;
});
loadOptions();
},
);
} else {
body = ComicList(
key: Key(widget.category + optionsValue.toString()),
errorLeading: buildOptions().paddingTop(topPadding),
leadingSliver: buildOptions().paddingTop(topPadding).toSliver(),
loadPage: (i) =>
data.load(widget.category, widget.param, optionsValue, i),
);
}
return Scaffold(
extendBodyBehindAppBar: true,
appBar: Appbar(
title: Text(widget.category),
),
body: ComicList(
key: Key(widget.category + optionsValue.toString()),
errorLeading: SizedBox(height: topPadding),
leadingSliver: buildOptions().paddingTop(topPadding).toSliver(),
loadPage: (i) => data.load(
widget.category,
widget.param,
optionsValue,
i,
),
),
appBar: Appbar(title: Text(widget.category)),
body: body,
);
}
Widget buildOptionItem(
String text, String value, int group, BuildContext context) {
String text,
String value,
int group,
BuildContext context,
) {
return OptionChip(
text: text.ts(sourceKey),
isSelected: value == optionsValue[group],
@@ -112,8 +160,26 @@ class _CategoryComicsPageState extends State<CategoryComicsPage> {
Widget buildOptions() {
List<Widget> children = [];
for (var optionList in options) {
children.add(Wrap(
var group = 0;
for (var optionList in options!) {
if (optionList.label.isNotEmpty) {
children.add(Padding(
padding: const EdgeInsets.only(
bottom: 8.0,
left: 4.0,
),
child: Text(
optionList.label.ts(sourceKey),
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
));
}
if (optionList.options.length <= 8) {
children.add(
Wrap(
spacing: 8,
runSpacing: 8,
children: [
@@ -121,14 +187,30 @@ class _CategoryComicsPageState extends State<CategoryComicsPage> {
buildOptionItem(
option.value.tl,
option.key,
options.indexOf(optionList),
group,
context,
)
),
],
),
);
} else {
var g = group;
children.add(Select(
current: optionList.options[optionsValue[g]],
values: optionList.options.values.toList(),
onTap: (i) {
var key = optionList.options.keys.elementAt(i);
if (key == optionsValue[g]) return;
setState(() {
optionsValue[g] = key;
});
},
));
if (options.last != optionList) {
}
if (options!.last != optionList) {
children.add(const SizedBox(height: 8));
}
group++;
}
return Column(
mainAxisSize: MainAxisSize.min,

View File

@@ -77,8 +77,10 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
@override
void onReadEnd() {
history ??=
HistoryManager().find(widget.id, ComicType(widget.sourceKey.hashCode));
history ??= HistoryManager().find(
widget.id,
ComicType(widget.sourceKey.hashCode),
);
update();
}
@@ -93,6 +95,32 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
);
}
@override
Widget buildError() {
final isDownloaded = LocalManager().isDownloaded(
widget.id,
ComicType.fromKey(widget.sourceKey),
);
Widget? action;
if (isDownloaded) {
action = FilledButton.tonal(
child: Text("Read".tl),
onPressed: () {
final localComic = LocalManager().find(
widget.id,
ComicType.fromKey(widget.sourceKey),
);
if (localComic == null) {
context.showMessage(message: "Local comic not found".tl);
return;
}
localComic.read();
},
);
}
return NetworkError(message: error!, retry: retry, action: action);
}
@override
void initState() {
scrollController.addListener(onScroll);
@@ -114,7 +142,8 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
ComicDetails get comic => data!;
void onScroll() {
var offset = scrollController.position.pixels -
var offset =
scrollController.position.pixels -
scrollController.position.minScrollExtent;
var showFAB = offset > 0;
if (showFAB != this.showFAB) {
@@ -145,9 +174,11 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
floatingActionButton: showFAB
? FloatingActionButton(
onPressed: () {
scrollController.animateTo(0,
scrollController.animateTo(
0,
duration: const Duration(milliseconds: 200),
curve: Curves.ease);
curve: Curves.ease,
);
},
child: const Icon(Icons.arrow_upward),
)
@@ -164,7 +195,9 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
buildThumbnails(),
buildRecommend(),
SliverPadding(
padding: EdgeInsets.only(bottom: context.padding.bottom + 80), // Add additional padding for FAB
padding: EdgeInsets.only(
bottom: context.padding.bottom + 80,
), // Add additional padding for FAB
),
],
),
@@ -190,12 +223,9 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
initialPage: history?.page,
initialChapter: history?.ep,
initialChapterGroup: history?.group,
history: history ??
History.fromModel(
model: localComic,
ep: 0,
page: 0,
),
history:
history ??
History.fromModel(model: localComic, ep: 0, page: 0),
author: localComic.subTitle ?? '',
tags: localComic.tags,
);
@@ -215,8 +245,10 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
widget.id,
ComicType(widget.sourceKey.hashCode),
);
history =
HistoryManager().find(widget.id, ComicType(widget.sourceKey.hashCode));
history = HistoryManager().find(
widget.id,
ComicType(widget.sourceKey.hashCode),
);
return comicSource.loadComicInfo!(widget.id);
}
@@ -225,11 +257,7 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
isLiked = comic.isLiked ?? false;
isFavorite = comic.isFavorite ?? false;
if (comic.chapters == null) {
isDownloaded = LocalManager().isDownloaded(
comic.id,
comic.comicType,
0,
);
isDownloaded = LocalManager().isDownloaded(comic.id, comic.comicType, 0);
}
}
@@ -242,7 +270,9 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
),
actions: [
IconButton(
onPressed: showMoreActions, icon: const Icon(Icons.more_horiz))
onPressed: showMoreActions,
icon: const Icon(Icons.more_horiz),
),
],
);
@@ -288,8 +318,10 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
children: [
SelectableText(comic.title, style: ts.s18),
if (comic.subTitle != null)
SelectableText(comic.subTitle!, style: ts.s14)
.paddingVertical(4),
SelectableText(
comic.subTitle!,
style: ts.s14,
).paddingVertical(4),
Text(
(ComicSource.find(comic.sourceKey)?.name) ?? '',
style: ts.s12,
@@ -338,7 +370,8 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
icon: const Icon(Icons.favorite_border),
activeIcon: const Icon(Icons.favorite),
isActive: isLiked,
text: ((data!.likesCount != null)
text:
((data!.likesCount != null)
? (data!.likesCount! + (isLiked ? 1 : 0))
: (isLiked ? 'Liked'.tl : 'Like'.tl))
.toString(),
@@ -383,9 +416,11 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
Expanded(
child: hasHistory
? FilledButton(
onPressed: continueRead, child: Text("Continue".tl))
: FilledButton(onPressed: read, child: Text("Read".tl)),
onPressed: continueRead,
child: Text("Continue".tl),
)
: FilledButton(onPressed: read, child: Text("Read".tl)),
),
],
).paddingHorizontal(16).paddingVertical(8),
if (history != null)
@@ -412,19 +447,20 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
var epName = "E$ep";
String? groupName;
try {
if (group == null){
if (group == null) {
epName = comic.chapters!.titles.elementAt(
math.min(ep - 1, comic.chapters!.length - 1),
);
} else {
groupName = comic.chapters!.groups.elementAt(group - 1);
groupName = comic.chapters!.groups.elementAt(
group - 1,
);
epName = comic.chapters!
.getGroupByIndex(group - 1)
.values
.elementAt(ep - 1);
}
}
catch(e) {
} catch (e) {
// ignore
}
text = groupName == null
@@ -453,9 +489,7 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
return SliverLazyToBoxAdapter(
child: Column(
children: [
ListTile(
title: Text("Description".tl),
),
ListTile(title: Text("Description".tl)),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: SelectableText(comic.description!).fixWidth(double.infinity),
@@ -539,10 +573,7 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
);
} else {
return Container(
decoration: BoxDecoration(
color: color,
borderRadius: borderRadius,
),
decoration: BoxDecoration(color: color, borderRadius: borderRadius),
child: Text(text).padding(padding),
);
}
@@ -552,13 +583,13 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
if (int.tryParse(time) != null) {
var t = int.tryParse(time);
if (t! > 1000000000000) {
return DateTime.fromMillisecondsSinceEpoch(t)
.toString()
.substring(0, 19);
return DateTime.fromMillisecondsSinceEpoch(
t,
).toString().substring(0, 19);
} else {
return DateTime.fromMillisecondsSinceEpoch(t * 1000)
.toString()
.substring(0, 19);
return DateTime.fromMillisecondsSinceEpoch(
t * 1000,
).toString().substring(0, 19);
}
}
if (time.contains('T') || time.contains('Z')) {
@@ -583,17 +614,11 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListTile(
title: Text("Information".tl),
),
ListTile(title: Text("Information".tl)),
if (comic.stars != null)
Row(
children: [
StarRating(
value: comic.stars!,
size: 24,
onTap: starRating,
),
StarRating(value: comic.stars!, size: 24, onTap: starRating),
const SizedBox(width: 8),
Text(comic.stars!.toStringAsFixed(2)),
],
@@ -671,24 +696,19 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
if (comic.recommend == null || comic.recommend!.isEmpty) {
return const SliverPadding(padding: EdgeInsets.zero);
}
return SliverMainAxisGroup(slivers: [
SliverToBoxAdapter(
child: ListTile(
title: Text("Related".tl),
),
),
return SliverMainAxisGroup(
slivers: [
SliverToBoxAdapter(child: ListTile(title: Text("Related".tl))),
SliverGridComics(comics: comic.recommend!),
]);
],
);
}
Widget buildComments() {
if (comic.comments == null || comic.comments!.isEmpty) {
return const SliverPadding(padding: EdgeInsets.zero);
}
return _CommentsPart(
comments: comic.comments!,
showMore: showComments,
);
return _CommentsPart(comments: comic.comments!, showMore: showComments);
}
}
@@ -793,8 +813,8 @@ class _SelectDownloadChapterState extends State<_SelectDownloadChapter> {
itemBuilder: (context, i) {
return CheckboxListTile(
title: Text(widget.eps[i]),
value: selected.contains(i) ||
widget.downloadedEps.contains(i),
value:
selected.contains(i) || widget.downloadedEps.contains(i),
onChanged: widget.downloadedEps.contains(i)
? null
: (v) {
@@ -805,7 +825,8 @@ class _SelectDownloadChapterState extends State<_SelectDownloadChapter> {
selected.add(i);
}
});
});
},
);
},
),
),
@@ -813,9 +834,7 @@ class _SelectDownloadChapterState extends State<_SelectDownloadChapter> {
height: 50,
decoration: BoxDecoration(
border: Border(
top: BorderSide(
color: context.colorScheme.outlineVariant,
),
top: BorderSide(color: context.colorScheme.outlineVariant),
),
),
child: Row(
@@ -880,8 +899,12 @@ class _ComicPageLoadingPlaceHolder extends StatelessWidget {
@override
Widget build(BuildContext context) {
Widget buildContainer(double? width, double? height,
{Color? color, double? radius}) {
Widget buildContainer(
double? width,
double? height, {
Color? color,
double? radius,
}) {
return Container(
height: height,
width: width,
@@ -923,13 +946,9 @@ class _ComicPageLoadingPlaceHolder extends StatelessWidget {
if (context.width < changePoint)
Row(
children: [
Expanded(
child: buildContainer(null, 36, radius: 18),
),
Expanded(child: buildContainer(null, 36, radius: 18)),
const SizedBox(width: 16),
Expanded(
child: buildContainer(null, 36, radius: 18),
),
Expanded(child: buildContainer(null, 36, radius: 18)),
],
).paddingHorizontal(16),
const Divider(),
@@ -938,7 +957,7 @@ class _ComicPageLoadingPlaceHolder extends StatelessWidget {
child: CircularProgressIndicator(
strokeWidth: 2.4,
).fixHeight(24).fixWidth(24),
)
),
],
),
);
@@ -948,11 +967,7 @@ class _ComicPageLoadingPlaceHolder extends StatelessWidget {
Widget child;
if (cover != null) {
child = AnimatedImage(
image: CachedImageProvider(
cover!,
sourceKey: sourceKey,
cid: cid,
),
image: CachedImageProvider(cover!, sourceKey: sourceKey, cid: cid),
width: double.infinity,
height: double.infinity,
fit: BoxFit.cover,

View File

@@ -512,6 +512,18 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
);
},
),
if (selectedComics.length == 1)
MenuEntry(
icon: Icons.chrome_reader_mode_outlined,
text: "Read".tl,
onClick: () {
final c = selectedComics.keys.first as FavoriteItem;
App.rootContext.to(() => ReaderWithLoading(
id: c.id,
sourceKey: c.sourceKey,
));
},
),
]),
],
)

View File

@@ -2,11 +2,11 @@ name: venera
description: "A comic app."
publish_to: 'none'
version: 1.4.6+146
version: 1.5.1+151
environment:
sdk: '>=3.8.0 <4.0.0'
flutter: 3.35.2
flutter: 3.35.3
dependencies:
flutter: