Merge pull request #317 from venera-app/v1.4.0-dev

V1.4.0
This commit is contained in:
nyne
2025-04-05 22:06:21 +08:00
committed by GitHub
42 changed files with 893 additions and 454 deletions

View File

@@ -67,7 +67,6 @@ android {
} }
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.github.wgh136.venera" applicationId = "com.github.wgh136.venera"
// You can update the following values to match your application needs. // You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
@@ -125,6 +124,6 @@ flutter {
} }
dependencies { dependencies {
implementation "androidx.activity:activity-ktx:1.9.2" implementation "androidx.activity:activity-ktx:1.10.1"
implementation 'androidx.documentfile:documentfile:1.0.1' implementation 'androidx.documentfile:documentfile:1.0.1'
} }

View File

@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip

View File

@@ -18,7 +18,7 @@ pluginManagement {
plugins { plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version '8.3.2' apply false 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 "1.8.10" apply false
} }

View File

@@ -140,18 +140,18 @@
"Block": "屏蔽", "Block": "屏蔽",
"Add new favorite to": "添加新收藏到", "Add new favorite to": "添加新收藏到",
"Move favorite after reading": "阅读后移动收藏", "Move favorite after reading": "阅读后移动收藏",
"Delete folder?" : "删除文件夹?", "Delete folder?": "删除文件夹?",
"Delete folder '@f' ?" : "删除文件夹 '@f' ?", "Delete folder '@f' ?": "删除文件夹 '@f' ?",
"Import from file": "从文件导入", "Import from file": "从文件导入",
"Failed to import": "导入失败", "Failed to import": "导入失败",
"Cache Limit": "缓存限制", "Cache Limit": "缓存限制",
"Set Cache Limit": "设置缓存限制", "Set Cache Limit": "设置缓存限制",
"Size in MB": "大小MB", "Size in MB": "大小MB",
"Select a directory which contains the comic directories." : "选择一个包含漫画文件夹的目录", "Select a directory which contains the comic directories.": "选择一个包含漫画文件夹的目录",
"Help": "帮助", "Help": "帮助",
"Export as cbz": "导出为cbz", "Export as cbz": "导出为cbz",
"Select an archive file (cbz, zip, 7z, cb7)" : "选择一个归档文件 (cbz, zip, 7z, cb7)", "Select an archive file (cbz, zip, 7z, cb7)": "选择一个归档文件 (cbz, zip, 7z, cb7)",
"An archive file" : "一个归档文件", "An archive file": "一个归档文件",
"Fullscreen": "全屏", "Fullscreen": "全屏",
"Exit": "退出", "Exit": "退出",
"View more": "查看更多", "View more": "查看更多",
@@ -198,9 +198,9 @@
"Long press on the favorite button to quickly add to this folder": "长按收藏按钮快速添加到这个文件夹", "Long press on the favorite button to quickly add to this folder": "长按收藏按钮快速添加到这个文件夹",
"Added": "已添加", "Added": "已添加",
"Turn page by volume keys": "使用音量键翻页", "Turn page by volume keys": "使用音量键翻页",
"Display time & battery info in reader":"在阅读器中显示时间和电量信息", "Display time & battery info in reader": "在阅读器中显示时间和电量信息",
"EhViewer downloads":"EhViewer下载", "EhViewer downloads": "EhViewer下载",
"Select an EhViewer database and a download folder.":"选择EhViewer的下载数据导出的db文件与存放下载内容的目录", "Select an EhViewer database and a download folder.": "选择EhViewer的下载数据导出的db文件与存放下载内容的目录",
"(EhViewer)Default": "(EhViewer)默认", "(EhViewer)Default": "(EhViewer)默认",
"If you import an EhViewer's database, program will automatically create folders according to the download label in that database.": "若通过EhViewer数据库导入漫画程序将会按其中的下载标签自动创建收藏文件夹。", "If you import an EhViewer's database, program will automatically create folders according to the download label in that database.": "若通过EhViewer数据库导入漫画程序将会按其中的下载标签自动创建收藏文件夹。",
"Multi-Select": "进入多选模式", "Multi-Select": "进入多选模式",
@@ -241,7 +241,7 @@
"Delete all unavailable local favorite items": "删除所有无效的本地收藏", "Delete all unavailable local favorite items": "删除所有无效的本地收藏",
"Deleted @a favorite items.": "已删除 @a 条无效收藏", "Deleted @a favorite items.": "已删除 @a 条无效收藏",
"New version available": "有新版本可用", "New version available": "有新版本可用",
"A new version is available. Do you want to update now?" : "有新版本可用。您要现在更新吗?", "A new version is available. Do you want to update now?": "有新版本可用。您要现在更新吗?",
"No new version available": "没有新版本可用", "No new version available": "没有新版本可用",
"Export as pdf": "导出为pdf", "Export as pdf": "导出为pdf",
"Export as epub": "导出为epub", "Export as epub": "导出为epub",
@@ -288,15 +288,15 @@
"Copy the title successfully": "复制标题成功", "Copy the title successfully": "复制标题成功",
"The comic is invalid, please long press to delete, you can double click the title to copy": "该漫画已失效, 请长按删除, 可以双击标题进行复制", "The comic is invalid, please long press to delete, you can double click the title to copy": "该漫画已失效, 请长按删除, 可以双击标题进行复制",
"No search results found": "未找到搜索结果", "No search results found": "未找到搜索结果",
"Added @c comics to download queue." : "已添加 @c 本漫画到下载队列", "Added @c comics to download queue.": "已添加 @c 本漫画到下载队列",
"Download started": "下载已开始", "Download started": "下载已开始",
"Click favorite": "点击收藏", "Click favorite": "点击收藏",
"End": "末尾", "End": "末尾",
"None": "无", "None": "无",
"View Detail": "查看详情", "View Detail": "查看详情",
"Select a directory which contains multiple archive files." : "选择一个包含多个归档文件的目录", "Select a directory which contains multiple archive files.": "选择一个包含多个归档文件的目录",
"Multiple archive files" : "多个归档文件", "Multiple archive files": "多个归档文件",
"No valid comics found" : "未找到有效的漫画", "No valid comics found": "未找到有效的漫画",
"Enable DNS Overrides": "启用DNS覆写", "Enable DNS Overrides": "启用DNS覆写",
"DNS Overrides": "DNS覆写", "DNS Overrides": "DNS覆写",
"Custom Image Processing": "自定义图片处理", "Custom Image Processing": "自定义图片处理",
@@ -342,12 +342,12 @@
"Replies": "回复", "Replies": "回复",
"Follow Updates": "追更", "Follow Updates": "追更",
"Not Configured": "未配置", "Not Configured": "未配置",
"Choose a folder to follow updates." : "选择一个文件夹以追更", "Choose a folder to follow updates.": "选择一个文件夹以追更",
"Choose Folder": "选择文件夹", "Choose Folder": "选择文件夹",
"No folders available": "没有可用的文件夹", "No folders available": "没有可用的文件夹",
"Updating comics...": "更新漫画中...", "Updating comics...": "更新漫画中...",
"Automatic update checking enabled." : "已启用自动更新检查", "Automatic update checking enabled.": "已启用自动更新检查",
"The app will check for updates at most once a day." : "APP将每天最多检查一次更新", "The app will check for updates at most once a day.": "APP将每天最多检查一次更新",
"Change Folder": "更改文件夹", "Change Folder": "更改文件夹",
"Check Now": "立即检查", "Check Now": "立即检查",
"Updates": "更新", "Updates": "更新",
@@ -360,7 +360,7 @@
"Disabled": "已禁用", "Disabled": "已禁用",
"Auto Sync Data": "自动同步数据", "Auto Sync Data": "自动同步数据",
"Mark all as read": "全部标记为已读", "Mark all as read": "全部标记为已读",
"Do you want to mark all as read?" : "您要全部标记为已读吗?", "Do you want to mark all as read?": "您要全部标记为已读吗?",
"Swipe down for previous chapter": "向下滑动查看上一章", "Swipe down for previous chapter": "向下滑动查看上一章",
"Swipe up for next chapter": "向上滑动查看下一章", "Swipe up for next chapter": "向上滑动查看下一章",
"Initial Page": "初始页面", "Initial Page": "初始页面",
@@ -378,7 +378,16 @@
"Page": "页面", "Page": "页面",
"Jump": "跳转", "Jump": "跳转",
"Copy Image": "复制图片", "Copy Image": "复制图片",
"A valid WebDav directory URL": "有效的WebDav目录URL" "A valid WebDav directory URL": "有效的WebDav目录URL",
"Shut Down": "关闭",
"Uploading data...": "正在上传数据...",
"Pages": "页数",
"Long press zoom position": "长按缩放位置",
"Press position": "按压位置",
"Screen center": "屏幕中心",
"Suggestions": "建议",
"Do not report any issues related to sources to App repo.": "请不要向App仓库报告任何与源相关的问题",
"Click the setting icon to change the source list url.": "点击设置图标更改源列表URL"
}, },
"zh_TW": { "zh_TW": {
"Home": "首頁", "Home": "首頁",
@@ -520,18 +529,18 @@
"Block": "封鎖", "Block": "封鎖",
"Add new favorite to": "添加新收藏到", "Add new favorite to": "添加新收藏到",
"Move favorite after reading": "閱讀後移動收藏", "Move favorite after reading": "閱讀後移動收藏",
"Delete folder?" : "刪除資料夾?", "Delete folder?": "刪除資料夾?",
"Delete folder '@f' ?" : "刪除資料夾 '@f' ", "Delete folder '@f' ?": "刪除資料夾 '@f' ",
"Import from file": "從文件匯入", "Import from file": "從文件匯入",
"Failed to import": "匯入失敗", "Failed to import": "匯入失敗",
"Cache Limit": "快取限制", "Cache Limit": "快取限制",
"Set Cache Limit": "設定快取限制", "Set Cache Limit": "設定快取限制",
"Size in MB": "大小MB", "Size in MB": "大小MB",
"Select a directory which contains the comic directories." : "選擇一個包含漫畫資料夾的目錄", "Select a directory which contains the comic directories.": "選擇一個包含漫畫資料夾的目錄",
"Help": "幫助", "Help": "幫助",
"Export as cbz": "匯出為cbz", "Export as cbz": "匯出為cbz",
"Select an archive file (cbz, zip, 7z, cb7)" : "選擇一個歸檔文件 (cbz, zip, 7z, cb7)", "Select an archive file (cbz, zip, 7z, cb7)": "選擇一個歸檔文件 (cbz, zip, 7z, cb7)",
"An archive file" : "一個歸檔文件", "An archive file": "一個歸檔文件",
"Fullscreen": "全螢幕", "Fullscreen": "全螢幕",
"Exit": "退出", "Exit": "退出",
"View more": "查看更多", "View more": "查看更多",
@@ -622,13 +631,13 @@
"Delete all unavailable local favorite items": "刪除所有無效的本機收藏", "Delete all unavailable local favorite items": "刪除所有無效的本機收藏",
"Deleted @a favorite items.": "已刪除 @a 條無效收藏", "Deleted @a favorite items.": "已刪除 @a 條無效收藏",
"New version available": "有新版本可用", "New version available": "有新版本可用",
"A new version is available. Do you want to update now?" : "有新版本可用。您要現在更新嗎?", "A new version is available. Do you want to update now?": "有新版本可用。您要現在更新嗎?",
"No new version available": "沒有新版本可用", "No new version available": "沒有新版本可用",
"Export as pdf": "匯出為pdf", "Export as pdf": "匯出為pdf",
"Export as epub": "匯出為epub", "Export as epub": "匯出為epub",
"Aggregated Search": "聚合搜尋", "Aggregated Search": "聚合搜尋",
"No search results found": "未找到搜尋結果", "No search results found": "未找到搜尋結果",
"Added @c comics to download queue." : "已添加 @c 本漫畫到下載佇列", "Added @c comics to download queue.": "已添加 @c 本漫畫到下載佇列",
"Download started": "下載已開始", "Download started": "下載已開始",
"Click favorite": "點擊收藏", "Click favorite": "點擊收藏",
"Local comic collection is not supported at present": "本機收藏暫不支援", "Local comic collection is not supported at present": "本機收藏暫不支援",
@@ -675,9 +684,9 @@
"End": "末尾", "End": "末尾",
"None": "無", "None": "無",
"View Detail": "查看詳情", "View Detail": "查看詳情",
"Select a directory which contains multiple archive files." : "選擇一個包含多個歸檔文件的目錄", "Select a directory which contains multiple archive files.": "選擇一個包含多個歸檔文件的目錄",
"Multiple archive files" : "多個歸檔文件", "Multiple archive files": "多個歸檔文件",
"No valid comics found" : "未找到有效的漫畫", "No valid comics found": "未找到有效的漫畫",
"Enable DNS Overrides": "啟用DNS覆寫", "Enable DNS Overrides": "啟用DNS覆寫",
"DNS Overrides": "DNS覆寫", "DNS Overrides": "DNS覆寫",
"Custom Image Processing": "自訂圖片處理", "Custom Image Processing": "自訂圖片處理",
@@ -723,12 +732,12 @@
"Replies": "回覆", "Replies": "回覆",
"Follow Updates": "追更", "Follow Updates": "追更",
"Not Configured": "未配置", "Not Configured": "未配置",
"Choose a folder to follow updates." : "選擇一個資料夾以追更", "Choose a folder to follow updates.": "選擇一個資料夾以追更",
"Choose Folder": "選擇資料夾", "Choose Folder": "選擇資料夾",
"No folders available": "沒有可用的資料夾", "No folders available": "沒有可用的資料夾",
"Updating comics...": "更新漫畫中...", "Updating comics...": "更新漫畫中...",
"Automatic update checking enabled." : "已啟用自動更新檢查", "Automatic update checking enabled.": "已啟用自動更新檢查",
"The app will check for updates at most once a day." : "APP將每天最多檢查一次更新", "The app will check for updates at most once a day.": "APP將每天最多檢查一次更新",
"Change Folder": "更改資料夾", "Change Folder": "更改資料夾",
"Check Now": "立即檢查", "Check Now": "立即檢查",
"Updates": "更新", "Updates": "更新",
@@ -741,7 +750,7 @@
"Disabled": "已停用", "Disabled": "已停用",
"Auto Sync Data": "自動同步資料", "Auto Sync Data": "自動同步資料",
"Mark all as read": "全部標記為已讀", "Mark all as read": "全部標記為已讀",
"Do you want to mark all as read?" : "您要全部標記為已讀嗎?", "Do you want to mark all as read?": "您要全部標記為已讀嗎?",
"Swipe down for previous chapter": "向下滑動查看上一章", "Swipe down for previous chapter": "向下滑動查看上一章",
"Swipe up for next chapter": "向上滑動查看下一章", "Swipe up for next chapter": "向上滑動查看下一章",
"Initial Page": "初始頁面", "Initial Page": "初始頁面",
@@ -759,6 +768,15 @@
"Page": "頁面", "Page": "頁面",
"Jump": "跳轉", "Jump": "跳轉",
"Copy Image": "複製圖片", "Copy Image": "複製圖片",
"A valid WebDav directory URL": "有效的WebDav目錄URL" "A valid WebDav directory URL": "有效的WebDav目錄URL",
"Shut Down": "關閉",
"Uploading data...": "正在上傳數據...",
"Pages": "頁數",
"Long press zoom position": "長按縮放位置",
"Press position": "按壓位置",
"Screen center": "螢幕中心",
"Suggestions": "建議",
"Do not report any issues related to sources to App repo.": "請不要向App倉庫報告任何與源相關的問題",
"Click the setting icon to change the source list url.": "點擊設定圖示更改源列表URL"
} }
} }

BIN
debian/gui/venera.png vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 64 KiB

View File

@@ -334,7 +334,12 @@ class ComicTile extends StatelessWidget {
} }
var children = <Widget>[]; var children = <Widget>[];
for (var line in text.split('\n')) { var lines = text.split('\n');
lines.removeWhere((e) => e.trim().isEmpty);
if (lines.length > 3) {
lines = lines.sublist(0, 3);
}
for (var line in lines) {
children.add(Container( children.add(Container(
margin: const EdgeInsets.fromLTRB(2, 0, 2, 2), margin: const EdgeInsets.fromLTRB(2, 0, 2, 2),
padding: constraints.maxWidth < 80 padding: constraints.maxWidth < 80

View File

@@ -163,3 +163,29 @@ class SliverLazyToBoxAdapter extends StatelessWidget {
]); ]);
} }
} }
class SliverAnimatedVisibility extends StatelessWidget {
const SliverAnimatedVisibility({
super.key,
required this.visible,
required this.child,
});
final bool visible;
final Widget child;
@override
Widget build(BuildContext context) {
var child = visible ? this.child : const SizedBox.shrink();
return SliverToBoxAdapter(
child: AnimatedSize(
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
alignment: Alignment.topCenter,
child: child,
),
);
}
}

View File

@@ -51,10 +51,32 @@ class _SmoothScrollProviderState extends State<SmoothScrollProvider> {
static bool _isMouseScroll = App.isDesktop; static bool _isMouseScroll = App.isDesktop;
late int id;
static int _id = 0;
var activeChildren = <int>{};
ScrollState? parent;
@override @override
void initState() { void initState() {
_controller = widget.controller ?? ScrollController(); _controller = widget.controller ?? ScrollController();
super.initState(); super.initState();
id = _id;
_id++;
}
@override
void didChangeDependencies() {
parent = ScrollState.maybeOf(context);
super.didChangeDependencies();
}
@override
void dispose() {
parent?.onChildInactive(id);
super.dispose();
} }
@override @override
@@ -66,8 +88,7 @@ class _SmoothScrollProviderState extends State<SmoothScrollProvider> {
const BouncingScrollPhysics(), const BouncingScrollPhysics(),
); );
} }
return Listener( var child = Listener(
behavior: HitTestBehavior.translucent,
onPointerDown: (event) { onPointerDown: (event) {
_futurePosition = null; _futurePosition = null;
if (_isMouseScroll) { if (_isMouseScroll) {
@@ -77,6 +98,9 @@ class _SmoothScrollProviderState extends State<SmoothScrollProvider> {
} }
}, },
onPointerSignal: (pointerSignal) { onPointerSignal: (pointerSignal) {
if (activeChildren.isNotEmpty) {
return;
}
if (pointerSignal is PointerScrollEvent) { if (pointerSignal is PointerScrollEvent) {
if (HardwareKeyboard.instance.isShiftPressed) { if (HardwareKeyboard.instance.isShiftPressed) {
return; return;
@@ -113,8 +137,14 @@ class _SmoothScrollProviderState extends State<SmoothScrollProvider> {
}); });
} }
}, },
child: ScrollControllerProvider._( child: ScrollState._(
controller: _controller, controller: _controller,
onChildActive: (id) {
activeChildren.add(id);
},
onChildInactive: (id) {
activeChildren.remove(id);
},
child: widget.builder( child: widget.builder(
context, context,
_controller, _controller,
@@ -124,25 +154,49 @@ class _SmoothScrollProviderState extends State<SmoothScrollProvider> {
), ),
), ),
); );
if (parent != null) {
return MouseRegion(
onEnter: (_) {
parent!.onChildActive(id);
},
onExit: (_) {
parent!.onChildInactive(id);
},
child: child,
);
}
return child;
} }
} }
class ScrollControllerProvider extends InheritedWidget { class ScrollState extends InheritedWidget {
const ScrollControllerProvider._({ const ScrollState._({
required this.controller, required this.controller,
required super.child, required super.child,
required this.onChildActive,
required this.onChildInactive,
}); });
final ScrollController controller; final ScrollController controller;
static ScrollController of(BuildContext context) { final void Function(int id) onChildActive;
final ScrollControllerProvider? provider =
context.dependOnInheritedWidgetOfExactType<ScrollControllerProvider>(); final void Function(int id) onChildInactive;
return provider!.controller;
static ScrollState of(BuildContext context) {
final ScrollState? provider =
context.dependOnInheritedWidgetOfExactType<ScrollState>();
return provider!;
}
static ScrollState? maybeOf(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<ScrollState>();
} }
@override @override
bool updateShouldNotify(ScrollControllerProvider oldWidget) { bool updateShouldNotify(ScrollState oldWidget) {
return oldWidget.controller != controller; return oldWidget.controller != controller;
} }
} }

View File

@@ -82,10 +82,7 @@ class _WindowFrameState extends State<WindowFrame> {
return; return;
} }
} }
windowManager.close().then((_) {
// Make sure the app exits when the window is closed.
exit(0); exit(0);
});
} }
@override @override
@@ -570,7 +567,6 @@ class _VirtualWindowFrameState extends State<VirtualWindowFrame>
boxShadow: <BoxShadow>[ boxShadow: <BoxShadow>[
BoxShadow( BoxShadow(
color: Colors.black.toOpacity(_isFocused ? 0.4 : 0.2), color: Colors.black.toOpacity(_isFocused ? 0.4 : 0.2),
offset: Offset(0.0, 2),
blurRadius: 4, blurRadius: 4,
) )
], ],

View File

@@ -13,7 +13,7 @@ export "widget_utils.dart";
export "context.dart"; export "context.dart";
class _App { class _App {
final version = "1.3.4"; final version = "1.4.0";
bool get isAndroid => Platform.isAndroid; bool get isAndroid => Platform.isAndroid;
@@ -47,6 +47,7 @@ class _App {
late String dataPath; late String dataPath;
late String cachePath; late String cachePath;
String? externalStoragePath;
final rootNavigatorKey = GlobalKey<NavigatorState>(); final rootNavigatorKey = GlobalKey<NavigatorState>();
@@ -77,6 +78,9 @@ class _App {
Future<void> init() async { Future<void> init() async {
cachePath = (await getApplicationCacheDirectory()).path; cachePath = (await getApplicationCacheDirectory()).path;
dataPath = (await getApplicationSupportDirectory()).path; dataPath = (await getApplicationSupportDirectory()).path;
if (isAndroid) {
externalStoragePath = (await getExternalStorageDirectory())!.path;
}
} }
Future<void> initComponents() async { Future<void> initComponents() async {

View File

@@ -161,6 +161,7 @@ class Settings with ChangeNotifier {
'cacheSize': 2048, // in MB 'cacheSize': 2048, // in MB
'downloadThreads': 5, 'downloadThreads': 5,
'enableLongPressToZoom': true, 'enableLongPressToZoom': true,
'longPressZoomPosition': "press", // press, center
'checkUpdateOnStart': false, 'checkUpdateOnStart': false,
'limitImageWidth': true, 'limitImageWidth': true,
'webdav': [], // empty means not configured 'webdav': [], // empty means not configured

View File

@@ -34,24 +34,28 @@ class CategoryButtonData {
}); });
} }
class CategoryItem {
final String label;
final PageJumpTarget target;
const CategoryItem(this.label, this.target);
}
abstract class BaseCategoryPart { abstract class BaseCategoryPart {
String get title; String get title;
List<String> get categories; List<CategoryItem> get categories;
List<String>? get categoryParams => null;
bool get enableRandom; bool get enableRandom;
String get categoryType;
/// Data class for building a part of category page. /// Data class for building a part of category page.
const BaseCategoryPart(); const BaseCategoryPart();
} }
class FixedCategoryPart extends BaseCategoryPart { class FixedCategoryPart extends BaseCategoryPart {
@override @override
final List<String> categories; final List<CategoryItem> categories;
@override @override
bool get enableRandom => false; bool get enableRandom => false;
@@ -59,19 +63,12 @@ class FixedCategoryPart extends BaseCategoryPart {
@override @override
final String title; final String title;
@override
final String categoryType;
@override
final List<String>? categoryParams;
/// A [BaseCategoryPart] that show fixed tags on category page. /// A [BaseCategoryPart] that show fixed tags on category page.
const FixedCategoryPart(this.title, this.categories, this.categoryType, const FixedCategoryPart(this.title, this.categories);
[this.categoryParams]);
} }
class RandomCategoryPart extends BaseCategoryPart { class RandomCategoryPart extends BaseCategoryPart {
final List<String> tags; final List<CategoryItem> all;
final int randomNumber; final int randomNumber;
@@ -81,67 +78,59 @@ class RandomCategoryPart extends BaseCategoryPart {
@override @override
bool get enableRandom => true; bool get enableRandom => true;
@override List<CategoryItem> _categories() {
final String categoryType; if (randomNumber >= all.length) {
return all;
List<String> _categories() {
if (randomNumber >= tags.length) {
return tags;
} }
var start = math.Random().nextInt(tags.length - randomNumber); var start = math.Random().nextInt(all.length - randomNumber);
return tags.sublist(start, start + randomNumber); return all.sublist(start, start + randomNumber);
} }
@override @override
List<String> get categories => _categories(); List<CategoryItem> get categories => _categories();
/// A [BaseCategoryPart] that show random tags on category page. /// A [BaseCategoryPart] that show a part of random tags on category page.
const RandomCategoryPart( const RandomCategoryPart(
this.title, this.tags, this.randomNumber, this.categoryType); this.title,
this.all,
this.randomNumber,
);
} }
class RandomCategoryPartWithRuntimeData extends BaseCategoryPart { class DynamicCategoryPart extends BaseCategoryPart {
final Iterable<String> Function() loadTags; final JSAutoFreeFunction loader;
final int randomNumber; final String sourceKey;
@override @override
final String title; List<CategoryItem> get categories {
var data = loader([]);
@override if (data is! List) {
bool get enableRandom => true; throw "DynamicCategoryPart loader must return a List";
@override
final String categoryType;
static final random = math.Random();
List<String> _categories() {
var tags = loadTags();
if (randomNumber >= tags.length) {
return tags.toList();
} }
final start = random.nextInt(tags.length - randomNumber); var res = <CategoryItem>[];
var res = List.filled(randomNumber, ''); for (var item in data) {
int index = -1; if (item is! Map) {
for (var s in tags) { throw "DynamicCategoryPart loader must return a List of Map";
index++;
if (start > index) {
continue;
} else if (index == start + randomNumber) {
break;
} }
res[index - start] = s; var label = item['label'];
var target = PageJumpTarget.parse(sourceKey, item['target']);
if (label is! String) {
throw "Category label must be a String";
}
res.add(CategoryItem(label, target));
} }
return res; return res;
} }
@override @override
List<String> get categories => _categories(); bool get enableRandom => false;
/// A [BaseCategoryPart] that show random tags on category page. @override
RandomCategoryPartWithRuntimeData( final String title;
this.title, this.loadTags, this.randomNumber, this.categoryType);
/// A [BaseCategoryPart] that show dynamic tags on category page.
const DynamicCategoryPart(this.title, this.loader, this.sourceKey);
} }
CategoryData getCategoryDataWithKey(String key) { CategoryData getCategoryDataWithKey(String key) {

View File

@@ -11,6 +11,8 @@ import 'package:venera/foundation/app.dart';
import 'package:venera/foundation/comic_type.dart'; import 'package:venera/foundation/comic_type.dart';
import 'package:venera/foundation/history.dart'; import 'package:venera/foundation/history.dart';
import 'package:venera/foundation/res.dart'; import 'package:venera/foundation/res.dart';
import 'package:venera/pages/category_comics_page.dart';
import 'package:venera/pages/search_result_page.dart';
import 'package:venera/utils/data_sync.dart'; import 'package:venera/utils/data_sync.dart';
import 'package:venera/utils/ext.dart'; import 'package:venera/utils/ext.dart';
import 'package:venera/utils/init.dart'; import 'package:venera/utils/init.dart';
@@ -349,7 +351,7 @@ class ExplorePagePart {
/// - category:categoryName /// - category:categoryName
/// ///
/// End with `@`+`param` if the category has a parameter. /// End with `@`+`param` if the category has a parameter.
final String? viewMore; final PageJumpTarget? viewMore;
const ExplorePagePart(this.title, this.comics, this.viewMore); const ExplorePagePart(this.title, this.comics, this.viewMore);
} }

View File

@@ -169,7 +169,9 @@ class ComicDetails with HistoryMixin {
static Map<String, List<String>> _generateMap(Map<dynamic, dynamic> map) { static Map<String, List<String>> _generateMap(Map<dynamic, dynamic> map) {
var res = <String, List<String>>{}; var res = <String, List<String>>{};
map.forEach((key, value) { map.forEach((key, value) {
if (value is List) {
res[key] = List<String>.from(value); res[key] = List<String>.from(value);
}
}); });
return res; return res;
} }
@@ -342,7 +344,8 @@ class ComicChapters {
} else if (groupedChapters.isNotEmpty) { } else if (groupedChapters.isNotEmpty) {
return ComicChapters.grouped(groupedChapters); return ComicChapters.grouped(groupedChapters);
} else { } else {
throw ArgumentError("Empty chapter list"); // return a empty list.
return ComicChapters(chapters);
} }
} }
@@ -429,3 +432,110 @@ class ComicChapters {
} }
} }
} }
class PageJumpTarget {
final String sourceKey;
final String page;
final Map<String, dynamic>? attributes;
const PageJumpTarget(this.sourceKey, this.page, this.attributes);
static PageJumpTarget parse(String sourceKey, dynamic value) {
if (value is Map) {
if (value['page'] != null) {
return PageJumpTarget(
sourceKey,
value["page"] ?? "search",
value["attributes"],
);
} else if (value["action"] != null) {
// old version `onClickTag`
var page = value["action"];
if (page == "search") {
return PageJumpTarget(
sourceKey,
"search",
{
"text": value["keyword"],
},
);
} else if (page == "category") {
return PageJumpTarget(
sourceKey,
"category",
{
"category": value["keyword"],
"param": value["param"],
},
);
} else {
return PageJumpTarget(sourceKey, page, null);
}
}
} else if (value is String) {
// old version string encoding. search: `search:keyword`, category: `category:keyword` or `category:keyword@param`
var segments = value.split(":");
var page = segments[0];
if (page == "search") {
return PageJumpTarget(
sourceKey,
"search",
{
"text": segments[1],
},
);
} else if (page == "category") {
var c = segments[1];
if (c.contains('@')) {
var parts = c.split('@');
return PageJumpTarget(
sourceKey,
"category",
{
"category": parts[0],
"param": parts[1],
},
);
} else {
return PageJumpTarget(
sourceKey,
"category",
{
"category": c,
},
);
}
} else {
return PageJumpTarget(sourceKey, page, null);
}
}
return PageJumpTarget(sourceKey, "Invalid Data", null);
}
void jump(BuildContext context) {
if (page == "search") {
context.to(
() => SearchResultPage(
text: attributes?["text"] ?? attributes?["keyword"] ?? "",
sourceKey: sourceKey,
options: List.from(attributes?["options"] ?? []),
),
);
} else if (page == "category") {
var key = ComicSource.find(sourceKey)!.categoryData!.key;
context.to(
() => CategoryComicsPage(
categoryKey: key,
category: attributes?["category"] ??
(throw ArgumentError("Category name is required")),
options: List.from(attributes?["options"] ?? []),
param: attributes?["param"],
),
);
} else {
Log.error("Page Jump", "Unknown page: $page");
}
}
}

View File

@@ -80,9 +80,8 @@ class ComicSourceParser {
Future<ComicSource> parse(String js, String filePath) async { Future<ComicSource> parse(String js, String filePath) async {
js = js.replaceAll("\r\n", "\n"); js = js.replaceAll("\r\n", "\n");
var line1 = js var line1 =
.split('\n') js.split('\n').firstWhereOrNull((e) => e.trim().startsWith("class "));
.firstWhereOrNull((e) => e.trim().startsWith("class "));
if (line1 == null || if (line1 == null ||
!line1.startsWith("class ") || !line1.startsWith("class ") ||
!line1.contains("extends ComicSource")) { !line1.contains("extends ComicSource")) {
@@ -336,7 +335,7 @@ class ComicSourceParser {
(e['comics'] as List).map((e) { (e['comics'] as List).map((e) {
return Comic.fromJson(e, _key!); return Comic.fromJson(e, _key!);
}).toList(), }).toList(),
e['viewMore'], PageJumpTarget.parse(_key!, e['viewMore']),
); );
}), }),
), ),
@@ -404,6 +403,43 @@ class ComicSourceParser {
var categoryParts = <BaseCategoryPart>[]; var categoryParts = <BaseCategoryPart>[];
for (var c in doc["parts"]) { for (var c in doc["parts"]) {
if (c["categories"] != null && c["categories"] is! List) {
continue;
}
List? categories = c["categories"];
if (categories == null || categories[0] is Map) {
// new format
final String name = c["name"];
final String type = c["type"];
final cs = categories
?.map(
(e) => CategoryItem(
e['label'],
PageJumpTarget.parse(_key!, e['target']),
),
)
.toList();
if (type != "dynamic" && (cs == null || cs.isEmpty)) {
continue;
}
if (type == "fixed") {
categoryParts.add(FixedCategoryPart(name, cs!));
} else if (type == "random") {
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!,
));
}
} else {
// old format
final String name = c["name"]; final String name = c["name"];
final String type = c["type"]; final String type = c["type"];
final List<String> tags = List.from(c["categories"]); final List<String> tags = List.from(c["categories"]);
@@ -413,12 +449,45 @@ class ComicSourceParser {
if (groupParam != null) { if (groupParam != null) {
categoryParams = List.filled(tags.length, groupParam); categoryParams = List.filled(tags.length, groupParam);
} }
var cs = <CategoryItem>[];
for (int i = 0; i < tags.length; i++) {
PageJumpTarget target;
if (itemType == 'category') {
target = PageJumpTarget(
_key!,
'category',
{
"category": tags[i],
"param": categoryParams?.elementAtOrNull(i),
},
);
} else if (itemType == 'search') {
target = PageJumpTarget(
_key!,
'search',
{
"keyword": tags[i],
},
);
} else if (itemType == 'search_with_namespace') {
target = PageJumpTarget(
_key!,
'search',
{
"keyword": "$name:$tags[i]",
},
);
} else {
target = PageJumpTarget(_key!, itemType, null);
}
cs.add(CategoryItem(tags[i], target));
}
if (type == "fixed") { if (type == "fixed") {
categoryParts categoryParts.add(FixedCategoryPart(name, cs));
.add(FixedCategoryPart(name, tags, itemType, categoryParams));
} else if (type == "random") { } else if (type == "random") {
categoryParts.add( categoryParts
RandomCategoryPart(name, tags, c["randomNumber"] ?? 1, itemType)); .add(RandomCategoryPart(name, cs, c["randomNumber"] ?? 1));
}
} }
} }
@@ -620,7 +689,8 @@ class ComicSourceParser {
final bool multiFolder = _getValue("favorites.multiFolder"); final bool multiFolder = _getValue("favorites.multiFolder");
final bool? isOldToNewSort = _getValue("favorites.isOldToNewSort"); final bool? isOldToNewSort = _getValue("favorites.isOldToNewSort");
final bool? singleFolderForSingleComic = _getValue("favorites.singleFolderForSingleComic"); final bool? singleFolderForSingleComic =
_getValue("favorites.singleFolderForSingleComic");
Future<Res<T>> retryZone<T>(Future<Res<T>> Function() func) async { Future<Res<T>> retryZone<T>(Future<Res<T>> Function() func) async {
if (!ComicSource.find(_key!)!.isLogged) { if (!ComicSource.find(_key!)!.isLogged) {
@@ -978,9 +1048,12 @@ class ComicSourceParser {
var res = JsEngine().runCode(""" var res = JsEngine().runCode("""
ComicSource.sources.$_key.comic.onClickTag(${jsonEncode(namespace)}, ${jsonEncode(tag)}) ComicSource.sources.$_key.comic.onClickTag(${jsonEncode(namespace)}, ${jsonEncode(tag)})
"""); """);
var r = Map<String, String?>.from(res); if (res is! Map) {
return null;
}
var r = Map<String, dynamic>.from(res);
r.removeWhere((key, value) => value == null); r.removeWhere((key, value) => value == null);
return Map.from(r); return PageJumpTarget.parse(_key!, r);
}; };
} }

View File

@@ -41,7 +41,7 @@ typedef LikeCommentFunc = Future<Res<int?>> Function(
typedef VoteCommentFunc = Future<Res<int?>> Function( typedef VoteCommentFunc = Future<Res<int?>> Function(
String comicId, String? subId, String commentId, bool isUp, bool isCancel); String comicId, String? subId, String commentId, bool isUp, bool isCancel);
typedef HandleClickTagEvent = Map<String, String> Function( typedef HandleClickTagEvent = PageJumpTarget? Function(
String namespace, String tag); String namespace, String tag);
/// [rating] is the rating value, 0-10. 1 represents 0.5 star. /// [rating] is the rating value, 0-10. 1 represents 0.5 star.

View File

@@ -461,6 +461,10 @@ class LocalManager with ChangeNotifier {
if (comic != null) { if (comic != null) {
return Directory(FilePath.join(path, comic.directory)); return Directory(FilePath.join(path, comic.directory));
} }
const comicDirectoryMaxLength = 128;
if (name.length > comicDirectoryMaxLength) {
name = name.substring(0, comicDirectoryMaxLength);
}
var dir = findValidDirectoryName(path, name); var dir = findValidDirectoryName(path, name);
return Directory(FilePath.join(path, dir)).create().then((value) => value); return Directory(FilePath.join(path, dir)).create().then((value) => value);
} }

View File

@@ -1,7 +1,7 @@
import 'dart:io';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:venera/foundation/app.dart';
import 'package:venera/utils/ext.dart'; import 'package:venera/utils/ext.dart';
import 'package:venera/utils/io.dart';
class LogItem { class LogItem {
final LogLevel level; final LogLevel level;
@@ -28,9 +28,6 @@ class Log {
static bool ignoreLimitation = false; static bool ignoreLimitation = false;
/// only for debug
static const String? logFile = null;
static void printWarning(String text) { static void printWarning(String text) {
debugPrint('\x1B[33m$text\x1B[0m'); debugPrint('\x1B[33m$text\x1B[0m');
} }
@@ -39,7 +36,20 @@ class Log {
debugPrint('\x1B[31m$text\x1B[0m'); debugPrint('\x1B[31m$text\x1B[0m');
} }
static IOSink? _file;
static void addLog(LogLevel level, String title, String content) { static void addLog(LogLevel level, String title, String content) {
if (_file == null) {
Directory dir;
if (App.isAndroid) {
dir = Directory(App.externalStoragePath!);
} else {
dir = Directory(App.dataPath);
}
var file = dir.joinFile("logs.txt");
_file = file.openWrite();
}
if (!ignoreLimitation && content.length > maxLogLength) { if (!ignoreLimitation && content.length > maxLogLength) {
content = "${content.substring(0, maxLogLength)}..."; content = "${content.substring(0, maxLogLength)}...";
} }
@@ -62,8 +72,8 @@ class Log {
} }
_logs.add(newLog); _logs.add(newLog);
if(logFile != null) { if(_file != null) {
File(logFile!).writeAsString(newLog.toString(), mode: FileMode.append); _file!.write(newLog.toString());
} }
if (_logs.length > maxLogNumber) { if (_logs.length > maxLogNumber) {
var res = _logs.remove( var res = _logs.remove(

View File

@@ -35,8 +35,14 @@ void main(List<String> args) {
} }
await windowManager.setMinimumSize(const Size(500, 600)); await windowManager.setMinimumSize(const Size(500, 600));
var placement = await WindowPlacement.loadFromFile(); var placement = await WindowPlacement.loadFromFile();
if (App.isLinux) {
await windowManager.show();
await placement.applyToWindow();
} else {
await placement.applyToWindow(); await placement.applyToWindow();
await windowManager.show(); await windowManager.show();
}
WindowPlacement.loop(); WindowPlacement.loop();
}); });
} }

View File

@@ -482,7 +482,7 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
chapters: comic!.chapters, chapters: comic!.chapters,
cover: File(_cover!.split("file://").last).name, cover: File(_cover!.split("file://").last).name,
comicType: ComicType(source.key.hashCode), comicType: ComicType(source.key.hashCode),
downloadedChapters: chapters ?? [], downloadedChapters: chapters ?? comic?.chapters?.ids.toList() ?? [],
createdAt: DateTime.now(), createdAt: DateTime.now(),
); );
} }

View File

@@ -4,12 +4,10 @@ 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/pages/ranking_page.dart'; import 'package:venera/pages/ranking_page.dart';
import 'package:venera/pages/search_result_page.dart';
import 'package:venera/pages/settings/settings_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';
import 'category_comics_page.dart';
import 'comic_source_page.dart'; import 'comic_source_page.dart';
class CategoriesPage extends StatefulWidget { class CategoriesPage extends StatefulWidget {
@@ -147,43 +145,6 @@ class _CategoryPage extends StatelessWidget {
return ""; return "";
} }
void handleClick(
String tag,
String? param,
String type,
String namespace,
String categoryKey,
) {
if (type == 'search') {
App.mainNavigatorKey?.currentContext?.to(
() => SearchResultPage(
text: tag,
options: const [],
sourceKey: findComicSourceKey(),
),
);
} else if (type == "search_with_namespace") {
if (tag.contains(" ")) {
tag = '"$tag"';
}
App.mainNavigatorKey?.currentContext?.to(
() => SearchResultPage(
text: "$namespace:$tag",
options: const [],
sourceKey: findComicSourceKey(),
),
);
} else if (type == "category") {
App.mainNavigatorKey!.currentContext!.to(
() => CategoryComicsPage(
category: tag,
categoryKey: categoryKey,
param: param,
),
);
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var children = <Widget>[]; var children = <Widget>[];
@@ -194,11 +155,11 @@ class _CategoryPage extends StatelessWidget {
child: Wrap( child: Wrap(
children: [ children: [
if (data.enableRankingPage) if (data.enableRankingPage)
buildTag("Ranking".tl, (p0, p1) { buildTag("Ranking".tl, () {
context.to(() => RankingPage(categoryKey: data.key)); context.to(() => RankingPage(categoryKey: data.key));
}), }),
for (var buttonData in data.buttons) for (var buttonData in data.buttons)
buildTag(buttonData.label.tl, (p0, p1) => buttonData.onTap()) buildTag(buttonData.label.tl, buttonData.onTap)
], ],
), ),
)); ));
@@ -212,36 +173,14 @@ class _CategoryPage extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
buildTitleWithRefresh(part.title, () => updater(() {})), buildTitleWithRefresh(part.title, () => updater(() {})),
buildTagsWithParams( buildTags(part.categories)
part.categories,
part.categoryParams,
part.title,
(key, param) => handleClick(
key,
param,
part.categoryType,
part.title,
category,
),
)
], ],
); );
})); }));
} else { } else {
children.add(buildTitle(part.title)); children.add(buildTitle(part.title));
children.add( children.add(
buildTagsWithParams( buildTags(part.categories),
part.categories,
part.categoryParams,
part.title,
(tag, param) => handleClick(
tag,
param,
part.categoryType,
part.title,
data.key,
),
),
); );
} }
} }
@@ -280,30 +219,28 @@ class _CategoryPage extends StatelessWidget {
); );
} }
Widget buildTagsWithParams( Widget buildTags(
List<String> tags, List<CategoryItem> categories,
List<String>? params,
String? namespace,
ClickTagCallback onClick,
) { ) {
return Padding( return Padding(
padding: const EdgeInsets.fromLTRB(10, 0, 10, 16), padding: const EdgeInsets.fromLTRB(10, 0, 10, 16),
child: Wrap( child: Wrap(
children: List<Widget>.generate( children: List<Widget>.generate(
tags.length, categories.length,
(index) => buildTag( (index) => buildCategory(categories[index]),
tags[index],
onClick,
namespace,
params?.elementAtOrNull(index),
),
), ),
), ),
); );
} }
Widget buildTag(String tag, ClickTagCallback onClick, Widget buildCategory(CategoryItem c) {
[String? namespace, String? param]) { return buildTag(c.label, () {
var context = App.mainNavigatorKey!.currentContext!;
c.target.jump(context);
});
}
Widget buildTag(String label, VoidCallback onClick) {
return Padding( return Padding(
padding: const EdgeInsets.fromLTRB(8, 6, 8, 6), padding: const EdgeInsets.fromLTRB(8, 6, 8, 6),
child: Builder( child: Builder(
@@ -313,10 +250,10 @@ class _CategoryPage extends StatelessWidget {
color: context.colorScheme.primaryContainer.toOpacity(0.72), color: context.colorScheme.primaryContainer.toOpacity(0.72),
child: InkWell( child: InkWell(
borderRadius: const BorderRadius.all(Radius.circular(8)), borderRadius: const BorderRadius.all(Radius.circular(8)),
onTap: () => onClick(tag, param), onTap: onClick,
child: Padding( child: Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
child: Text(tag), child: Text(label),
), ),
), ),
); );

View File

@@ -9,6 +9,7 @@ class CategoryComicsPage extends StatefulWidget {
required this.category, required this.category,
this.param, this.param,
required this.categoryKey, required this.categoryKey,
this.options,
super.key, super.key,
}); });
@@ -18,6 +19,8 @@ class CategoryComicsPage extends StatefulWidget {
final String categoryKey; final String categoryKey;
final List<String>? options;
@override @override
State<CategoryComicsPage> createState() => _CategoryComicsPageState(); State<CategoryComicsPage> createState() => _CategoryComicsPageState();
} }
@@ -31,6 +34,9 @@ class _CategoryComicsPageState extends State<CategoryComicsPage> {
void findData() { void findData() {
for (final source in ComicSource.all()) { for (final source in ComicSource.all()) {
if (source.categoryData?.key == widget.categoryKey) { if (source.categoryData?.key == widget.categoryKey) {
if (source.categoryComicsData == null) {
throw "The comic source ${source.name} does not support category comics";
}
data = source.categoryComicsData!; data = source.categoryComicsData!;
options = data.options.where((element) { options = data.options.where((element) {
if (element.notShowWhen.contains(widget.category)) { if (element.notShowWhen.contains(widget.category)) {
@@ -40,7 +46,16 @@ class _CategoryComicsPageState extends State<CategoryComicsPage> {
} }
return true; return true;
}).toList(); }).toList();
optionsValue = options.map((e) => e.options.keys.first).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];
}
optionsValue = newOptionsValue;
}
sourceKey = source.key; sourceKey = source.key;
return; return;
} }
@@ -50,6 +65,11 @@ class _CategoryComicsPageState extends State<CategoryComicsPage> {
@override @override
void initState() { void initState() {
if (widget.options != null) {
optionsValue = widget.options!;
} else {
optionsValue = [];
}
findData(); findData();
super.initState(); super.initState();
} }

View File

@@ -294,27 +294,9 @@ abstract mixin class _ComicPageActions {
} }
void onTapTag(String tag, String namespace) { void onTapTag(String tag, String namespace) {
var config = comicSource.handleClickTagEvent?.call(namespace, tag) ?? var target = comicSource.handleClickTagEvent?.call(namespace, tag);
{
'action': 'search',
'keyword': tag,
};
var context = App.mainNavigatorKey!.currentContext!; var context = App.mainNavigatorKey!.currentContext!;
if (config['action'] == 'search') { target?.jump(context);
context.to(() => SearchResultPage(
text: config['keyword'] ?? '',
sourceKey: comicSource.key,
options: const [],
));
} else if (config['action'] == 'category') {
context.to(
() => CategoryComicsPage(
category: config['keyword'] ?? '',
categoryKey: comicSource.categoryData!.key,
param: config['param'],
),
);
}
} }
void showMoreActions() { void showMoreActions() {

View File

@@ -105,7 +105,7 @@ class _NormalComicChaptersState extends State<_NormalComicChapters> {
var value = chapters[key]!; var value = chapters[key]!;
bool visited = (history?.readEpisode ?? {}).contains(i + 1); bool visited = (history?.readEpisode ?? {}).contains(i + 1);
return Padding( return Padding(
padding: const EdgeInsets.fromLTRB(6, 4, 6, 4), padding: const EdgeInsets.fromLTRB(4, 4, 4, 4),
child: Material( child: Material(
color: context.colorScheme.surfaceContainer, color: context.colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
@@ -113,7 +113,7 @@ class _NormalComicChaptersState extends State<_NormalComicChapters> {
onTap: () => state.read(i + 1), onTap: () => state.read(i + 1),
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4), padding: const EdgeInsets.symmetric(horizontal: 8),
child: Center( child: Center(
child: Text( child: Text(
value, value,
@@ -134,7 +134,7 @@ class _NormalComicChaptersState extends State<_NormalComicChapters> {
}, },
), ),
gridDelegate: const SliverGridDelegateWithFixedHeight( gridDelegate: const SliverGridDelegateWithFixedHeight(
maxCrossAxisExtent: 200, maxCrossAxisExtent: 250,
itemHeight: 48, itemHeight: 48,
), ),
).sliverPadding(const EdgeInsets.symmetric(horizontal: 8)), ).sliverPadding(const EdgeInsets.symmetric(horizontal: 8)),
@@ -300,15 +300,15 @@ class _GroupedComicChaptersState extends State<_GroupedComicChapters>
history!.readEpisode.contains(rawIndex); history!.readEpisode.contains(rawIndex);
} }
return Padding( return Padding(
padding: const EdgeInsets.fromLTRB(6, 4, 6, 4), padding: const EdgeInsets.fromLTRB(4, 4, 4, 4),
child: Material( child: Material(
color: context.colorScheme.surfaceContainer, color: context.colorScheme.surfaceContainerLow,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(12),
child: InkWell( child: InkWell(
onTap: () => state.read(chapterIndex + 1), onTap: () => state.read(chapterIndex + 1),
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(12),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4), padding: const EdgeInsets.symmetric(horizontal: 8),
child: Center( child: Center(
child: Text( child: Text(
value, value,
@@ -329,7 +329,7 @@ class _GroupedComicChaptersState extends State<_GroupedComicChapters>
}, },
), ),
gridDelegate: const SliverGridDelegateWithFixedHeight( gridDelegate: const SliverGridDelegateWithFixedHeight(
maxCrossAxisExtent: 200, maxCrossAxisExtent: 250,
itemHeight: 48, itemHeight: 48,
), ),
).sliverPadding(const EdgeInsets.symmetric(horizontal: 8)), ).sliverPadding(const EdgeInsets.symmetric(horizontal: 8)),

View File

@@ -17,10 +17,8 @@ import 'package:venera/foundation/image_provider/cached_image.dart';
import 'package:venera/foundation/local.dart'; import 'package:venera/foundation/local.dart';
import 'package:venera/foundation/res.dart'; import 'package:venera/foundation/res.dart';
import 'package:venera/network/download.dart'; import 'package:venera/network/download.dart';
import 'package:venera/pages/category_comics_page.dart';
import 'package:venera/pages/favorites/favorites_page.dart'; import 'package:venera/pages/favorites/favorites_page.dart';
import 'package:venera/pages/reader/reader.dart'; import 'package:venera/pages/reader/reader.dart';
import 'package:venera/pages/search_result_page.dart';
import 'package:venera/utils/app_links.dart'; import 'package:venera/utils/app_links.dart';
import 'package:venera/utils/ext.dart'; import 'package:venera/utils/ext.dart';
import 'package:venera/utils/io.dart'; import 'package:venera/utils/io.dart';
@@ -411,7 +409,9 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
var group = history!.group; var group = history!.group;
String text; String text;
if (haveChapter) { if (haveChapter) {
var epName = group == null var epName = "E$ep";
try {
epName = group == null
? comic.chapters!.titles.elementAt( ? comic.chapters!.titles.elementAt(
math.min(ep - 1, comic.chapters!.length - 1), math.min(ep - 1, comic.chapters!.length - 1),
) )
@@ -419,6 +419,10 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
.getGroupByIndex(group - 1) .getGroupByIndex(group - 1)
.values .values
.elementAt(ep - 1); .elementAt(ep - 1);
}
catch(e) {
// ignore
}
text = "${"Last Reading".tl}: $epName P$page"; text = "${"Last Reading".tl}: $epName P$page";
} else { } else {
text = "${"Last Reading".tl}: P$page"; text = "${"Last Reading".tl}: P$page";
@@ -461,7 +465,8 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
if (comic.tags.isEmpty && if (comic.tags.isEmpty &&
comic.uploader == null && comic.uploader == null &&
comic.uploadTime == null && comic.uploadTime == null &&
comic.uploadTime == null) { comic.uploadTime == null &&
comic.maxPage == null) {
return const SliverPadding(padding: EdgeInsets.zero); return const SliverPadding(padding: EdgeInsets.zero);
} }
@@ -625,6 +630,13 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
buildTag(text: formatTime(comic.updateTime!)), buildTag(text: formatTime(comic.updateTime!)),
], ],
), ),
if (comic.maxPage != null)
buildWrap(
children: [
buildTag(text: 'Pages'.tl, isTitle: true),
buildTag(text: comic.maxPage.toString()),
],
),
const SizedBox(height: 12), const SizedBox(height: 12),
const Divider(), const Divider(),
], ],

View File

@@ -99,7 +99,11 @@ class _CommentsPageState extends State<CommentsPage> {
return Column( return Column(
children: [ children: [
Expanded( Expanded(
child: ListView.builder( child: SmoothScrollProvider(
builder: (context, controller, physics) {
return ListView.builder(
controller: controller,
physics: physics,
primary: false, primary: false,
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
itemCount: _comments!.length + 2, itemCount: _comments!.length + 2,
@@ -156,6 +160,8 @@ class _CommentsPageState extends State<CommentsPage> {
showAvatar: showAvatar, showAvatar: showAvatar,
); );
}, },
);
},
), ),
), ),
buildBottom(context) buildBottom(context)

View File

@@ -374,8 +374,35 @@ class _ComicSourceListState extends State<_ComicSourceList> {
} else { } else {
var currentKey = ComicSource.all().map((e) => e.key).toList(); var currentKey = ComicSource.all().map((e) => e.key).toList();
return ListView.builder( return ListView.builder(
itemCount: json!.length, itemCount: json!.length + 1,
itemBuilder: (context, index) { itemBuilder: (context, index) {
if (index == 0) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 12),
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: context.colorScheme.primaryContainer,
),
child: Row(
children: [
const Icon(Icons.info_outline),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Do not report any issues related to sources to App repo.".tl),
Text("Click the setting icon to change the source list url.".tl),
],
),
),
],
),
);
}
index--;
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)
@@ -403,9 +430,14 @@ class _ComicSourceListState extends State<_ComicSourceList> {
}, },
).fixHeight(32); ).fixHeight(32);
var description = json![index]["version"];
if (json![index]["description"] != null) {
description = "$description\n${json![index]["description"]}";
}
return ListTile( return ListTile(
title: Text(json![index]["name"]), title: Text(json![index]["name"]),
subtitle: Text(json![index]["version"]), subtitle: Text(description),
trailing: action, trailing: action,
); );
}, },
@@ -461,6 +493,7 @@ void _addAllPagesWithComicSource(ComicSource source) {
var explorePages = appdata.settings['explore_pages']; var explorePages = appdata.settings['explore_pages'];
var categoryPages = appdata.settings['categories']; var categoryPages = appdata.settings['categories'];
var networkFavorites = appdata.settings['favorites']; var networkFavorites = appdata.settings['favorites'];
var searchPages = appdata.settings['searchSources'];
if (source.explorePages.isNotEmpty) { if (source.explorePages.isNotEmpty) {
for (var page in source.explorePages) { for (var page in source.explorePages) {
@@ -477,10 +510,15 @@ void _addAllPagesWithComicSource(ComicSource source) {
!networkFavorites.contains(source.favoriteData!.key)) { !networkFavorites.contains(source.favoriteData!.key)) {
networkFavorites.add(source.favoriteData!.key); networkFavorites.add(source.favoriteData!.key);
} }
if (source.searchPageData != null &&
!searchPages.contains(source.key)) {
searchPages.add(source.key);
}
appdata.settings['explore_pages'] = explorePages.toSet().toList(); appdata.settings['explore_pages'] = explorePages.toSet().toList();
appdata.settings['categories'] = categoryPages.toSet().toList(); appdata.settings['categories'] = categoryPages.toSet().toList();
appdata.settings['favorites'] = networkFavorites.toSet().toList(); appdata.settings['favorites'] = networkFavorites.toSet().toList();
appdata.settings['searchSources'] = searchPages.toSet().toList();
appdata.saveData(); appdata.saveData();
} }

View File

@@ -6,13 +6,10 @@ import 'package:venera/foundation/comic_source/comic_source.dart';
import 'package:venera/foundation/global_state.dart'; import 'package:venera/foundation/global_state.dart';
import 'package:venera/foundation/res.dart'; import 'package:venera/foundation/res.dart';
import 'package:venera/pages/comic_source_page.dart'; import 'package:venera/pages/comic_source_page.dart';
import 'package:venera/pages/search_result_page.dart';
import 'package:venera/pages/settings/settings_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';
import 'category_comics_page.dart';
class ExplorePage extends StatefulWidget { class ExplorePage extends StatefulWidget {
const ExplorePage({super.key}); const ExplorePage({super.key});
@@ -445,30 +442,7 @@ Iterable<Widget> _buildExplorePagePart(
TextButton( TextButton(
onPressed: () { onPressed: () {
var context = App.mainNavigatorKey!.currentContext!; var context = App.mainNavigatorKey!.currentContext!;
if (part.viewMore!.startsWith("search:")) { part.viewMore!.jump(context);
context.to(
() => SearchResultPage(
text: part.viewMore!.replaceFirst("search:", ""),
options: const [],
sourceKey: sourceKey,
),
);
} else if (part.viewMore!.startsWith("category:")) {
var cp = part.viewMore!.replaceFirst("category:", "");
var c = cp.split('@').first;
String? p = cp.split('@').last;
if (p == c) {
p = null;
}
context.to(
() => CategoryComicsPage(
category: c,
categoryKey:
ComicSource.find(sourceKey)!.categoryData!.key,
param: p,
),
);
}
}, },
child: Text("View more".tl), child: Text("View more".tl),
) )

View File

@@ -52,7 +52,7 @@ class _SearchBar extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SliverToBoxAdapter( return SliverToBoxAdapter(
child: Container( child: Container(
height: 52, height: App.isMobile ? 52 : 46,
width: double.infinity, width: double.infinity,
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
child: Material( child: Material(
@@ -942,7 +942,7 @@ class _ImageFavoritesState extends State<ImageFavorites> {
displayType = type; displayType = type;
}); });
await Future.delayed(const Duration(milliseconds: 20)); await Future.delayed(const Duration(milliseconds: 20));
var scrollController = ScrollControllerProvider.of(context); var scrollController = ScrollState.of(context).controller;
scrollController.animateTo( scrollController.animateTo(
scrollController.position.maxScrollExtent, scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),

View File

@@ -43,9 +43,10 @@ class _ReaderImagesState extends State<_ReaderImages> {
}); });
} }
} else { } else {
var cp = reader.widget.chapters?.ids.elementAtOrNull(reader.chapter - 1);
var res = await reader.type.comicSource!.loadComicPages!( var res = await reader.type.comicSource!.loadComicPages!(
reader.widget.cid, reader.widget.cid,
reader.widget.chapters?.ids.elementAt(reader.chapter - 1), cp,
); );
if (res.error) { if (res.error) {
setState(() { setState(() {
@@ -343,10 +344,19 @@ class _GalleryModeState extends State<_GalleryMode>
} }
var photoViewController = photoViewControllers[reader.page]!; var photoViewController = photoViewControllers[reader.page]!;
double target = photoViewController.getInitialScale!.call()! * 1.75; double target = photoViewController.getInitialScale!.call()! * 1.75;
var size = MediaQuery.of(context).size; var size = reader.size;
Offset zoomPosition;
if (appdata.settings['longPressZoomPosition'] != 'center') {
zoomPosition = Offset(
size.width / 2 - location.dx,
size.height / 2 - location.dy,
);
} else {
zoomPosition = Offset(0, 0);
}
photoViewController.animateScale?.call( photoViewController.animateScale?.call(
target, target,
Offset(size.width / 2 - location.dx, size.height / 2 - location.dy), zoomPosition,
); );
isLongPressing = true; isLongPressing = true;
} }
@@ -608,6 +618,13 @@ class _ContinuousModeState extends State<_ContinuousMode>
} }
bool onScaleUpdate([double? scale]) { bool onScaleUpdate([double? scale]) {
if (prepareToNextChapter || prepareToPrevChapter) {
setState(() {
prepareToPrevChapter = false;
prepareToNextChapter = false;
});
context.readerScaffold.setFloatingButton(0);
}
var isZoomedIn = (scale ?? photoViewController.scale) != 1.0; var isZoomedIn = (scale ?? photoViewController.scale) != 1.0;
if (isZoomedIn != this.isZoomedIn) { if (isZoomedIn != this.isZoomedIn) {
setState(() { setState(() {
@@ -731,7 +748,8 @@ class _ContinuousModeState extends State<_ContinuousMode>
} }
Offset offset; Offset offset;
var sp = scrollController.position; var sp = scrollController.position;
if (sp.pixels < sp.minScrollExtent || sp.pixels > sp.maxScrollExtent) { if (sp.pixels <= sp.minScrollExtent ||
sp.pixels >= sp.maxScrollExtent) {
offset = Offset(value.dx, value.dy); offset = Offset(value.dx, value.dy);
} else { } else {
if (reader.mode == ReaderMode.continuousTopToBottom) { if (reader.mode == ReaderMode.continuousTopToBottom) {
@@ -759,7 +777,10 @@ class _ContinuousModeState extends State<_ContinuousMode>
delayedSetIsScrolling(false); delayedSetIsScrolling(false);
} }
if (notification is ScrollUpdateNotification) { var scale = photoViewController.scale ?? 1.0;
if (notification is ScrollUpdateNotification &&
(scale - 1).abs() < 0.05) {
if (!scrollController.hasClients) return false; if (!scrollController.hasClients) return false;
if (scrollController.position.pixels <= if (scrollController.position.pixels <=
scrollController.position.minScrollExtent && scrollController.position.minScrollExtent &&
@@ -800,8 +821,8 @@ class _ContinuousModeState extends State<_ContinuousMode>
}, },
child: widget, child: widget,
); );
var width = MediaQuery.of(context).size.width; var width = reader.size.width;
var height = MediaQuery.of(context).size.height; var height = reader.size.height;
if (appdata.settings['limitImageWidth'] && if (appdata.settings['limitImageWidth'] &&
width / height > 0.7 && width / height > 0.7 &&
reader.mode == ReaderMode.continuousTopToBottom) { reader.mode == ReaderMode.continuousTopToBottom) {
@@ -882,9 +903,19 @@ class _ContinuousModeState extends State<_ContinuousMode>
return; return;
} }
double target = photoViewController.getInitialScale!.call()! * 1.75; double target = photoViewController.getInitialScale!.call()! * 1.75;
var size = reader.size;
Offset zoomPosition;
if (appdata.settings['longPressZoomPosition'] != 'center') {
zoomPosition = Offset(
size.width / 2 - location.dx,
size.height / 2 - location.dy,
);
} else {
zoomPosition = Offset(0, 0);
}
photoViewController.animateScale?.call( photoViewController.animateScale?.call(
target, target,
Offset(0, 0), zoomPosition,
); );
onScaleUpdate(target); onScaleUpdate(target);
isLongPressing = true; isLongPressing = true;

View File

@@ -309,6 +309,13 @@ class _ReaderState extends State<Reader>
} }
return chapter == maxChapter; return chapter == maxChapter;
} }
/// Get the size of the reader.
/// The size is not always the same as the size of the screen.
Size get size {
var renderBox = context.findRenderObject() as RenderBox;
return renderBox.size;
}
} }
abstract mixin class _ImagePerPageHandler { abstract mixin class _ImagePerPageHandler {
@@ -363,8 +370,24 @@ abstract mixin class _VolumeListener {
bool toPrevPage(); bool toPrevPage();
bool toNextChapter();
bool toPrevChapter();
VolumeListener? volumeListener; VolumeListener? volumeListener;
void onDown() {
if (!toNextPage()) {
toNextChapter();
}
}
void onUp() {
if (!toPrevPage()) {
toPrevChapter();
}
}
void handleVolumeEvent() { void handleVolumeEvent() {
if (!App.isAndroid) { if (!App.isAndroid) {
// Currently only support Android // Currently only support Android
@@ -374,8 +397,8 @@ abstract mixin class _VolumeListener {
volumeListener?.cancel(); volumeListener?.cancel();
} }
volumeListener = VolumeListener( volumeListener = VolumeListener(
onDown: toNextPage, onDown: onDown,
onUp: toPrevPage, onUp: onUp,
)..listen(); )..listen();
} }

View File

@@ -441,6 +441,11 @@ class _SearchSettingsDialogState extends State<_SearchSettingsDialog> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var sources = ComicSource.all();
var enabled = appdata.settings['searchSources'] as List;
sources.removeWhere((e) {
return !enabled.contains(e.key);
});
return ContentDialog( return ContentDialog(
title: "Settings".tl, title: "Settings".tl,
content: Column( content: Column(
@@ -452,7 +457,7 @@ class _SearchSettingsDialogState extends State<_SearchSettingsDialog> {
Wrap( Wrap(
spacing: 8, spacing: 8,
runSpacing: 8, runSpacing: 8,
children: ComicSource.all().map((e) { children: sources.map((e) {
return OptionChip( return OptionChip(
text: e.name.tl, text: e.name.tl,
isSelected: searchTarget == e.key, isSelected: searchTarget == e.key,

View File

@@ -140,17 +140,6 @@ class _AppSettingsState extends State<AppSettings> {
}, },
actionTitle: 'Set'.tl, actionTitle: 'Set'.tl,
).toSliver(), ).toSliver(),
_SettingPartTitle(
title: "Log".tl,
icon: Icons.error_outline,
),
_CallbackSetting(
title: "Open Log".tl,
callback: () {
context.to(() => const LogsPage());
},
actionTitle: 'Open'.tl,
).toSliver(),
_SettingPartTitle( _SettingPartTitle(
title: "User".tl, title: "User".tl,
icon: Icons.person_outline, icon: Icons.person_outline,

View File

@@ -0,0 +1,95 @@
part of 'settings_page.dart';
class DebugPage extends StatefulWidget {
const DebugPage({super.key});
@override
State<DebugPage> createState() => DebugPageState();
}
class DebugPageState extends State<DebugPage> {
final controller = TextEditingController();
var result = "";
@override
Widget build(BuildContext context) {
return SmoothCustomScrollView(
slivers: [
SliverAppbar(title: Text("Debug".tl)),
_CallbackSetting(
title: "Reload Configs",
actionTitle: "Reload",
callback: () {
ComicSourceManager().reload();
},
).toSliver(),
_CallbackSetting(
title: "Open Log".tl,
callback: () {
context.to(() => const LogsPage());
},
actionTitle: 'Open'.tl,
).toSliver(),
SliverToBoxAdapter(
child: Column(
children: [
const SizedBox(height: 8),
const Text(
"JS Evaluator",
style: TextStyle(fontSize: 16),
).toAlign(Alignment.centerLeft).paddingLeft(16),
Container(
width: double.infinity,
height: 200,
margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
child: TextField(
controller: controller,
maxLines: null,
expands: true,
textAlign: TextAlign.start,
textAlignVertical: TextAlignVertical.top,
decoration: InputDecoration(
border: const OutlineInputBorder(),
contentPadding: const EdgeInsets.all(8),
),
),
),
TextButton(
onPressed: () {
try {
var res = JsEngine().runCode(controller.text);
setState(() {
result = res.toString();
});
} catch (e) {
setState(() {
result = e.toString();
});
}
},
child: const Text("Run"),
).toAlign(Alignment.centerRight).paddingRight(16),
const Text(
"Result",
style: TextStyle(fontSize: 16),
).toAlign(Alignment.centerLeft).paddingLeft(16),
Container(
width: double.infinity,
height: 200,
margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
decoration: BoxDecoration(
border: Border.all(color: context.colorScheme.outline),
borderRadius: BorderRadius.circular(4),
),
child: SingleChildScrollView(
child: Text(result).paddingAll(4),
),
),
],
),
),
],
);
}
}

View File

@@ -48,6 +48,7 @@ class _ReaderSettingsState extends State<ReaderSettings> {
"continuousTopToBottom": "Continuous (Top to Bottom)".tl, "continuousTopToBottom": "Continuous (Top to Bottom)".tl,
}, },
onChanged: () { onChanged: () {
setState(() {});
var readerMode = appdata.settings['readerMode']; var readerMode = appdata.settings['readerMode'];
if (readerMode?.toLowerCase().startsWith('continuous') ?? false) { if (readerMode?.toLowerCase().startsWith('continuous') ?? false) {
appdata.settings['readerScreenPicNumberForLandscape'] = 1; appdata.settings['readerScreenPicNumberForLandscape'] = 1;
@@ -68,22 +69,12 @@ class _ReaderSettingsState extends State<ReaderSettings> {
widget.onChanged?.call("autoPageTurningInterval"); widget.onChanged?.call("autoPageTurningInterval");
}, },
).toSliver(), ).toSliver(),
SliverToBoxAdapter( SliverAnimatedVisibility(
child: AbsorbPointer( visible: appdata.settings['readerMode']!.startsWith('gallery'),
absorbing: (appdata.settings['readerMode']
?.toLowerCase()
.startsWith('continuous') ??
false),
child: AnimatedOpacity(
opacity: (appdata.settings['readerMode']
?.toLowerCase()
.startsWith('continuous') ??
false)
? 0.5
: 1.0,
duration: Duration(milliseconds: 300),
child: _SliderSetting( child: _SliderSetting(
title: "The number of pic in screen for landscape (Only Gallery Mode)".tl, title:
"The number of pic in screen for landscape (Only Gallery Mode)"
.tl,
settingsIndex: "readerScreenPicNumberForLandscape", settingsIndex: "readerScreenPicNumberForLandscape",
interval: 1, interval: 1,
min: 1, min: 1,
@@ -93,24 +84,12 @@ class _ReaderSettingsState extends State<ReaderSettings> {
}, },
), ),
), ),
), SliverAnimatedVisibility(
), visible: appdata.settings['readerMode']!.startsWith('gallery'),
SliverToBoxAdapter(
child: AbsorbPointer(
absorbing: (appdata.settings['readerMode']
?.toLowerCase()
.startsWith('continuous') ??
false),
child: AnimatedOpacity(
opacity: (appdata.settings['readerMode']
?.toLowerCase()
.startsWith('continuous') ??
false)
? 0.5
: 1.0,
duration: Duration(milliseconds: 300),
child: _SliderSetting( child: _SliderSetting(
title: "The number of pic in screen for portrait (Only Gallery Mode)".tl, title:
"The number of pic in screen for portrait (Only Gallery Mode)"
.tl,
settingsIndex: "readerScreenPicNumberForPortrait", settingsIndex: "readerScreenPicNumberForPortrait",
interval: 1, interval: 1,
min: 1, min: 1,
@@ -120,15 +99,25 @@ class _ReaderSettingsState extends State<ReaderSettings> {
}, },
), ),
), ),
),
),
_SwitchSetting( _SwitchSetting(
title: 'Long press to zoom'.tl, title: 'Long press to zoom'.tl,
settingKey: 'enableLongPressToZoom', settingKey: 'enableLongPressToZoom',
onChanged: () { onChanged: () {
setState(() {});
widget.onChanged?.call('enableLongPressToZoom'); widget.onChanged?.call('enableLongPressToZoom');
}, },
).toSliver(), ).toSliver(),
SliverAnimatedVisibility(
visible: appdata.settings['enableLongPressToZoom'] == true,
child: SelectSetting(
title: "Long press zoom position".tl,
settingKey: "longPressZoomPosition",
optionTranslation: {
"press": "Press position".tl,
"center": "Screen center".tl,
},
),
),
_SwitchSetting( _SwitchSetting(
title: 'Limit image width'.tl, title: 'Limit image width'.tl,
subtitle: 'When using Continuous(Top to Bottom) mode'.tl, subtitle: 'When using Continuous(Top to Bottom) mode'.tl,

View File

@@ -30,6 +30,7 @@ part 'local_favorites.dart';
part 'app.dart'; part 'app.dart';
part 'about.dart'; part 'about.dart';
part 'network.dart'; part 'network.dart';
part 'debug.dart';
class SettingsPage extends StatefulWidget { class SettingsPage extends StatefulWidget {
const SettingsPage({this.initialPage = -1, super.key}); const SettingsPage({this.initialPage = -1, super.key});
@@ -55,6 +56,7 @@ class _SettingsPageState extends State<SettingsPage> implements PopEntry {
"APP", "APP",
"Network", "Network",
"About", "About",
"Debug"
]; ];
final icons = <IconData>[ final icons = <IconData>[
@@ -64,7 +66,8 @@ class _SettingsPageState extends State<SettingsPage> implements PopEntry {
Icons.collections_bookmark_rounded, Icons.collections_bookmark_rounded,
Icons.apps, Icons.apps,
Icons.public, Icons.public,
Icons.info Icons.info,
Icons.bug_report,
]; ];
double offset = 0; double offset = 0;
@@ -246,6 +249,9 @@ class _SettingsPageState extends State<SettingsPage> implements PopEntry {
} }
void handlePointerDown(PointerDownEvent event) { void handlePointerDown(PointerDownEvent event) {
if (!App.isIOS) {
return;
}
if (event.position.dx < 20) { if (event.position.dx < 20) {
gestureRecognizer.addPointer(event); gestureRecognizer.addPointer(event);
} }
@@ -350,6 +356,7 @@ class _SettingsPageState extends State<SettingsPage> implements PopEntry {
4 => const AppSettings(), 4 => const AppSettings(),
5 => const NetworkSettings(), 5 => const NetworkSettings(),
6 => const AboutSettings(), 6 => const AboutSettings(),
7 => const DebugPage(),
_ => throw UnimplementedError() _ => throw UnimplementedError()
}; };
} }

View File

@@ -1,4 +1,6 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:venera/components/components.dart';
import 'package:venera/components/window_frame.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';
@@ -10,6 +12,7 @@ import 'package:venera/utils/data.dart';
import 'package:venera/utils/ext.dart'; import 'package:venera/utils/ext.dart';
import 'package:webdav_client/webdav_client.dart' hide File; import 'package:webdav_client/webdav_client.dart' hide File;
import 'package:rhttp/rhttp.dart' as rhttp; import 'package:rhttp/rhttp.dart' as rhttp;
import 'package:venera/utils/translations.dart';
import 'io.dart'; import 'io.dart';
@@ -20,6 +23,10 @@ class DataSync with ChangeNotifier {
} }
LocalFavoritesManager().addListener(onDataChanged); LocalFavoritesManager().addListener(onDataChanged);
ComicSourceManager().addListener(onDataChanged); ComicSourceManager().addListener(onDataChanged);
Future.delayed(const Duration(seconds: 1), () {
var controller = WindowFrame.of(App.rootContext);
controller.addCloseListener(_handleWindowClose);
});
} }
void onDataChanged() { void onDataChanged() {
@@ -28,6 +35,28 @@ class DataSync with ChangeNotifier {
} }
} }
bool _handleWindowClose() {
if (_isUploading) {
_showWindowCloseDialog();
return false;
}
return true;
}
void _showWindowCloseDialog() async {
showLoadingDialog(
App.rootContext,
cancelButtonText: "Shut Down".tl,
onCancel: () => exit(0),
barrierDismissible: false,
message: "Uploading data...".tl,
);
while (_isUploading) {
await Future.delayed(const Duration(milliseconds: 50));
}
exit(0);
}
static DataSync? instance; static DataSync? instance;
factory DataSync() => instance ?? (instance = DataSync._()); factory DataSync() => instance ?? (instance = DataSync._());
@@ -100,6 +129,7 @@ class DataSync with ChangeNotifier {
rhttp.ClientSettings( rhttp.ClientSettings(
proxySettings: proxySettings:
proxy == null ? null : rhttp.ProxySettings.proxy(proxy), proxy == null ? null : rhttp.ProxySettings.proxy(proxy),
userAgent: "venera v${App.version}",
), ),
), ),
); );
@@ -172,6 +202,7 @@ class DataSync with ChangeNotifier {
rhttp.ClientSettings( rhttp.ClientSettings(
proxySettings: proxySettings:
proxy == null ? null : rhttp.ProxySettings.proxy(proxy), proxy == null ? null : rhttp.ProxySettings.proxy(proxy),
userAgent: "venera v${App.version}",
), ),
), ),
); );

View File

@@ -1,4 +1,3 @@
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:isolate'; import 'dart:isolate';
@@ -132,25 +131,28 @@ extension DirectoryExtension on Directory {
} }
/// Sanitize the file name. Remove invalid characters and trim the file name. /// Sanitize the file name. Remove invalid characters and trim the file name.
String sanitizeFileName(String fileName) { String sanitizeFileName(String fileName, {String? dir, int? maxLength}) {
if (fileName.endsWith('.')) { if (fileName.endsWith('.')) {
fileName = fileName.substring(0, fileName.length - 1); fileName = fileName.substring(0, fileName.length - 1);
} }
const maxLength = 255; var maxLength = 255;
if (dir != null) {
if (!dir.endsWith('/') && !dir.endsWith('\\')) {
dir = "$dir/";
}
maxLength -= dir.length;
}
final invalidChars = RegExp(r'[<>:"/\\|?*]'); final invalidChars = RegExp(r'[<>:"/\\|?*]');
final sanitizedFileName = fileName.replaceAll(invalidChars, ' '); final sanitizedFileName = fileName.replaceAll(invalidChars, ' ');
var trimmedFileName = sanitizedFileName.trim(); var trimmedFileName = sanitizedFileName.trim();
if (trimmedFileName.isEmpty) { if (trimmedFileName.isEmpty) {
throw Exception('Invalid File Name: Empty length.'); throw Exception('Invalid File Name: Empty length.');
} }
while (true) { if (maxLength <= 0) {
final bytes = utf8.encode(trimmedFileName); throw Exception('Invalid File Name: Max length is less than 0.');
if (bytes.length > maxLength) {
trimmedFileName =
trimmedFileName.substring(0, trimmedFileName.length - 1);
} else {
break;
} }
if (trimmedFileName.length > maxLength) {
trimmedFileName = trimmedFileName.substring(0, maxLength);
} }
return trimmedFileName; return trimmedFileName;
} }

View File

@@ -80,6 +80,7 @@ static void my_application_activate(GApplication* application) {
gtk_window_set_default_size(window, 1280, 720); gtk_window_set_default_size(window, 1280, 720);
GdkVisual* visual; GdkVisual* visual;
gtk_widget_set_app_paintable(GTK_WIDGET(window), TRUE); gtk_widget_set_app_paintable(GTK_WIDGET(window), TRUE);
gtk_window_set_decorated(window, FALSE);
visual = gdk_screen_get_rgba_visual(screen); visual = gdk_screen_get_rgba_visual(screen);
if (visual != NULL && gdk_screen_is_composited(screen)) { if (visual != NULL && gdk_screen_is_composited(screen)) {
gtk_widget_set_visual(GTK_WIDGET(window), visual); gtk_widget_set_visual(GTK_WIDGET(window), visual);

View File

@@ -433,8 +433,8 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "."
ref: "690a03a954f1603e0149cfd479c8961b88f21336" ref: fe182cdf40e5fa6230f451bc1d643b860f610d13
resolved-ref: "690a03a954f1603e0149cfd479c8961b88f21336" resolved-ref: fe182cdf40e5fa6230f451bc1d643b860f610d13
url: "https://github.com/venera-app/flutter_saf" url: "https://github.com/venera-app/flutter_saf"
source: git source: git
version: "0.0.1" version: "0.0.1"

View File

@@ -2,7 +2,7 @@ name: venera
description: "A comic app." description: "A comic app."
publish_to: 'none' publish_to: 'none'
version: 1.3.4+134 version: 1.4.0+140
environment: environment:
sdk: '>=3.6.0 <4.0.0' sdk: '>=3.6.0 <4.0.0'
@@ -72,7 +72,7 @@ dependencies:
flutter_saf: flutter_saf:
git: git:
url: https://github.com/venera-app/flutter_saf url: https://github.com/venera-app/flutter_saf
ref: 690a03a954f1603e0149cfd479c8961b88f21336 ref: fe182cdf40e5fa6230f451bc1d643b860f610d13
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

View File

@@ -2,11 +2,11 @@
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
#define MyAppName "Venera" #define MyAppName "Venera"
#define MyAppVersion "1.3.4" #define MyAppVersion "{{version}}"
#define MyAppPublisher "nyne" #define MyAppPublisher "nyne"
#define MyAppURL "https://github.com/venera-app/venera" #define MyAppURL "https://github.com/venera-app/venera"
#define MyAppExeName "venera.exe" #define MyAppExeName "venera.exe"
#define RootPath "D:\code\venera" #define RootPath "{{root_path}}"
[Setup] [Setup]
; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. ; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.