Merge pull request #157 from venera-app/dev

v1.2.2
This commit is contained in:
nyne
2025-01-26 14:29:13 +08:00
committed by GitHub
32 changed files with 974 additions and 457 deletions

View File

@@ -99,6 +99,17 @@ android {
} }
} }
} }
debug {
ndk {
abiFilters "armeabi-v7a", "arm64-v8a", "x86_64"
}
signingConfig signingConfigs.debug
applicationVariants.all { variant ->
variant.outputs.all { output ->
versionCodeOverride = variant.versionCode * 10 + 4
}
}
}
} }
dependenciesInfo { dependenciesInfo {

View File

@@ -160,7 +160,7 @@
"Date Desc": "日期降序", "Date Desc": "日期降序",
"Start": "开始", "Start": "开始",
"Export App Data": "导出应用数据", "Export App Data": "导出应用数据",
"Import App Data (Please restart after success)": "导入应用数据(成功后请手动重启)", "Import App Data": "导入应用数据",
"Export": "导出", "Export": "导出",
"Download Threads": "下载线程数", "Download Threads": "下载线程数",
"Update Time": "更新时间", "Update Time": "更新时间",
@@ -229,7 +229,7 @@
"Clear History": "清除历史", "Clear History": "清除历史",
"Are you sure you want to clear your history?": "确定要清除您的历史记录吗?", "Are you sure you want to clear your history?": "确定要清除您的历史记录吗?",
"No Explore Pages": "没有探索页面", "No Explore Pages": "没有探索页面",
"Add a comic source in home page": "在主页添加一个漫画源", "Please add some sources": "请添加一些源",
"Please check your settings": "请检查您的设置", "Please check your settings": "请检查您的设置",
"No Category Pages": "没有分类页面", "No Category Pages": "没有分类页面",
"Chapter @ep": "第 @ep 章", "Chapter @ep": "第 @ep 章",
@@ -314,7 +314,11 @@
"New Version": "新版本", "New Version": "新版本",
"@c updates": "@c 项更新", "@c updates": "@c 项更新",
"No updates": "无更新", "No updates": "无更新",
"Set comic source list url": "设置漫画源列表URL" "Set comic source list url": "设置漫画源列表URL",
"Deselect All": "取消全选",
"Add keyword": "添加关键词",
"Keyword": "关键词",
"Manage": "管理"
}, },
"zh_TW": { "zh_TW": {
"Home": "首頁", "Home": "首頁",
@@ -477,7 +481,7 @@
"Start": "開始", "Start": "開始",
"Reversed successfully": "反轉成功", "Reversed successfully": "反轉成功",
"Export App Data": "匯出應用數據", "Export App Data": "匯出應用數據",
"Import App Data (Please restart after success)": "匯入應用數據(成功后請手動重啟)", "Import App Data": "匯入應用數據",
"Export": "匯出", "Export": "匯出",
"Download Threads": "下載線程數", "Download Threads": "下載線程數",
"Update Time": "更新時間", "Update Time": "更新時間",
@@ -546,7 +550,7 @@
"Clear History": "清除歷史", "Clear History": "清除歷史",
"Are you sure you want to clear your history?": "確定要清除您的歷史記錄嗎?", "Are you sure you want to clear your history?": "確定要清除您的歷史記錄嗎?",
"No Explore Pages": "沒有探索頁面", "No Explore Pages": "沒有探索頁面",
"Add a comic source in home page": "在主頁添加一個漫畫源", "Please add some sources": "請添加一些源",
"Please check your settings": "請檢查您的設定", "Please check your settings": "請檢查您的設定",
"No Category Pages": "沒有分類頁面", "No Category Pages": "沒有分類頁面",
"Chapter @ep": "第 @ep 章", "Chapter @ep": "第 @ep 章",
@@ -631,6 +635,10 @@
"New Version": "新版本", "New Version": "新版本",
"@c updates": "@c 項更新", "@c updates": "@c 項更新",
"No updates": "無更新", "No updates": "無更新",
"Set comic source list url": "設置漫畫源列表URL" "Set comic source list url": "設置漫畫源列表URL",
"Deselect All": "取消全選",
"Add keyword": "添加關鍵詞",
"Keyword": "關鍵詞",
"Manage": "管理"
} }
} }

View File

@@ -1,12 +1,14 @@
part of 'components.dart'; part of 'components.dart';
class Appbar extends StatefulWidget implements PreferredSizeWidget { class Appbar extends StatefulWidget implements PreferredSizeWidget {
const Appbar( const Appbar({
{required this.title, required this.title,
this.leading, this.leading,
this.actions, this.actions,
this.backgroundColor, this.backgroundColor,
super.key}); this.style = AppbarStyle.blur,
super.key,
});
final Widget title; final Widget title;
@@ -16,6 +18,8 @@ class Appbar extends StatefulWidget implements PreferredSizeWidget {
final Color? backgroundColor; final Color? backgroundColor;
final AppbarStyle style;
@override @override
State<Appbar> createState() => _AppbarState(); State<Appbar> createState() => _AppbarState();
@@ -108,10 +112,18 @@ class _AppbarState extends State<Appbar> {
], ],
).paddingTop(context.padding.top), ).paddingTop(context.padding.top),
); );
return BlurEffect( if (widget.style == AppbarStyle.shadow) {
blur: _scrolledUnder ? 15 : 0, return Material(
child: content, color: context.colorScheme.surface,
); elevation: _scrolledUnder ? 2 : 0,
child: content,
);
} else {
return BlurEffect(
blur: _scrolledUnder ? 15 : 0,
child: content,
);
}
} }
} }
@@ -256,18 +268,25 @@ class _MySliverAppBarDelegate extends SliverPersistentHeaderDelegate {
} }
} }
class FilledTabBar extends StatefulWidget { class AppTabBar extends StatefulWidget {
const FilledTabBar({super.key, this.controller, required this.tabs}); const AppTabBar({
super.key,
this.controller,
required this.tabs,
this.actionButton,
});
final TabController? controller; final TabController? controller;
final List<Tab> tabs; final List<Tab> tabs;
final Widget? actionButton;
@override @override
State<FilledTabBar> createState() => _FilledTabBarState(); State<AppTabBar> createState() => _AppTabBarState();
} }
class _FilledTabBarState extends State<FilledTabBar> { class _AppTabBarState extends State<AppTabBar> {
late TabController _controller; late TabController _controller;
late List<GlobalKey> keys; late List<GlobalKey> keys;
@@ -315,7 +334,7 @@ class _FilledTabBarState extends State<FilledTabBar> {
} }
@override @override
void didUpdateWidget(covariant FilledTabBar oldWidget) { void didUpdateWidget(covariant AppTabBar oldWidget) {
if (widget.controller != oldWidget.controller) { if (widget.controller != oldWidget.controller) {
_controller = widget.controller ?? DefaultTabController.of(context); _controller = widget.controller ?? DefaultTabController.of(context);
_controller.animation!.addListener(onTabChanged); _controller.animation!.addListener(onTabChanged);
@@ -366,25 +385,27 @@ class _FilledTabBarState extends State<FilledTabBar> {
painter: painter, painter: painter,
child: _TabRow( child: _TabRow(
callback: _tabLayoutCallback, callback: _tabLayoutCallback,
children: List.generate(widget.tabs.length, buildTab), children: List.generate(widget.tabs.length, buildTab)
..addIfNotNull(widget.actionButton?.padding(tabPadding)),
), ),
).paddingHorizontal(4), ).paddingHorizontal(4),
); );
}, },
); );
return Container( return Container(
key: tabBarKey, key: tabBarKey,
height: _kTabHeight, height: _kTabHeight,
width: double.infinity, width: double.infinity,
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border( border: Border(
bottom: BorderSide( bottom: BorderSide(
color: context.colorScheme.outlineVariant, color: context.colorScheme.outlineVariant,
width: 0.6, width: 0.6,
),
), ),
), ),
child: widget.tabs.isEmpty ? const SizedBox() : child); ),
child: widget.tabs.isEmpty ? const SizedBox() : child,
);
} }
int? previousIndex; int? previousIndex;
@@ -544,7 +565,7 @@ class _IndicatorPainter extends CustomPainter {
var rect = Rect.fromLTWH( var rect = Rect.fromLTWH(
tabLeft + padding.left + horizontalPadding, tabLeft + padding.left + horizontalPadding,
_FilledTabBarState._kTabHeight - 3.6, _AppTabBarState._kTabHeight - 3.6,
tabRight - tabLeft - padding.horizontal - horizontalPadding * 2, tabRight - tabLeft - padding.horizontal - horizontalPadding * 2,
3, 3,
); );
@@ -621,7 +642,6 @@ class _TabViewBodyState extends State<TabViewBody> {
} }
} }
class SearchBarController { class SearchBarController {
_SearchBarMixin? _state; _SearchBarMixin? _state;
@@ -894,3 +914,42 @@ class _SearchBarState extends State<AppSearchBar> with _SearchBarMixin {
); );
} }
} }
class TabActionButton extends StatelessWidget {
const TabActionButton({
super.key,
required this.icon,
required this.text,
required this.onPressed,
});
final Icon icon;
final String text;
final void Function() onPressed;
static const _kTabHeight = 46.0;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onPressed,
borderRadius: BorderRadius.circular(8),
child: Container(
height: _kTabHeight,
padding: const EdgeInsets.symmetric(horizontal: 12),
child: IconTheme(
data: IconThemeData(size: 20, color: context.colorScheme.primary),
child: Row(
children: [
icon,
const SizedBox(width: 8),
Text(text, style: ts.withColor(context.colorScheme.primary)),
],
),
),
),
);
}
}

View File

@@ -55,7 +55,7 @@ class _CodeEditorState extends State<CodeEditor> {
Widget buildLineNumbers() { Widget buildLineNumbers() {
return SizedBox( return SizedBox(
width: 32, width: 36,
child: Column( child: Column(
children: [ children: [
for (var i = 1; i <= lineCount; i++) for (var i = 1; i <= lineCount; i++)

View File

@@ -356,14 +356,13 @@ class ComicTile extends StatelessWidget {
), ),
Padding( Padding(
padding: const EdgeInsets.fromLTRB(4, 4, 4, 0), padding: const EdgeInsets.fromLTRB(4, 4, 4, 0),
child: TextScroll( child: Text(
comic.title.replaceAll('\n', ''), comic.title.replaceAll('\n', ''),
mode: TextScrollMode.endless, maxLines: 1,
overflow: TextOverflow.clip,
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
delayBefore: Duration(milliseconds: 500),
velocity: const Velocity(pixelsPerSecond: Offset(40, 0)),
), ),
), ),
], ],

View File

@@ -9,7 +9,6 @@ import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:syntax_highlight/syntax_highlight.dart'; import 'package:syntax_highlight/syntax_highlight.dart';
import 'package:text_scroll/text_scroll.dart';
import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/app.dart';
import 'package:venera/foundation/app_page_route.dart'; import 'package:venera/foundation/app_page_route.dart';
import 'package:venera/foundation/appdata.dart'; import 'package:venera/foundation/appdata.dart';

View File

@@ -58,26 +58,12 @@ class _AnimatedTapRegionState extends State<AnimatedTapRegion> {
}, },
child: GestureDetector( child: GestureDetector(
onTap: widget.onTap, onTap: widget.onTap,
child: AnimatedContainer( child: AnimatedPhysicalModel(
duration: _fastAnimationDuration, duration: _fastAnimationDuration,
decoration: BoxDecoration( elevation: isHovered ? 3 : 1,
borderRadius: BorderRadius.circular(widget.borderRadius), color: context.colorScheme.surface,
boxShadow: isHovered shadowColor: context.colorScheme.shadow,
? [ borderRadius: BorderRadius.circular(widget.borderRadius),
BoxShadow(
color: context.colorScheme.outline,
blurRadius: 2,
offset: const Offset(0, 2),
),
]
: [
BoxShadow(
color: context.colorScheme.outlineVariant,
blurRadius: 1,
offset: const Offset(0, 1),
),
],
),
child: widget.child, child: widget.child,
), ),
), ),

View File

@@ -277,35 +277,38 @@ class _AnimatedImageState extends State<AnimatedImage>
if (_imageInfo != null) { if (_imageInfo != null) {
if (widget.part != null) { if (widget.part != null) {
return CustomPaint( result = CustomPaint(
isComplex: true,
painter: ImagePainter( painter: ImagePainter(
image: _imageInfo!.image, image: _imageInfo!.image,
part: widget.part!, part: widget.part!,
fit: widget.fit ?? BoxFit.cover,
), ),
child: SizedBox( child: SizedBox(
width: widget.width, width: widget.width,
height: widget.height, height: widget.height,
), ),
); );
} else {
result = RawImage(
image: _imageInfo?.image,
width: widget.width,
height: widget.height,
debugImageLabel: _imageInfo?.debugLabel,
scale: _imageInfo?.scale ?? 1.0,
color: widget.color,
opacity: widget.opacity,
colorBlendMode: widget.colorBlendMode,
fit: BoxFit.cover,
alignment: widget.alignment,
repeat: widget.repeat,
centerSlice: widget.centerSlice,
matchTextDirection: widget.matchTextDirection,
invertColors: _invertColors,
isAntiAlias: widget.isAntiAlias,
filterQuality: widget.filterQuality,
);
} }
result = RawImage(
image: _imageInfo?.image,
width: widget.width,
height: widget.height,
debugImageLabel: _imageInfo?.debugLabel,
scale: _imageInfo?.scale ?? 1.0,
color: widget.color,
opacity: widget.opacity,
colorBlendMode: widget.colorBlendMode,
fit: BoxFit.cover,
alignment: widget.alignment,
repeat: widget.repeat,
centerSlice: widget.centerSlice,
matchTextDirection: widget.matchTextDirection,
invertColors: _invertColors,
isAntiAlias: widget.isAntiAlias,
filterQuality: widget.filterQuality,
);
} else if (_lastException != null) { } else if (_lastException != null) {
result = const Center( result = const Center(
child: Icon(Icons.error), child: Icon(Icons.error),
@@ -362,10 +365,13 @@ class ImagePainter extends CustomPainter {
final ImagePart part; final ImagePart part;
final BoxFit fit;
/// Render a part of the image. /// Render a part of the image.
const ImagePainter({ const ImagePainter({
required this.image, required this.image,
this.part = const ImagePart(), this.part = const ImagePart(),
this.fit = BoxFit.cover,
}); });
@override @override
@@ -377,7 +383,8 @@ class ImagePainter extends CustomPainter {
part.y2 ?? image.height.toDouble(), part.y2 ?? image.height.toDouble(),
), ),
); );
final Rect dst = Offset.zero & size; var fitted = applyBoxFit(fit, Size(src.width, src.height), size).destination;
var dst = Alignment.center.inscribe(fitted, Offset.zero & size);
canvas.drawImageRect(image, src, dst, Paint()); canvas.drawImageRect(image, src, dst, Paint());
} }

View File

@@ -2,7 +2,10 @@ part of 'components.dart';
class SliverGridViewWithFixedItemHeight extends StatelessWidget { class SliverGridViewWithFixedItemHeight extends StatelessWidget {
const SliverGridViewWithFixedItemHeight( const SliverGridViewWithFixedItemHeight(
{required this.delegate, required this.maxCrossAxisExtent, required this.itemHeight, super.key}); {required this.delegate,
required this.maxCrossAxisExtent,
required this.itemHeight,
super.key});
final SliverChildDelegate delegate; final SliverChildDelegate delegate;
@@ -62,7 +65,8 @@ class SliverGridDelegateWithFixedHeight extends SliverGridDelegate {
@override @override
bool shouldRelayout(covariant SliverGridDelegate oldDelegate) { bool shouldRelayout(covariant SliverGridDelegate oldDelegate) {
if (oldDelegate is! SliverGridDelegateWithFixedHeight) return true; if (oldDelegate is! SliverGridDelegateWithFixedHeight) return true;
if (oldDelegate.maxCrossAxisExtent != maxCrossAxisExtent || oldDelegate.itemHeight != itemHeight) { if (oldDelegate.maxCrossAxisExtent != maxCrossAxisExtent ||
oldDelegate.itemHeight != itemHeight) {
return true; return true;
} }
return false; return false;
@@ -70,28 +74,29 @@ class SliverGridDelegateWithFixedHeight extends SliverGridDelegate {
} }
class SliverGridDelegateWithComics extends SliverGridDelegate { class SliverGridDelegateWithComics extends SliverGridDelegate {
SliverGridDelegateWithComics([this.useBriefMode = false, this.scale]); SliverGridDelegateWithComics();
final bool useBriefMode; final bool useBriefMode = appdata.settings['comicDisplayMode'] == 'brief';
final double? scale; final double scale = (appdata.settings['comicTileScale'] as num).toDouble();
@override @override
SliverGridLayout getLayout(SliverConstraints constraints) { SliverGridLayout getLayout(SliverConstraints constraints) {
if (appdata.settings['comicDisplayMode'] == 'brief' || useBriefMode) { if (useBriefMode) {
return getBriefModeLayout( return getBriefModeLayout(
constraints, constraints,
scale ?? (appdata.settings['comicTileScale'] as num).toDouble(), scale,
); );
} else { } else {
return getDetailedModeLayout( return getDetailedModeLayout(
constraints, constraints,
scale ?? (appdata.settings['comicTileScale'] as num).toDouble(), scale,
); );
} }
} }
SliverGridLayout getDetailedModeLayout(SliverConstraints constraints, double scale) { SliverGridLayout getDetailedModeLayout(
SliverConstraints constraints, double scale) {
const minCrossAxisExtent = 360; const minCrossAxisExtent = 360;
final itemHeight = 152 * scale; final itemHeight = 152 * scale;
final width = constraints.crossAxisExtent; final width = constraints.crossAxisExtent;
@@ -106,11 +111,14 @@ class SliverGridDelegateWithComics extends SliverGridDelegate {
reverseCrossAxis: false); reverseCrossAxis: false);
} }
SliverGridLayout getBriefModeLayout(SliverConstraints constraints, double scale) { SliverGridLayout getBriefModeLayout(
SliverConstraints constraints, double scale) {
final maxCrossAxisExtent = 192.0 * scale; final maxCrossAxisExtent = 192.0 * scale;
const childAspectRatio = 0.68; const childAspectRatio = 0.64;
const crossAxisSpacing = 0.0; const crossAxisSpacing = 0.0;
int crossAxisCount = (constraints.crossAxisExtent / (maxCrossAxisExtent + crossAxisSpacing)).ceil(); int crossAxisCount =
(constraints.crossAxisExtent / (maxCrossAxisExtent + crossAxisSpacing))
.ceil();
// Ensure a minimum count of 1, can be zero and result in an infinite extent // Ensure a minimum count of 1, can be zero and result in an infinite extent
// below when the window size is 0. // below when the window size is 0.
crossAxisCount = math.max(1, crossAxisCount); crossAxisCount = math.max(1, crossAxisCount);
@@ -132,6 +140,11 @@ class SliverGridDelegateWithComics extends SliverGridDelegate {
@override @override
bool shouldRelayout(covariant SliverGridDelegate oldDelegate) { bool shouldRelayout(covariant SliverGridDelegate oldDelegate) {
return true; if (oldDelegate is! SliverGridDelegateWithComics) return true;
if (oldDelegate.scale != scale ||
oldDelegate.useBriefMode != useBriefMode) {
return true;
}
return false;
} }
} }

View File

@@ -6,6 +6,7 @@ class NetworkError extends StatelessWidget {
required this.message, required this.message,
this.retry, this.retry,
this.withAppbar = true, this.withAppbar = true,
this.buttonText,
}); });
final String message; final String message;
@@ -14,6 +15,8 @@ class NetworkError extends StatelessWidget {
final bool withAppbar; final bool withAppbar;
final String? buttonText;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var cfe = CloudflareException.fromString(message); var cfe = CloudflareException.fromString(message);
@@ -60,7 +63,7 @@ class NetworkError extends StatelessWidget {
else else
FilledButton( FilledButton(
onPressed: retry, onPressed: retry,
child: Text('Retry'.tl), child: Text(buttonText ?? 'Retry'.tl),
), ),
], ],
), ),

View File

@@ -98,8 +98,17 @@ class _SmoothScrollProviderState extends State<SmoothScrollProvider> {
_controller.position.maxScrollExtent, _controller.position.maxScrollExtent,
); );
if (_futurePosition == old) return; if (_futurePosition == old) return;
_controller.animateTo(_futurePosition!, var target = _futurePosition!;
duration: _fastAnimationDuration, curve: Curves.linear); _controller.animateTo(
_futurePosition!,
duration: _fastAnimationDuration,
curve: Curves.linear,
).then((_) {
var current = _controller.position.pixels;
if (current == target && current == _futurePosition) {
_futurePosition = null;
}
});
} }
}, },
child: ScrollControllerProvider._( child: ScrollControllerProvider._(

View File

@@ -10,7 +10,7 @@ export "widget_utils.dart";
export "context.dart"; export "context.dart";
class _App { class _App {
final version = "1.2.1"; final version = "1.2.2";
bool get isAndroid => Platform.isAndroid; bool get isAndroid => Platform.isAndroid;

View File

@@ -1,4 +1,6 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_saf/flutter_saf.dart'; import 'package:flutter_saf/flutter_saf.dart';
import 'package:rhttp/rhttp.dart';
import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/app.dart';
import 'package:venera/foundation/cache_manager.dart'; import 'package:venera/foundation/cache_manager.dart';
import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/comic_source/comic_source.dart';
@@ -8,12 +10,12 @@ import 'package:venera/foundation/js_engine.dart';
import 'package:venera/foundation/local.dart'; import 'package:venera/foundation/local.dart';
import 'package:venera/foundation/log.dart'; import 'package:venera/foundation/log.dart';
import 'package:venera/network/cookie_jar.dart'; import 'package:venera/network/cookie_jar.dart';
import 'package:venera/utils/app_links.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 'foundation/appdata.dart'; import 'foundation/appdata.dart';
extension FutureInit<T> on Future<T> { extension _FutureInit<T> on Future<T> {
/// Prevent unhandled exception /// Prevent unhandled exception
/// ///
/// A unhandled exception occurred in init() will cause the app to crash. /// A unhandled exception occurred in init() will cause the app to crash.
@@ -27,6 +29,7 @@ extension FutureInit<T> on Future<T> {
} }
Future<void> init() async { Future<void> init() async {
await Rhttp.init();
await SAFTaskWorker().init().wait(); await SAFTaskWorker().init().wait();
await AppTranslation.init().wait(); await AppTranslation.init().wait();
await appdata.init().wait(); await appdata.init().wait();
@@ -39,4 +42,11 @@ 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 (App.isAndroid) {
handleLinks();
}
FlutterError.onError = (details) {
Log.error(
"Unhandled Exception", "${details.exception}\n${details.stack}");
};
} }

View File

@@ -1,14 +1,13 @@
import 'dart:async'; import 'dart:async';
import 'package:desktop_webview_window/desktop_webview_window.dart'; import 'package:desktop_webview_window/desktop_webview_window.dart';
import 'package:dynamic_color/dynamic_color.dart'; import 'package:dynamic_color/dynamic_color.dart';
import 'package:flex_seed_scheme/flex_seed_scheme.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:rhttp/rhttp.dart';
import 'package:venera/foundation/log.dart'; import 'package:venera/foundation/log.dart';
import 'package:venera/pages/auth_page.dart'; import 'package:venera/pages/auth_page.dart';
import 'package:venera/pages/main_page.dart'; import 'package:venera/pages/main_page.dart';
import 'package:venera/utils/app_links.dart';
import 'package:venera/utils/io.dart'; import 'package:venera/utils/io.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
import 'components/components.dart'; import 'components/components.dart';
@@ -18,21 +17,11 @@ import 'foundation/appdata.dart';
import 'init.dart'; import 'init.dart';
void main(List<String> args) { void main(List<String> args) {
if (runWebViewTitleBarWidget(args)) { if (runWebViewTitleBarWidget(args)) return;
return;
}
overrideIO(() { overrideIO(() {
runZonedGuarded(() async { runZonedGuarded(() async {
await Rhttp.init();
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await init(); await init();
if (App.isAndroid) {
handleLinks();
}
FlutterError.onError = (details) {
Log.error(
"Unhandled Exception", "${details.exception}\n${details.stack}");
};
runApp(const MyApp()); runApp(const MyApp());
if (App.isDesktop) { if (App.isDesktop) {
await windowManager.ensureInitialized(); await windowManager.ensureInitialized();
@@ -55,7 +44,7 @@ void main(List<String> args) {
}); });
} }
}, (error, stack) { }, (error, stack) {
Log.error("Unhandled Exception", "$error\n$stack"); Log.error("Unhandled Exception", error, stack);
}); });
}); });
} }
@@ -156,50 +145,44 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
home = const MainPage(); home = const MainPage();
} }
return DynamicColorBuilder(builder: (light, dark) { return DynamicColorBuilder(builder: (light, dark) {
Color? primary, secondary, tertiary;
if (appdata.settings['color'] != 'system' || if (appdata.settings['color'] != 'system' ||
light == null || light == null ||
dark == null) { dark == null) {
var color = translateColorSetting(); primary = translateColorSetting();
light = ColorScheme.fromSeed(
seedColor: color,
surface: Colors.white,
);
dark = ColorScheme.fromSeed(
seedColor: color,
brightness: Brightness.dark,
surface: Colors.black,
);
} else { } else {
light = ColorScheme.fromSeed( primary = light.primary;
seedColor: light.primary, secondary = light.secondary;
surface: Colors.white, tertiary = light.tertiary;
);
dark = ColorScheme.fromSeed(
seedColor: dark.primary,
brightness: Brightness.dark,
surface: Colors.black,
);
} }
return MaterialApp( return MaterialApp(
home: home, home: home,
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
theme: ThemeData( theme: ThemeData(
colorScheme: light, colorScheme: SeedColorScheme.fromSeeds(
fontFamily: App.isWindows ? "Microsoft YaHei" : null, primaryKey: primary,
secondaryKey: secondary,
tertiaryKey: tertiary,
tones: FlexTones.vividBackground(Brightness.light),
),
), ),
navigatorKey: App.rootNavigatorKey, navigatorKey: App.rootNavigatorKey,
darkTheme: ThemeData( darkTheme: ThemeData(
colorScheme: dark, colorScheme: SeedColorScheme.fromSeeds(
fontFamily: App.isWindows ? "Microsoft YaHei" : null, 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,
_ => ThemeMode.system _ => ThemeMode.system
}, },
localizationsDelegates: const [ localizationsDelegates: [
GlobalMaterialLocalizations.delegate, GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
], ],
locale: () { locale: () {
@@ -215,9 +198,9 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
}; };
}(), }(),
supportedLocales: const [ supportedLocales: const [
Locale('en'),
Locale('zh', 'CN'), Locale('zh', 'CN'),
Locale('zh', 'TW'), Locale('zh', 'TW'),
Locale('en'),
], ],
builder: (context, widget) { builder: (context, widget) {
ErrorWidget.builder = (details) { ErrorWidget.builder = (details) {

View File

@@ -1,5 +1,5 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:dio/dio.dart'; import 'package:venera/network/app_dio.dart';
class NetworkCache { class NetworkCache {
final Uri uri; final Uri uri;
@@ -117,7 +117,7 @@ class NetworkCacheManager implements Interceptor {
var o = options.copyWith( var o = options.copyWith(
method: "HEAD", method: "HEAD",
); );
var dio = Dio(); var dio = AppDio();
var response = await dio.fetch(o); var response = await dio.fetch(o);
if (response.statusCode == 200 && if (response.statusCode == 200 &&
compareHeaders(cache.responseHeaders, response.headers.map)) { compareHeaders(cache.responseHeaders, response.headers.map)) {

View File

@@ -487,6 +487,7 @@ Future<Res<T>> runWithRetry<T>(Future<T> Function() task,
if (i == retry - 1) { if (i == retry - 1) {
return Res.error(e.toString()); return Res.error(e.toString());
} }
await Future.delayed(Duration(seconds: i + 1));
} }
} }
throw UnimplementedError(); throw UnimplementedError();

View File

@@ -3,80 +3,128 @@ 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/appdata.dart';
import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/comic_source/comic_source.dart';
import 'package:venera/foundation/state_controller.dart';
import 'package:venera/pages/ranking_page.dart'; import 'package:venera/pages/ranking_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/ext.dart';
import 'package:venera/utils/translations.dart'; import 'package:venera/utils/translations.dart';
import 'category_comics_page.dart'; import 'category_comics_page.dart';
import 'comic_source_page.dart';
class CategoriesPage extends StatelessWidget { class CategoriesPage extends StatefulWidget {
const CategoriesPage({super.key}); const CategoriesPage({super.key});
@override
State<CategoriesPage> createState() => _CategoriesPageState();
}
class _CategoriesPageState extends State<CategoriesPage> {
var categories = <String>[];
void onSettingsChanged() {
var categories =
List.from(appdata.settings["categories"]).whereType<String>().toList();
var allCategories = ComicSource.all()
.map((e) => e.categoryData?.key)
.where((element) => element != null)
.map((e) => e!)
.toList();
categories =
categories.where((element) => allCategories.contains(element)).toList();
if (!categories.isEqualsTo(this.categories)) {
setState(() {
this.categories = categories;
});
}
}
@override
void initState() {
super.initState();
var categories =
List.from(appdata.settings["categories"]).whereType<String>().toList();
var allCategories = ComicSource.all()
.map((e) => e.categoryData?.key)
.where((element) => element != null)
.map((e) => e!)
.toList();
this.categories =
categories.where((element) => allCategories.contains(element)).toList();
appdata.settings.addListener(onSettingsChanged);
}
void addPage() {
showPopUpWidget(App.rootContext, setCategoryPagesWidget());
}
@override
void dispose() {
super.dispose();
appdata.settings.removeListener(onSettingsChanged);
}
Widget buildEmpty() {
var msg = "No Category Pages".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 = addPage;
}
return NetworkError(
message: msg,
retry: onTap,
withAppbar: false,
buttonText: "Manage".tl,
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return StateBuilder<SimpleController>( if (categories.isEmpty) {
tag: "category", return buildEmpty();
init: SimpleController(), }
builder: (controller) {
var categories = List.from(appdata.settings["categories"]);
var allCategories = ComicSource.all()
.map((e) => e.categoryData?.key)
.where((element) => element != null)
.map((e) => e!)
.toList();
categories = categories
.where((element) => allCategories.contains(element))
.toList();
if(categories.isEmpty) { return Material(
var msg = "No Category Pages".tl; child: DefaultTabController(
msg += '\n'; length: categories.length,
if(ComicSource.isEmpty) { key: Key(categories.toString()),
msg += "Add a comic source in home page".tl; child: Column(
} else { children: [
msg += "Please check your settings".tl; AppTabBar(
} key: PageStorageKey(categories.toString()),
return NetworkError( tabs: categories.map((e) {
message: msg, String title = e;
retry: () { try {
controller.update(); title = getCategoryDataWithKey(e).title;
}, } catch (e) {
withAppbar: false, //
); }
} return Tab(
text: title,
return Material( key: Key(e),
child: DefaultTabController( );
length: categories.length, }).toList(),
key: Key(categories.toString()), actionButton: TabActionButton(
child: Column( icon: const Icon(Icons.add),
children: [ text: "Add".tl,
FilledTabBar( onPressed: addPage,
key: PageStorageKey(categories.toString()), ),
tabs: categories.map((e) { ).paddingTop(context.padding.top),
String title = e; Expanded(
try { child: TabBarView(
title = getCategoryDataWithKey(e).title; children: categories.map((e) => _CategoryPage(e)).toList(),
} catch (e) { ),
// )
} ],
return Tab( ),
text: title, ),
key: Key(e),
);
}).toList(),
).paddingTop(context.padding.top),
Expanded(
child: TabBarView(
children:
categories.map((e) => _CategoryPage(e)).toList()),
)
],
),
),
);
},
); );
} }
} }

View File

@@ -1283,7 +1283,9 @@ class _ComicThumbnailsState extends State<_ComicThumbnails> {
y2 = double.parse(r.split('-')[1]); y2 = double.parse(r.split('-')[1]);
} }
} }
} finally {} } catch (_) {
// ignore
}
part = ImagePart(x1: x1, y1: y1, x2: x2, y2: y2); part = ImagePart(x1: x1, y1: y1, x2: x2, y2: y2);
} }
return Padding( return Padding(
@@ -1297,30 +1299,29 @@ class _ComicThumbnailsState extends State<_ComicThumbnails> {
child: InkWell( child: InkWell(
onTap: () => state.read(null, index + 1), onTap: () => state.read(null, index + 1),
borderRadius: borderRadius:
const BorderRadius.all(Radius.circular(16)), const BorderRadius.all(Radius.circular(8)),
child: Container( child: Container(
decoration: BoxDecoration( foregroundDecoration: BoxDecoration(
borderRadius: borderRadius: BorderRadius.circular(8),
const BorderRadius.all(Radius.circular(16)),
border: Border.all( border: Border.all(
color: Theme.of(context).colorScheme.outline, color: Theme.of(context).colorScheme.outline,
), ),
), ),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
),
width: double.infinity, width: double.infinity,
height: double.infinity, height: double.infinity,
child: ClipRRect( clipBehavior: Clip.antiAlias,
borderRadius: child: AnimatedImage(
const BorderRadius.all(Radius.circular(16)), image: CachedImageProvider(
child: AnimatedImage( url,
image: CachedImageProvider( sourceKey: state.widget.sourceKey,
url,
sourceKey: state.widget.sourceKey,
),
fit: BoxFit.contain,
width: double.infinity,
height: double.infinity,
part: part,
), ),
fit: BoxFit.contain,
width: double.infinity,
height: double.infinity,
part: part,
), ),
), ),
), ),
@@ -1336,7 +1337,7 @@ class _ComicThumbnailsState extends State<_ComicThumbnails> {
), ),
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 200, maxCrossAxisExtent: 200,
childAspectRatio: 0.65, childAspectRatio: 0.68,
), ),
), ),
if (error != null) if (error != null)
@@ -2000,6 +2001,7 @@ class _ComicPageLoadingPlaceHolder extends StatelessWidget {
} }
return Shimmer( return Shimmer(
color: context.isDarkMode ? Colors.grey.shade700 : Colors.white,
child: Column( child: Column(
children: [ children: [
Appbar(title: Text(""), backgroundColor: context.colorScheme.surface), Appbar(title: Text(""), backgroundColor: context.colorScheme.surface),

View File

@@ -19,8 +19,7 @@ class ComicSourcePage extends StatefulWidget {
return 0; return 0;
} }
var dio = AppDio(); var dio = AppDio();
var res = await dio.get<String>( var res = await dio.get<String>(appdata.settings['comicSourceListUrl']);
"https://raw.githubusercontent.com/venera-app/venera-configs/master/index.json");
if (res.statusCode != 200) { if (res.statusCode != 200) {
return -1; return -1;
} }
@@ -298,10 +297,10 @@ class _BodyState extends State<_Body> {
// //
} }
} }
context.to(() => _EditFilePage(source.filePath)).then((value) async { context.to(() => _EditFilePage(source.filePath, () async {
await ComicSource.reload(); await ComicSource.reload();
setState(() {}); setState(() {});
}); }));
} }
static Future<void> update(ComicSource source) async { static Future<void> update(ComicSource source) async {
@@ -419,7 +418,8 @@ class _BodyState extends State<_Body> {
} }
void help() { void help() {
launchUrlString("https://github.com/venera-app/venera/blob/master/doc/comic_source.md"); launchUrlString(
"https://github.com/venera-app/venera/blob/master/doc/comic_source.md");
} }
Future<void> handleAddSource(String url) async { Future<void> handleAddSource(String url) async {
@@ -521,18 +521,29 @@ class _ComicSourceListState extends State<_ComicSourceList> {
var key = json![index]["key"]; var key = json![index]["key"];
var action = currentKey.contains(key) var action = currentKey.contains(key)
? const Icon(Icons.check, size: 20).paddingRight(8) ? const Icon(Icons.check, size: 20).paddingRight(8)
: Tooltip( : Button.filled(
message: "Add", child: Text("Add".tl),
child: Button.icon( onPressed: () async {
color: context.colorScheme.primary, var fileName = json![index]["fileName"];
icon: const Icon(Icons.add), var url = json![index]["url"];
onPressed: () async { if (url == null || !(url.toString()).isURL) {
await widget.onAdd( var listUrl =
"https://raw.githubusercontent.com/venera-app/venera-configs/master/${json![index]["fileName"]}"); appdata.settings['comicSourceListUrl'] as String;
setState(() {}); if (listUrl
}, .replaceFirst("https://", "")
), .replaceFirst("http://", "")
); .contains("/")) {
url =
listUrl.substring(0, listUrl.lastIndexOf("/") + 1) +
fileName;
} else {
url = '$listUrl/$fileName';
}
}
await widget.onAdd(url);
setState(() {});
},
).fixHeight(32);
return ListTile( return ListTile(
title: Text(json![index]["name"]), title: Text(json![index]["name"]),
@@ -617,10 +628,12 @@ void _addAllPagesWithComicSource(ComicSource source) {
} }
class _EditFilePage extends StatefulWidget { class _EditFilePage extends StatefulWidget {
const _EditFilePage(this.path); const _EditFilePage(this.path, this.onExit);
final String path; final String path;
final void Function() onExit;
@override @override
State<_EditFilePage> createState() => __EditFilePageState(); State<_EditFilePage> createState() => __EditFilePageState();
} }
@@ -637,6 +650,7 @@ class __EditFilePageState extends State<_EditFilePage> {
@override @override
void dispose() { void dispose() {
File(widget.path).writeAsStringSync(current); File(widget.path).writeAsStringSync(current);
widget.onExit();
super.dispose(); super.dispose();
} }

View File

@@ -73,6 +73,7 @@ class _CommentsPageState extends State<CommentsPage> {
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
appBar: Appbar( appBar: Appbar(
title: Text("Comments".tl), title: Text("Comments".tl),
style: AppbarStyle.shadow,
), ),
body: buildBody(context), body: buildBody(context),
); );
@@ -529,6 +530,7 @@ class _Tag {
'u' => style.underline, 'u' => style.underline,
's' => style.lineThrough, 's' => style.lineThrough,
'a' => style.withColor(context.colorScheme.primary), 'a' => style.withColor(context.colorScheme.primary),
'strong' => style.bold,
'span' => () { 'span' => () {
if (attributes.containsKey('style')) { if (attributes.containsKey('style')) {
var s = attributes['style']!; var s = attributes['style']!;
@@ -622,10 +624,14 @@ class RichCommentContent extends StatefulWidget {
class _RichCommentContentState extends State<RichCommentContent> { class _RichCommentContentState extends State<RichCommentContent> {
var textSpan = <InlineSpan>[]; var textSpan = <InlineSpan>[];
var images = <_CommentImage>[]; var images = <_CommentImage>[];
bool isRendered = false;
@override @override
void didChangeDependencies() { void didChangeDependencies() {
render(); if (!isRendered) {
render();
isRendered = true;
}
super.didChangeDependencies(); super.didChangeDependencies();
} }
@@ -670,7 +676,7 @@ class _RichCommentContentState extends State<RichCommentContent> {
attributes[attrSplits[0]] = attrSplits[1].replaceAll('"', ''); attributes[attrSplits[0]] = attrSplits[1].replaceAll('"', '');
} }
} }
const acceptedTags = ['img', 'a', 'b', 'i', 'u', 's', 'br', 'span']; const acceptedTags = ['img', 'a', 'b', 'i', 'u', 's', 'br', 'span', 'strong'];
if (acceptedTags.contains(tagName)) { if (acceptedTags.contains(tagName)) {
writeBuffer(); writeBuffer();
if (tagName == 'img') { if (tagName == 'img') {

View File

@@ -5,7 +5,9 @@ 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/foundation/res.dart'; import 'package:venera/foundation/res.dart';
import 'package:venera/foundation/state_controller.dart'; import 'package:venera/foundation/state_controller.dart';
import 'package:venera/pages/comic_source_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/ext.dart'; import 'package:venera/utils/ext.dart';
import 'package:venera/utils/translations.dart'; import 'package:venera/utils/translations.dart';
@@ -56,6 +58,10 @@ class _ExplorePageState extends State<ExplorePage>
} }
} }
void addPage() {
showPopUpWidget(App.rootContext, setExplorePagesWidget());
}
NaviPaneState? naviPane; NaviPaneState? naviPane;
@override @override
@@ -117,15 +123,21 @@ class _ExplorePageState extends State<ExplorePage>
Widget buildEmpty() { Widget buildEmpty() {
var msg = "No Explore Pages".tl; var msg = "No Explore Pages".tl;
msg += '\n'; msg += '\n';
VoidCallback onTap;
if (ComicSource.isEmpty) { if (ComicSource.isEmpty) {
msg += "Add a comic source in home page".tl; msg += "Please add some sources".tl;
onTap = () {
context.to(() => ComicSourcePage());
};
} else { } else {
msg += "Please check your settings".tl; msg += "Please check your settings".tl;
onTap = addPage;
} }
return NetworkError( return NetworkError(
message: msg, message: msg,
retry: onSettingsChanged, retry: onTap,
withAppbar: false, withAppbar: false,
buttonText: "Manage".tl,
); );
} }
@@ -137,10 +149,15 @@ class _ExplorePageState extends State<ExplorePage>
} }
Widget tabBar = Material( Widget tabBar = Material(
child: FilledTabBar( child: AppTabBar(
key: PageStorageKey(pages.toString()), key: PageStorageKey(pages.toString()),
tabs: pages.map((e) => buildTab(e)).toList(), tabs: pages.map((e) => buildTab(e)).toList(),
controller: controller, controller: controller,
actionButton: TabActionButton(
icon: const Icon(Icons.add),
text: "Add".tl,
onPressed: addPage,
),
), ),
).paddingTop(context.padding.top); ).paddingTop(context.padding.top);

View File

@@ -391,7 +391,7 @@ class _ImageFavoritesDialogState extends State<_ImageFavoritesDialog> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget tabBar = Material( Widget tabBar = Material(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
child: FilledTabBar( child: AppTabBar(
key: PageStorageKey(optionTypes), key: PageStorageKey(optionTypes),
tabs: optionTypes.map((e) => Tab(text: e.tl, key: Key(e))).toList(), tabs: optionTypes.map((e) => Tab(text: e.tl, key: Key(e))).toList(),
), ),

View File

@@ -48,7 +48,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
var readerMode = context.reader.mode; var readerMode = context.reader.mode;
if (value == 1 && showFloatingButtonValue == 0) { if (value == 1 && showFloatingButtonValue == 0) {
showFloatingButtonValue = 1; showFloatingButtonValue = 1;
_floatingButtonDragListener = _DragListener( _floatingButtonDragListener = _DragListener(
onMove: (offset) { onMove: (offset) {
if (readerMode == ReaderMode.continuousTopToBottom) { if (readerMode == ReaderMode.continuousTopToBottom) {
fABValue.value -= offset.dy; fABValue.value -= offset.dy;
@@ -845,6 +845,7 @@ class _BatteryWidgetState extends State<_BatteryWidget> {
late int _batteryLevel = 100; late int _batteryLevel = 100;
Timer? _timer; Timer? _timer;
bool _hasBattery = false; bool _hasBattery = false;
BatteryState state = BatteryState.unknown;
@override @override
void initState() { void initState() {
@@ -856,29 +857,23 @@ class _BatteryWidgetState extends State<_BatteryWidget> {
void _checkBatteryAvailability() async { void _checkBatteryAvailability() async {
try { try {
_batteryLevel = await _battery.batteryLevel; _batteryLevel = await _battery.batteryLevel;
if (_batteryLevel != -1) { state = await _battery.batteryState;
if (_batteryLevel > 0 && state != BatteryState.unknown) {
setState(() { setState(() {
_hasBattery = true; _hasBattery = true;
_timer = Timer.periodic(const Duration(seconds: 1), (timer) { });
_battery.batteryLevel.then((level) => { _timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (_batteryLevel != level) _battery.batteryLevel.then((level) {
{ if (_batteryLevel != level) {
setState(() { setState(() {
_batteryLevel = level; _batteryLevel = level;
}) });
} }
});
}); });
}); });
} else {
setState(() {
_hasBattery = false;
});
} }
} catch (e) { } catch (_) {
setState(() { // ignore
_hasBattery = false;
});
} }
} }
@@ -900,7 +895,9 @@ class _BatteryWidgetState extends State<_BatteryWidget> {
IconData batteryIcon; IconData batteryIcon;
Color batteryColor = context.colorScheme.onSurface; Color batteryColor = context.colorScheme.onSurface;
if (batteryLevel >= 96) { if (state == BatteryState.charging) {
batteryIcon = Icons.battery_charging_full;
} else if (batteryLevel >= 96) {
batteryIcon = Icons.battery_full_sharp; batteryIcon = Icons.battery_full_sharp;
} else if (batteryLevel >= 84) { } else if (batteryLevel >= 84) {
batteryIcon = Icons.battery_6_bar_sharp; batteryIcon = Icons.battery_6_bar_sharp;

View File

@@ -107,7 +107,7 @@ class _AppSettingsState extends State<AppSettings> {
actionTitle: 'Export'.tl, actionTitle: 'Export'.tl,
).toSliver(), ).toSliver(),
_CallbackSetting( _CallbackSetting(
title: "Import App Data (Please restart after success)".tl, title: "Import App Data".tl,
callback: () async { callback: () async {
var controller = showLoadingDialog(context); var controller = showLoadingDialog(context);
var file = await selectFile(ext: ['venera', 'picadata']); var file = await selectFile(ext: ['venera', 'picadata']);
@@ -126,6 +126,7 @@ class _AppSettingsState extends State<AppSettings> {
context.showMessage(message: "Failed to import data".tl); context.showMessage(message: "Failed to import data".tl);
} finally { } finally {
cacheFile.deleteIgnoreError(); cacheFile.deleteIgnoreError();
App.forceRebuild();
} }
} }
controller.close(); controller.close();

View File

@@ -30,35 +30,11 @@ class _ExploreSettingsState extends State<ExploreSettings> {
).toSliver(), ).toSliver(),
_PopupWindowSetting( _PopupWindowSetting(
title: "Explore Pages".tl, title: "Explore Pages".tl,
builder: () { builder: setExplorePagesWidget,
var pages = <String, String>{};
for (var c in ComicSource.all()) {
for (var page in c.explorePages) {
pages[page.title] = page.title;
}
}
return _MultiPagesFilter(
title: "Explore Pages".tl,
settingsIndex: "explore_pages",
pages: pages,
);
},
).toSliver(), ).toSliver(),
_PopupWindowSetting( _PopupWindowSetting(
title: "Category Pages".tl, title: "Category Pages".tl,
builder: () { builder: setCategoryPagesWidget,
var pages = <String, String>{};
for (var c in ComicSource.all()) {
if (c.categoryData != null) {
pages[c.categoryData!.key] = c.categoryData!.title;
}
}
return _MultiPagesFilter(
title: "Category Pages".tl,
settingsIndex: "categories",
pages: pages,
);
},
).toSliver(), ).toSliver(),
_PopupWindowSetting( _PopupWindowSetting(
title: "Network Favorite Pages".tl, title: "Network Favorite Pages".tl,
@@ -132,8 +108,9 @@ class _ManageBlockingWordViewState extends State<_ManageBlockingWordView> {
return PopUpWidgetScaffold( return PopUpWidgetScaffold(
title: "Keyword blocking".tl, title: "Keyword blocking".tl,
tailing: [ tailing: [
IconButton( TextButton.icon(
icon: const Icon(Icons.add), icon: const Icon(Icons.add),
label: Text("Add".tl),
onPressed: add, onPressed: add,
), ),
], ],
@@ -159,7 +136,6 @@ class _ManageBlockingWordViewState extends State<_ManageBlockingWordView> {
void add() { void add() {
showDialog( showDialog(
context: App.rootContext, context: App.rootContext,
barrierColor: Colors.black.toOpacity(0.1),
builder: (context) { builder: (context) {
var controller = TextEditingController(); var controller = TextEditingController();
String? error; String? error;
@@ -205,3 +181,31 @@ class _ManageBlockingWordViewState extends State<_ManageBlockingWordView> {
); );
} }
} }
Widget setExplorePagesWidget() {
var pages = <String, String>{};
for (var c in ComicSource.all()) {
for (var page in c.explorePages) {
pages[page.title] = page.title.ts(c.key);
}
}
return _MultiPagesFilter(
title: "Explore Pages".tl,
settingsIndex: "explore_pages",
pages: pages,
);
}
Widget setCategoryPagesWidget() {
var pages = <String, String>{};
for (var c in ComicSource.all()) {
if (c.categoryData != null) {
pages[c.categoryData!.key] = c.categoryData!.title;
}
}
return _MultiPagesFilter(
title: "Category Pages".tl,
settingsIndex: "categories",
pages: pages,
);
}

View File

@@ -376,6 +376,14 @@ class _MultiPagesFilterState extends State<_MultiPagesFilter> {
super.initState(); super.initState();
} }
@override
void dispose() {
super.dispose();
Future.microtask(() {
updateSetting();
});
}
var reorderWidgetKey = UniqueKey(); var reorderWidgetKey = UniqueKey();
var scrollController = ScrollController(); var scrollController = ScrollController();
final _key = GlobalKey(); final _key = GlobalKey();
@@ -404,7 +412,6 @@ class _MultiPagesFilterState extends State<_MultiPagesFilter> {
setState(() { setState(() {
keys = List.from(reorderFunc(keys)); keys = List.from(reorderFunc(keys));
}); });
updateSetting();
}, },
children: tiles, children: tiles,
builder: (children) { builder: (children) {
@@ -424,7 +431,11 @@ class _MultiPagesFilterState extends State<_MultiPagesFilter> {
title: widget.title, title: widget.title,
tailing: [ tailing: [
if (keys.length < widget.pages.length) if (keys.length < widget.pages.length)
IconButton(onPressed: showAddDialog, icon: const Icon(Icons.add)) TextButton.icon(
label: Text("Add".tl),
icon: const Icon(Icons.add),
onPressed: showAddDialog,
)
], ],
body: view, body: view,
); );
@@ -438,9 +449,8 @@ class _MultiPagesFilterState extends State<_MultiPagesFilter> {
setState(() { setState(() {
keys.remove(key); keys.remove(key);
}); });
updateSetting();
}, },
icon: const Icon(Icons.delete)), icon: const Icon(Icons.delete_outline)),
); );
return ListTile( return ListTile(
@@ -463,30 +473,68 @@ class _MultiPagesFilterState extends State<_MultiPagesFilter> {
canAdd[key] = value; canAdd[key] = value;
} }
}); });
var selected = <String>[];
showDialog( showDialog(
context: context, context: context,
builder: (context) { builder: (context) {
return ContentDialog( return StatefulBuilder(builder: (context, setState) {
title: "Add".tl, return ContentDialog(
content: Column( title: "Add".tl,
mainAxisSize: MainAxisSize.min, content: Column(
children: canAdd.entries mainAxisSize: MainAxisSize.min,
.map( children: canAdd.entries
(e) => ListTile( .map(
title: Text(e.value), (e) => CheckboxListTile(
key: Key(e.key), value: selected.contains(e.key),
onTap: () { title: Text(e.value),
context.pop(); key: Key(e.key),
setState(() { onChanged: (value) {
keys.add(e.key); setState(() {
}); if (value!) {
updateSetting(); selected.add(e.key);
}, } else {
), selected.remove(e.key);
}
});
},
),
)
.toList(),
),
actions: [
if (selected.length < canAdd.length)
TextButton(
child: Text("Select All".tl),
onPressed: () {
setState(() {
selected = canAdd.keys.toList();
});
},
) )
.toList(), else
), TextButton(
); child: Text("Deselect All".tl),
onPressed: () {
setState(() {
selected.clear();
});
},
),
const SizedBox(width: 8),
FilledButton(
onPressed: selected.isNotEmpty
? () {
this.setState(() {
keys.addAll(selected);
});
Navigator.pop(context);
}
: null,
child: Text("Add".tl),
),
],
);
});
}, },
); );
} }

View File

@@ -170,7 +170,39 @@ class _SettingsPageState extends State<SettingsPage> implements PopEntry {
), ),
), ),
), ),
Expanded(child: buildRight()) Expanded(
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
transitionBuilder: (child, animation) {
return LayoutBuilder(
builder: (context, constrains) {
return AnimatedBuilder(
animation: animation,
builder: (context, _) {
var width = constrains.maxWidth;
var value = animation.isForwardOrCompleted
? 1 - animation.value
: 1;
var left = width * value;
return Stack(
children: [
Positioned(
top: 0,
bottom: 0,
left: left,
width: width,
child: child,
),
],
);
},
);
},
);
},
child: buildRight(),
),
)
], ],
); );
} else { } else {
@@ -179,14 +211,13 @@ class _SettingsPageState extends State<SettingsPage> implements PopEntry {
Positioned.fill(child: buildLeft()), Positioned.fill(child: buildLeft()),
Positioned( Positioned(
left: offset, left: offset,
right: 0, width: MediaQuery.of(context).size.width,
top: 0, top: 0,
bottom: 0, bottom: 0,
child: Listener( child: Listener(
onPointerDown: handlePointerDown, onPointerDown: handlePointerDown,
child: AnimatedSwitcher( child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 200),
reverseDuration: const Duration(milliseconds: 300),
switchInCurve: Curves.fastOutSlowIn, switchInCurve: Curves.fastOutSlowIn,
switchOutCurve: Curves.fastOutSlowIn, switchOutCurve: Curves.fastOutSlowIn,
transitionBuilder: (child, animation) { transitionBuilder: (child, animation) {
@@ -198,11 +229,10 @@ class _SettingsPageState extends State<SettingsPage> implements PopEntry {
child: child, child: child,
); );
}, },
child: currentPage == -1 child: Material(
? const SizedBox( key: ValueKey(currentPage),
key: Key("1"), child: buildRight(),
) ),
: buildRight(),
), ),
), ),
) )
@@ -307,7 +337,7 @@ class _SettingsPageState extends State<SettingsPage> implements PopEntry {
} }
Widget buildRight() { Widget buildRight() {
final Widget body = switch (currentPage) { return switch (currentPage) {
-1 => const SizedBox(), -1 => const SizedBox(),
0 => const ExploreSettings(), 0 => const ExploreSettings(),
1 => const ReaderSettings(), 1 => const ReaderSettings(),
@@ -318,10 +348,6 @@ class _SettingsPageState extends State<SettingsPage> implements PopEntry {
6 => const AboutSettings(), 6 => const AboutSettings(),
_ => throw UnimplementedError() _ => throw UnimplementedError()
}; };
return Material(
child: body,
);
} }
var canPop = ValueNotifier(true); var canPop = ValueNotifier(true);

View File

@@ -55,7 +55,7 @@ class DataSync with ChangeNotifier {
} }
Future<Res<bool>> uploadData() async { Future<Res<bool>> uploadData() async {
if(isDownloading) return const Res(true); if (isDownloading) return const Res(true);
if (haveWaitingTask) return const Res(true); if (haveWaitingTask) return const Res(true);
while (isUploading) { while (isUploading) {
haveWaitingTask = true; haveWaitingTask = true;
@@ -109,7 +109,7 @@ class DataSync with ChangeNotifier {
filename += '.venera'; filename += '.venera';
var files = await client.readDir('/'); var files = await client.readDir('/');
files = files.where((e) => e.name!.endsWith('.venera')).toList(); files = files.where((e) => e.name!.endsWith('.venera')).toList();
var old = files.firstWhereOrNull( (e) => e.name!.startsWith("$time-")); var old = files.firstWhereOrNull((e) => e.name!.startsWith("$time-"));
if (old != null) { if (old != null) {
await client.remove(old.name!); await client.remove(old.name!);
} }
@@ -176,8 +176,11 @@ class DataSync with ChangeNotifier {
var files = await client.readDir('/'); var files = await client.readDir('/');
files.sort((a, b) => b.name!.compareTo(a.name!)); files.sort((a, b) => b.name!.compareTo(a.name!));
var file = files.firstWhereOrNull((e) => e.name!.endsWith('.venera')); var file = files.firstWhereOrNull((e) => e.name!.endsWith('.venera'));
if (file == null) {
throw 'No data file found';
}
var version = var version =
file!.name!.split('-').elementAtOrNull(1)?.split('.').first; file.name!.split('-').elementAtOrNull(1)?.split('.').first;
if (version != null && int.tryParse(version) != null) { if (version != null && int.tryParse(version) != null) {
var currentVersion = appdata.settings['dataVersion']; var currentVersion = appdata.settings['dataVersion'];
if (currentVersion != null && int.parse(version) <= currentVersion) { if (currentVersion != null && int.parse(version) <= currentVersion) {

View File

@@ -26,7 +26,7 @@ class Image {
var codec = await ui.instantiateImageCodec(data); var codec = await ui.instantiateImageCodec(data);
var frame = await codec.getNextFrame(); var frame = await codec.getNextFrame();
codec.dispose(); codec.dispose();
var info = await frame.image.toByteData(); var info = await frame.image.toByteData(format: ui.ImageByteFormat.rawStraightRgba);
if (info == null) { if (info == null) {
throw Exception('Failed to decode image'); throw Exception('Failed to decode image');
} }
@@ -39,6 +39,14 @@ class Image {
return image; return image;
} }
Color getPixelAtIndex(int index) {
if (index < 0 || index >= _data.length) {
throw ArgumentError(
'Invalid argument: index must be in the range of [0, ${_data.length}).');
}
return Color.fromValue(_data[index]);
}
Image copyRange(int x, int y, int width, int height) { Image copyRange(int x, int y, int width, int height) {
if (width + x > this.width) { if (width + x > this.width) {
throw ArgumentError(''' throw ArgumentError('''
@@ -176,11 +184,11 @@ class Color {
Color.fromValue(this.value); Color.fromValue(this.value);
int get r => (value >> 16) & 0xFF; int get r => value & 0xFF;
int get g => (value >> 8) & 0xFF; int get g => (value >> 8) & 0xFF;
int get b => value & 0xFF; int get b => (value >> 16) & 0xFF;
int get a => (value >> 24) & 0xFF; int get a => (value >> 24) & 0xFF;
} }

View File

@@ -1,33 +1,28 @@
import 'dart:async';
import 'dart:convert';
import 'dart:isolate'; import 'dart:isolate';
import 'package:venera/foundation/app.dart';
import 'package:pdf/widgets.dart';
import 'package:venera/foundation/local.dart'; import 'package:venera/foundation/local.dart';
import 'package:venera/utils/image.dart';
import 'package:venera/utils/io.dart'; import 'package:venera/utils/io.dart';
import 'package:zip_flutter/zip_flutter.dart';
typedef DecodeImage = Future<Image> Function(Uint8List data);
Future<void> _createPdfFromComic({ Future<void> _createPdfFromComic({
required LocalComic comic, required LocalComic comic,
required String savePath, required String savePath,
required String localPath, required String localPath,
required DecodeImage decodeImage,
}) async { }) async {
final pdf = Document( var images = <String>[];
title: comic.title,
author: comic.subTitle ?? "",
producer: "Venera",
);
pdf.document.outline;
var baseDir = comic.directory.contains('/') || comic.directory.contains('\\') var baseDir = comic.directory.contains('/') || comic.directory.contains('\\')
? comic.directory ? comic.directory
: FilePath.join(localPath, comic.directory); : FilePath.join(localPath, comic.directory);
// add cover // add cover
var imageData = File(FilePath.join(baseDir, comic.cover)).readAsBytesSync(); images.add(FilePath.join(baseDir, comic.cover));
pdf.addPage(Page(
build: (Context context) {
return Image(MemoryImage(imageData), fit: BoxFit.contain);
},
));
bool multiChapters = comic.chapters != null; bool multiChapters = comic.chapters != null;
@@ -51,42 +46,360 @@ Future<void> _createPdfFromComic({
reorderFiles(files); reorderFiles(files);
for (var file in files) { for (var file in files) {
var imageData = (file as File).readAsBytesSync(); images.add(file.path);
pdf.addPage(Page(
build: (Context context) {
return Image(MemoryImage(imageData), fit: BoxFit.contain);
},
));
} }
} else { } else {
for (var chapter in comic.chapters!.keys) { for (var chapter in comic.chapters!.keys) {
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) {
var imageData = (file as File).readAsBytesSync(); images.add(file.path);
pdf.addPage(Page(
build: (Context context) {
return Image(MemoryImage(imageData), fit: BoxFit.contain);
},
));
} }
} }
} }
final file = File(savePath); var generator = PdfGenerator(
file.writeAsBytesSync(await pdf.save()); title: comic.title,
author: comic.subtitle,
imagePaths: images,
outputPath: savePath,
decodeImage: decodeImage,
);
await generator.generate();
}
Future<Isolate> _runIsolate(
LocalComic comic, String savePath, SendPort sendPort) {
var localPath = LocalManager().path;
return Isolate.spawn<SendPort>(
(sendPort) => overrideIO(
() async {
var receivePort = ReceivePort();
sendPort.send(receivePort.sendPort);
Completer<Image>? completer;
Future<Image> decodeImage(Uint8List data) async {
if (completer != null) {
throw Exception('Another image is being decoded');
}
sendPort.send(data);
completer = Completer();
return completer!.future;
}
receivePort.listen((message) {
if (message is Image) {
if (completer == null) {
throw Exception('No image is being decoded');
}
completer!.complete(message);
completer = null;
}
});
await _createPdfFromComic(
comic: comic,
savePath: savePath,
localPath: localPath,
decodeImage: decodeImage,
);
sendPort.send(null);
},
),
sendPort,
);
} }
Future<void> createPdfFromComicIsolate({ Future<void> createPdfFromComicIsolate({
required LocalComic comic, required LocalComic comic,
required String savePath, required String savePath,
}) async { }) async {
var localPath = LocalManager().path; var receivePort = ReceivePort();
return Isolate.run(() => overrideIO(() async { SendPort? sendPort;
return await _createPdfFromComic( Isolate? isolate;
comic: comic, var completer = Completer<void>();
savePath: savePath, receivePort.listen((message) {
localPath: localPath, if (message is SendPort) {
); sendPort = message;
})); } else if (message is Uint8List) {
Image.decodeImage(message).then((image) {
sendPort!.send(image);
});
} else if (message == null) {
receivePort.close();
completer.complete();
isolate!.kill();
}
});
isolate = await _runIsolate(comic, savePath, receivePort.sendPort);
return completer.future;
}
class PdfGenerator {
final String title;
final String author;
final List<String> imagePaths;
final String outputPath;
final DecodeImage decodeImage;
// PDF文件的对象ID计数器
int _objectId = 1;
// 存储每个对象在PDF中的字节位置
final Map<int, int> _objectOffsets = {};
static const double a4Width = 595.0; // points
static const double a4Height = 842.0; // points
PdfGenerator({
required this.title,
required this.author,
required this.imagePaths,
required this.outputPath,
required this.decodeImage,
});
Future<void> generate() async {
var file = File(outputPath);
final output = file.openWrite();
int length = 0;
void write(String str) {
var data = utf8.encode(str);
output.add(data);
length += data.length;
}
void writeData(Uint8List data) {
output.add(data);
length += data.length;
}
int getCurrentLength() {
return length;
}
// 1. 写入PDF头部
write('%PDF-1.7\n%\xFF\xFF\xFF\xFF\n\n');
// 2. 写入Catalog对象
_objectOffsets[_objectId] = getCurrentLength();
write('$_objectId 0 obj\n');
write('<<\n');
write('/Type /Catalog\n');
write('/Pages ${_objectId + 1} 0 R\n');
write('>>\nendobj\n\n');
final catalogId = _objectId++;
// 3. 写入Pages对象
_objectOffsets[_objectId] = getCurrentLength();
write('$_objectId 0 obj\n');
write('<<\n');
write('/Type /Pages\n');
write('/Kids [');
final pageIds = <int>[];
for (var i = 0; i < imagePaths.length; i++) {
pageIds.add(_objectId + 1 + i * 3);
write('${_objectId + 1 + i * 3} 0 R ');
}
write(']\n');
write('/Count ${imagePaths.length}\n');
write('>>\nendobj\n\n');
final pagesId = _objectId++;
// 4. 为每个图片创建Page和Image对象
for (var i = 0; i < imagePaths.length; i++) {
final imagePath = imagePaths[i];
final image = await _getImage(imagePath);
// 写入Page对象
_objectOffsets[_objectId] = getCurrentLength();
write('$_objectId 0 obj\n');
write('<<\n');
write('/Type /Page\n');
write('/Parent $pagesId 0 R\n');
write('/Resources <<\n');
write('/XObject << /Im${i + 1} ${_objectId + 1} 0 R >>\n');
write('>>\n');
write('/MediaBox [0 0 $a4Width $a4Height]\n');
write('/Contents ${_objectId + 2} 0 R\n');
write('>>\nendobj\n\n');
_objectId++;
// 写入Image对象
_objectOffsets[_objectId] = getCurrentLength();
write('$_objectId 0 obj\n');
write('<<\n');
write('/Type /XObject\n');
write('/Subtype /Image\n');
write('/Width ${image.width}\n');
write('/Height ${image.height}\n');
write('/ColorSpace /DeviceRGB\n');
write('/BitsPerComponent 8\n');
write('/Filter /FlateDecode\n');
write('/Length ${image.data.length}\n');
write('>>\nstream\n');
writeData(image.data);
write('\nendstream\nendobj\n\n');
_objectId++;
// 写入Contents对象绘制图片的指令
_objectOffsets[_objectId] = getCurrentLength();
write('$_objectId 0 obj\n');
write('<<\n');
var stream = '';
stream += 'q\n';
// Calculate scaling factors
var scaleX = a4Width / image.width;
var scaleY = a4Height / image.height;
var scale = scaleX < scaleY ? scaleX : scaleY;
// Calculate centering offsets
var offsetX = (a4Width - (image.width * scale)) / 2;
var offsetY = (a4Height - (image.height * scale)) / 2;
// Apply transformation matrix
stream += '1 0 0 1 $offsetX $offsetY cm\n'; // Translate
stream += '${scale * image.width} 0 0 ${scale * image.height} 0 0 cm\n';
stream += '/Im${i + 1} Do\n';
stream += 'Q\n';
var streamData = utf8.encode(stream);
write('/Length ${streamData.length}\n');
write('>>\nstream\n');
writeData(streamData);
write('endstream\nendobj\n\n');
_objectId++;
}
// 5. 写入Info对象元数据
final infoId = _objectId;
_objectOffsets[_objectId] = getCurrentLength();
write('$_objectId 0 obj\n');
write('<<\n');
write('/Title <');
writeData(_toPdfString(title));
write('>\n');
write('/Author <');
writeData(_toPdfString(author));
write('>\n');
write('/Producer (venera v${App.version})\n');
write('/CreationDate (D:${_formatDateTime(DateTime.now())})\n');
write('>>\nendobj\n\n');
_objectId++;
// 6. 写入交叉引用表
final xrefOffset = getCurrentLength();
write('xref\n');
write('0 $_objectId\n');
write('0000000000 65535 f\r\n');
for (var i = 1; i < _objectId; i++) {
final offset = _objectOffsets[i]!;
write('${offset.toString().padLeft(10, '0')} 00000 n\r\n'); // 使用\r\n
}
// 7. 写入文件尾部
write('trailer\n');
write('<<\n');
write('/Size $_objectId\n');
write('/Root $catalogId 0 R\n');
write('/Info $infoId 0 R\n');
write('>>\n');
write('startxref\n');
write('$xrefOffset\n');
write('%%EOF\n');
await output.close();
}
int _codeUnitForDigit(int digit) =>
digit < 10 ? digit + 0x30 : digit + 0x61 - 10;
Uint8List _toPdfString(String str) {
Uint8List data;
try {
data = latin1.encode(str);
} catch (e) {
data = Uint8List.fromList(<int>[0xfe, 0xff] + _encodeUtf16be(str));
}
var result = <int>[];
for (final byte in data) {
result.add(_codeUnitForDigit((byte & 0xF0) >> 4));
result.add(_codeUnitForDigit(byte & 0x0F));
}
return Uint8List.fromList(result);
}
List<int> _encodeUtf16be(String str) {
const unicodeReplacementCharacterCodePoint = 0xfffd;
const unicodeByteZeroMask = 0xff;
const unicodeByteOneMask = 0xff00;
const unicodeValidRangeMax = 0x10ffff;
const unicodePlaneOneMax = 0xffff;
const unicodeUtf16ReservedLo = 0xd800;
const unicodeUtf16ReservedHi = 0xdfff;
const unicodeUtf16Offset = 0x10000;
const unicodeUtf16SurrogateUnit0Base = 0xd800;
const unicodeUtf16SurrogateUnit1Base = 0xdc00;
const unicodeUtf16HiMask = 0xffc00;
const unicodeUtf16LoMask = 0x3ff;
final encoding = <int>[];
void add(int unit) {
encoding.add((unit & unicodeByteOneMask) >> 8);
encoding.add(unit & unicodeByteZeroMask);
}
for (final unit in str.codeUnits) {
if ((unit >= 0 && unit < unicodeUtf16ReservedLo) ||
(unit > unicodeUtf16ReservedHi && unit <= unicodePlaneOneMax)) {
add(unit);
} else if (unit > unicodePlaneOneMax && unit <= unicodeValidRangeMax) {
final base = unit - unicodeUtf16Offset;
add(unicodeUtf16SurrogateUnit0Base +
((base & unicodeUtf16HiMask) >> 10));
add(unicodeUtf16SurrogateUnit1Base + (base & unicodeUtf16LoMask));
} else {
add(unicodeReplacementCharacterCodePoint);
}
}
return encoding;
}
// 格式化日期时间
String _formatDateTime(DateTime dt) {
return dt
.toUtc()
.toString()
.replaceAll('-', '')
.replaceAll(':', '')
.replaceAll(' ', '')
.replaceAll('.', '')
.substring(0, 14);
}
Future<({int width, int height, Uint8List data})> _getImage(
String imagePath) async {
var data = await File(imagePath).readAsBytes();
var image = await decodeImage(data);
var width = image.width;
var height = image.height;
data = Uint8List(width * height * 3);
for (var i = 0; i < width * height; i++) {
var pixel = image.getPixelAtIndex(i);
data[i * 3] = pixel.r;
data[i * 3 + 1] = pixel.g;
data[i * 3 + 2] = pixel.b;
}
data = tdeflCompressData(data, true, true, 9);
return (width: width, height: height, data: data);
}
} }

View File

@@ -33,14 +33,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.4" version: "1.0.4"
archive:
dependency: transitive
description:
name: archive
sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d
url: "https://pub.dev"
source: hosted
version: "3.6.1"
args: args:
dependency: transitive dependency: transitive
description: description:
@@ -57,14 +49,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.11.0" version: "2.11.0"
barcode:
dependency: transitive
description:
name: barcode
sha256: ab180ce22c6555d77d45f0178a523669db67f95856e3378259ef2ffeb43e6003
url: "https://pub.dev"
source: hosted
version: "2.2.8"
battery_plus: battery_plus:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -81,14 +65,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.1" version: "2.0.1"
bidi:
dependency: transitive
description:
name: bidi
sha256: "9a712c7ddf708f7c41b1923aa83648a3ed44cfd75b04f72d598c45e5be287f9d"
url: "https://pub.dev"
source: hosted
version: "2.0.12"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@@ -298,6 +274,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.1" version: "1.1.1"
flex_seed_scheme:
dependency: "direct main"
description:
name: flex_seed_scheme
sha256: d3ba3c5c92d2d79d45e94b4c6c71d01fac3c15017da1545880c53864da5dfeb0
url: "https://pub.dev"
source: hosted
version: "3.5.0"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@@ -417,8 +401,8 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "."
ref: "598d50572a658f8e04775566fe3789954d9a01e3" ref: "5978d0c7784fbbefcacc573547f0ab01ba59b7b3"
resolved-ref: "598d50572a658f8e04775566fe3789954d9a01e3" resolved-ref: "5978d0c7784fbbefcacc573547f0ab01ba59b7b3"
url: "https://github.com/wgh136/flutter_qjs" url: "https://github.com/wgh136/flutter_qjs"
source: git source: git
version: "0.3.7" version: "0.3.7"
@@ -521,14 +505,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.1.0" version: "0.1.0"
image:
dependency: transitive
description:
name: image
sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d
url: "https://pub.dev"
source: hosted
version: "4.3.0"
intl: intl:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -690,14 +666,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.9.0" version: "1.9.0"
path_parsing:
dependency: transitive
description:
name: path_parsing
sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
path_provider: path_provider:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -746,14 +714,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.0" version: "2.3.0"
pdf:
dependency: "direct main"
description:
name: pdf
sha256: "05df53f8791587402493ac97b9869d3824eccbc77d97855f4545cf72df3cae07"
url: "https://pub.dev"
source: hosted
version: "3.11.1"
petitparser: petitparser:
dependency: transitive dependency: transitive
description: description:
@@ -795,14 +755,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.9.1" version: "3.9.1"
qr:
dependency: transitive
description:
name: qr
sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
rhttp: rhttp:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -977,14 +929,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.3" version: "0.7.3"
text_scroll:
dependency: "direct main"
description:
name: text_scroll
sha256: "7869d86a6fdd725dee56bdd150216a99f0372b82fbfcac319214dbd5f36e1908"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@@ -1139,7 +1083,7 @@ packages:
source: hosted source: hosted
version: "6.5.0" version: "6.5.0"
yaml: yaml:
dependency: "direct main" dependency: transitive
description: description:
name: yaml name: yaml
sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5"
@@ -1150,10 +1094,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: zip_flutter name: zip_flutter
sha256: ea7fdc86c988174ef3bb80dc26e8e8bfdf634c55930e2d18d7e77e991acf0483 sha256: fe63ef9098bb2426b001adba2e28029820d71ce80cce957a36676bd6b3227245
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.0.8" version: "0.0.9"
sdks: sdks:
dart: ">=3.6.0 <4.0.0" dart: ">=3.6.0 <4.0.0"
flutter: ">=3.27.2" flutter: ">=3.27.3"

View File

@@ -2,18 +2,16 @@ name: venera
description: "A comic app." description: "A comic app."
publish_to: 'none' publish_to: 'none'
version: 1.2.1+121 version: 1.2.2+122
environment: environment:
sdk: '>=3.6.0 <4.0.0' sdk: '>=3.6.0 <4.0.0'
flutter: 3.27.2 flutter: 3.27.3
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
path_provider: any path_provider: any
flutter_localizations:
sdk: flutter
intl: ^0.19.0 intl: ^0.19.0
window_manager: ^0.4.3 window_manager: ^0.4.3
sqlite3: ^2.4.7 sqlite3: ^2.4.7
@@ -21,11 +19,11 @@ dependencies:
flutter_qjs: flutter_qjs:
git: git:
url: https://github.com/wgh136/flutter_qjs url: https://github.com/wgh136/flutter_qjs
ref: 598d50572a658f8e04775566fe3789954d9a01e3 ref: 5978d0c7784fbbefcacc573547f0ab01ba59b7b3
crypto: ^3.0.6 crypto: ^3.0.6
dio: ^5.7.0 dio: ^5.7.0
html: ^0.15.5 html: ^0.15.5
pointycastle: any pointycastle: ^3.9.1
url_launcher: ^6.3.0 url_launcher: ^6.3.0
path: ^1.9.0 path: ^1.9.0
photo_view: photo_view:
@@ -40,7 +38,6 @@ dependencies:
ref: 09e756b1f1b04e6298318d99ec20a787fb360f59 ref: 09e756b1f1b04e6298318d99ec20a787fb360f59
path: packages/scrollable_positioned_list path: packages/scrollable_positioned_list
flutter_reorderable_grid_view: ^5.4.0 flutter_reorderable_grid_view: ^5.4.0
yaml: any
uuid: ^4.5.1 uuid: ^4.5.1
desktop_webview_window: desktop_webview_window:
git: git:
@@ -51,7 +48,7 @@ dependencies:
sliver_tools: ^0.2.12 sliver_tools: ^0.2.12
flutter_file_dialog: ^3.0.2 flutter_file_dialog: ^3.0.2
file_selector: ^1.0.3 file_selector: ^1.0.3
zip_flutter: ^0.0.8 zip_flutter: ^0.0.9
lodepng_flutter: lodepng_flutter:
git: git:
url: https://github.com/venera-app/lodepng_flutter url: https://github.com/venera-app/lodepng_flutter
@@ -67,16 +64,17 @@ dependencies:
git: git:
url: https://github.com/pkuislm/flutter_saf.git url: https://github.com/pkuislm/flutter_saf.git
ref: 7637b8b67d0a831f3cd7e702b8173e300880d32e ref: 7637b8b67d0a831f3cd7e702b8173e300880d32e
pdf: ^3.11.1
dynamic_color: ^1.7.0 dynamic_color: ^1.7.0
shimmer_animation: ^2.1.0 shimmer_animation: ^2.1.0
flutter_memory_info: ^0.0.1 flutter_memory_info: ^0.0.1
syntax_highlight: ^0.4.0 syntax_highlight: ^0.4.0
text_scroll: ^0.2.0
flutter_7zip: flutter_7zip:
git: git:
url: https://github.com/wgh136/flutter_7zip url: https://github.com/wgh136/flutter_7zip
ref: b33344797f1d2469339e0e1b75f5f954f1da224c ref: b33344797f1d2469339e0e1b75f5f954f1da224c
flex_seed_scheme: ^3.5.0
flutter_localizations:
sdk: flutter
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: