mirror of
https://github.com/venera-app/venera.git
synced 2025-09-26 23:47:23 +00:00
Feat: Image favorites (#126)
* feat: 增加图片收藏 * feat: 主体图片收藏页面实现 * feat: 点击打开大图浏览 * feat: 数据结构变更 * feat: 基本完成 * feat: 翻译与bug修复 * feat: 实机测试和问题修复 * feat: jm导入, pica历史记录nhentai有问题, 一键反转 * fix: 大小写不一致, 一个htManga, 一个htmanga * feat: 拉取收藏优化 * feat: 改成以ep为准 * feat: 兜底一些可能报错场景 * chore: 没有用到 * feat: 尽量保证和网络收藏顺序一致 * feat: 支持显示热点tag * feat: 支持双击收藏, 不过此时禁止放大图片 * fix: 自动塞封面逻辑完善, 切换快速收藏图片立刻生效 * Refactor * fix updateValue * feat: 双击功能提示 * fix: 被确定取消收藏的才删除 * Refactor ImageFavoritesPage * translate author * feat: 功能提示改到dialog中 * fix text editing * fix text editing * feat: 功能提示放到邮件或长按菜单中 * fix: 修复tag过滤不生效问题 * Improve image loading * The default value of quickCollectImage should be false. * Refactor DragListener * Refactor ImageFavoriteItem & ImageFavoritePhotoView * Refactor * Fix `ImageFavoriteManager.has` * Fix UI * Improve UI --------- Co-authored-by: nyne <me@nyne.dev>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -15,6 +15,7 @@ migrate_working_dir/
|
|||||||
*.ipr
|
*.ipr
|
||||||
*.iws
|
*.iws
|
||||||
.idea/
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
|
||||||
# The .vscode folder contains launch configuration and tasks you configure in
|
# The .vscode folder contains launch configuration and tasks you configure in
|
||||||
# VS Code which you may wish to be included in version control, so this line
|
# VS Code which you may wish to be included in version control, so this line
|
||||||
|
@@ -3,4 +3,4 @@ android.useAndroidX=true
|
|||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
android.defaults.buildfeatures.buildconfig=true
|
android.defaults.buildfeatures.buildconfig=true
|
||||||
android.nonTransitiveRClass=false
|
android.nonTransitiveRClass=false
|
||||||
android.nonFinalResIds=false
|
android.nonFinalResIds=false
|
@@ -18,7 +18,7 @@
|
|||||||
"help": "帮助",
|
"help": "帮助",
|
||||||
"Select": "选择",
|
"Select": "选择",
|
||||||
"Selected @a comics": "已选择 @a 部漫画",
|
"Selected @a comics": "已选择 @a 部漫画",
|
||||||
"Imported @a comics": "已导入 @a 部漫画",
|
"Imported @a comics, loaded @b pages, received @c comics": "已导入 @a 部漫画, 加载 @b 页, 接收到 @c 部漫画",
|
||||||
"Downloading": "下载中",
|
"Downloading": "下载中",
|
||||||
"Back": "后退",
|
"Back": "后退",
|
||||||
"Delete": "删除",
|
"Delete": "删除",
|
||||||
@@ -41,6 +41,7 @@
|
|||||||
"Select a folder": "选择一个文件夹",
|
"Select a folder": "选择一个文件夹",
|
||||||
"Folder": "文件夹",
|
"Folder": "文件夹",
|
||||||
"Confirm": "确认",
|
"Confirm": "确认",
|
||||||
|
"Reversed successfully": "反转成功",
|
||||||
"Remove comic from favorite?": "从收藏中移除漫画?",
|
"Remove comic from favorite?": "从收藏中移除漫画?",
|
||||||
"Move": "移动",
|
"Move": "移动",
|
||||||
"Move to folder": "移动到文件夹",
|
"Move to folder": "移动到文件夹",
|
||||||
@@ -164,7 +165,7 @@
|
|||||||
"Date Desc": "日期降序",
|
"Date Desc": "日期降序",
|
||||||
"Start": "开始",
|
"Start": "开始",
|
||||||
"Export App Data": "导出应用数据",
|
"Export App Data": "导出应用数据",
|
||||||
"Import App Data": "导入应用数据",
|
"Import App Data (Please restart after success)": "导入应用数据(成功后请手动重启)",
|
||||||
"Export": "导出",
|
"Export": "导出",
|
||||||
"Download Threads": "下载线程数",
|
"Download Threads": "下载线程数",
|
||||||
"Update Time": "更新时间",
|
"Update Time": "更新时间",
|
||||||
@@ -248,6 +249,47 @@
|
|||||||
"Export as pdf": "导出为pdf",
|
"Export as pdf": "导出为pdf",
|
||||||
"Export as epub": "导出为epub",
|
"Export as epub": "导出为epub",
|
||||||
"Aggregated Search": "聚合搜索",
|
"Aggregated Search": "聚合搜索",
|
||||||
|
"Local comic collection is not supported at present": "本地收藏暂不支持",
|
||||||
|
"The cover cannot be uncollected here": "封面不能在此取消收藏",
|
||||||
|
"Uncollected the image": "取消收藏图片",
|
||||||
|
"Successfully collected": "收藏成功",
|
||||||
|
"Collect the image": "收藏图片",
|
||||||
|
"Quick collect image": "快速收藏图片",
|
||||||
|
"Not enable": "不启用",
|
||||||
|
"Double Tap": "双击",
|
||||||
|
"Swipe": "滑动",
|
||||||
|
"On the image browsing page, you can quickly collect images by sliding horizontally or vertically according to your reading mode": "在图片浏览页面, 你可以根据你的阅读模式横滑或者竖滑快速收藏图片",
|
||||||
|
"Calculate your favorite from @a comics and @b images": "从 @a 本漫画和 @b 张图片中, 计算你最喜欢的",
|
||||||
|
"After the parentheses are the number of pictures or the number of pictures compared to the number of comic pages": "括号后是图片数量或图片数比漫画页数",
|
||||||
|
"The chapter order of the comic may have changed, temporarily not supported for collection": "漫画的章节顺序可能发生了变化, 暂不支持收藏此章节",
|
||||||
|
"Author: ": "作者: ",
|
||||||
|
"Tags: ": "标签: ",
|
||||||
|
"Comics(number): ": "漫画(数量): ",
|
||||||
|
"Comics(percentage): ": "漫画(比例): ",
|
||||||
|
"Time Filter": "时间筛选",
|
||||||
|
"Image Favorites Greater Than": "图片收藏数大于",
|
||||||
|
"Collection time": "收藏时间",
|
||||||
|
"favoritesCompareComicPages": "收藏数与漫画页数比较",
|
||||||
|
"Cover": "封面",
|
||||||
|
"Page @a": "第 @a 页",
|
||||||
|
"Time Asc": "时间升序",
|
||||||
|
"Time Desc": "时间降序",
|
||||||
|
"Favorite Num": "收藏数",
|
||||||
|
"Favorite Num Compare Comic Pages": "收藏数比漫画页数",
|
||||||
|
"All": "全部",
|
||||||
|
"Last Week": "上周",
|
||||||
|
"Last Month": "上月",
|
||||||
|
"Last Half Year": "半年",
|
||||||
|
"Last Year": "一年",
|
||||||
|
"Filter": "筛选",
|
||||||
|
"Image Favorites": "图片收藏",
|
||||||
|
"Title": "标题",
|
||||||
|
"@a Cover": "@a 封面",
|
||||||
|
"Photo View": "图片浏览",
|
||||||
|
"Delete @a images": "删除 @a 张图片",
|
||||||
|
"Update the page number by the latest collection": "按最新收藏更新页数",
|
||||||
|
"Copy the title successfully": "复制标题成功",
|
||||||
|
"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": "下载已开始",
|
||||||
@@ -265,7 +307,10 @@
|
|||||||
"Aggregated": "聚合",
|
"Aggregated": "聚合",
|
||||||
"Default Search Target": "默认搜索目标",
|
"Default Search Target": "默认搜索目标",
|
||||||
"Auto Language Filters": "自动语言筛选",
|
"Auto Language Filters": "自动语言筛选",
|
||||||
"Check for updates on startup": "启动时检查更新"
|
"Check for updates on startup": "启动时检查更新",
|
||||||
|
"Start Time": "开始时间",
|
||||||
|
"End Time": "结束时间",
|
||||||
|
"Custom": "自定义"
|
||||||
},
|
},
|
||||||
"zh_TW": {
|
"zh_TW": {
|
||||||
"Home": "首頁",
|
"Home": "首頁",
|
||||||
@@ -287,7 +332,7 @@
|
|||||||
"help": "幫助",
|
"help": "幫助",
|
||||||
"Select": "選擇",
|
"Select": "選擇",
|
||||||
"Selected @a comics": "已選擇 @a 部漫畫",
|
"Selected @a comics": "已選擇 @a 部漫畫",
|
||||||
"Imported @a comics": "已匯入 @a 部漫畫",
|
"Imported @a comics, loaded @b pages, received @c comics": "已匯入 @a 部漫畫, 加載 @b 頁, 接收到 @c 部漫畫",
|
||||||
"Downloading": "下載中",
|
"Downloading": "下載中",
|
||||||
"Back": "後退",
|
"Back": "後退",
|
||||||
"Delete": "刪除",
|
"Delete": "刪除",
|
||||||
@@ -431,8 +476,9 @@
|
|||||||
"Date": "日期",
|
"Date": "日期",
|
||||||
"Date Desc": "日期降序",
|
"Date Desc": "日期降序",
|
||||||
"Start": "開始",
|
"Start": "開始",
|
||||||
|
"Reversed successfully": "反轉成功",
|
||||||
"Export App Data": "匯出應用數據",
|
"Export App Data": "匯出應用數據",
|
||||||
"Import App Data": "匯入應用數據",
|
"Import App Data (Please restart after success)": "匯入應用數據(成功后請手動重啟)",
|
||||||
"Export": "匯出",
|
"Export": "匯出",
|
||||||
"Download Threads": "下載線程數",
|
"Download Threads": "下載線程數",
|
||||||
"Update Time": "更新時間",
|
"Update Time": "更新時間",
|
||||||
@@ -520,6 +566,47 @@
|
|||||||
"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": "本地收藏暫不支持",
|
||||||
|
"The cover cannot be uncollected here": "封面不能在此取消收藏",
|
||||||
|
"Uncollected the image": "取消收藏圖片",
|
||||||
|
"Successfully collected": "收藏成功",
|
||||||
|
"Collect the image": "收藏圖片",
|
||||||
|
"Quick collect image": "快速收藏圖片",
|
||||||
|
"On the image browsing page, you can quickly collect images by sliding horizontally or vertically according to your reading mode": "在圖片瀏覽頁面, 你可以根據你的閱讀模式橫向或者縱向滑動快速收藏圖片",
|
||||||
|
"Calculate your favorite from @a comics and @b images": "從 @a 本漫畫和 @b 張圖片中, 計算你最喜歡的",
|
||||||
|
"After the parentheses are the number of pictures or the number of pictures compared to the number of comic pages": "括號後是圖片數量或圖片數比漫畫頁數",
|
||||||
|
"The chapter order of the comic may have changed, temporarily not supported for collection": "漫畫的章節順序可能發生了變化, 暫不支持收藏此章節",
|
||||||
|
"Author: ": "作者: ",
|
||||||
|
"Tags: ": "標籤: ",
|
||||||
|
"Comics(number): ": "漫畫(數量): ",
|
||||||
|
"Comics(percentage): ": "漫畫(比例): ",
|
||||||
|
"Time Filter": "時間篩選",
|
||||||
|
"Image Favorites Greater Than": "圖片收藏數大於",
|
||||||
|
"Collection time": "收藏時間",
|
||||||
|
"Not enable": "不启用",
|
||||||
|
"Double Tap": "雙擊",
|
||||||
|
"Swipe": "滑動",
|
||||||
|
"favoritesCompareComicPages": "收藏數與漫畫頁數比較",
|
||||||
|
"Cover": "封面",
|
||||||
|
"Page @a": "第 @a 頁",
|
||||||
|
"Time Asc": "時間升序",
|
||||||
|
"Time Desc": "時間降序",
|
||||||
|
"Favorite Num": "收藏數",
|
||||||
|
"Favorite Num Compare Comic Pages": "收藏數比漫畫頁數",
|
||||||
|
"All": "全部",
|
||||||
|
"Last Week": "上周",
|
||||||
|
"Last Month": "上月",
|
||||||
|
"Last Half Year": "半年",
|
||||||
|
"Last Year": "一年",
|
||||||
|
"Filter": "篩選",
|
||||||
|
"Image Favorites": "圖片收藏",
|
||||||
|
"Title": "標題",
|
||||||
|
"@a Cover": "@a 封面",
|
||||||
|
"Photo View": "圖片瀏覽",
|
||||||
|
"Delete @a images": "刪除 @a 張圖片",
|
||||||
|
"Update the page number by the latest collection": "按最新收藏更新頁數",
|
||||||
|
"Copy the title successfully": "複製標題成功",
|
||||||
|
"The comic is invalid, please long press to delete, you can double click the title to copy": "該漫畫已失效, 請長按刪除, 可以雙擊標題進行複製",
|
||||||
"End": "末尾",
|
"End": "末尾",
|
||||||
"None": "無",
|
"None": "無",
|
||||||
"View Detail": "查看詳情",
|
"View Detail": "查看詳情",
|
||||||
@@ -533,6 +620,9 @@
|
|||||||
"Aggregated": "聚合",
|
"Aggregated": "聚合",
|
||||||
"Default Search Target": "默認搜索目標",
|
"Default Search Target": "默認搜索目標",
|
||||||
"Auto Language Filters": "自動語言篩選",
|
"Auto Language Filters": "自動語言篩選",
|
||||||
"Check for updates on startup": "啟動時檢查更新"
|
"Check for updates on startup": "啟動時檢查更新",
|
||||||
|
"Start Time": "開始時間",
|
||||||
|
"End Time": "結束時間",
|
||||||
|
"Custom": "自定義"
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -577,6 +577,51 @@ class _IndicatorPainter extends CustomPainter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class TabViewBody extends StatefulWidget {
|
||||||
|
/// Create a tab view body, which will show the child at the current tab index.
|
||||||
|
const TabViewBody({super.key, required this.children, this.controller});
|
||||||
|
|
||||||
|
final List<Widget> children;
|
||||||
|
|
||||||
|
final TabController? controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<TabViewBody> createState() => _TabViewBodyState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TabViewBodyState extends State<TabViewBody> {
|
||||||
|
late TabController _controller;
|
||||||
|
|
||||||
|
int _currentIndex = 0;
|
||||||
|
|
||||||
|
void updateIndex() {
|
||||||
|
if (_controller.index != _currentIndex) {
|
||||||
|
setState(() {
|
||||||
|
_currentIndex = _controller.index;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
_controller = widget.controller ?? DefaultTabController.of(context);
|
||||||
|
_controller.addListener(updateIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
_controller.removeListener(updateIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return widget.children[_currentIndex];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class SearchBarController {
|
class SearchBarController {
|
||||||
_SearchBarMixin? _state;
|
_SearchBarMixin? _state;
|
||||||
|
|
||||||
|
@@ -726,9 +726,16 @@ class _SliverGridComicsState extends State<SliverGridComics> {
|
|||||||
comics.add(comic);
|
comics.add(comic);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
HistoryManager().addListener(update);
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
HistoryManager().removeListener(update);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
void update() {
|
void update() {
|
||||||
setState(() {
|
setState(() {
|
||||||
comics.clear();
|
comics.clear();
|
||||||
|
@@ -22,6 +22,7 @@ class AnimatedImage extends StatefulWidget {
|
|||||||
this.filterQuality = FilterQuality.medium,
|
this.filterQuality = FilterQuality.medium,
|
||||||
this.isAntiAlias = false,
|
this.isAntiAlias = false,
|
||||||
this.part,
|
this.part,
|
||||||
|
this.onError,
|
||||||
Map<String, String>? headers,
|
Map<String, String>? headers,
|
||||||
int? cacheWidth,
|
int? cacheWidth,
|
||||||
int? cacheHeight,
|
int? cacheHeight,
|
||||||
@@ -63,6 +64,8 @@ class AnimatedImage extends StatefulWidget {
|
|||||||
|
|
||||||
final ImagePart? part;
|
final ImagePart? part;
|
||||||
|
|
||||||
|
final Function? onError;
|
||||||
|
|
||||||
static void clear() => _AnimatedImageState.clear();
|
static void clear() => _AnimatedImageState.clear();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -169,6 +172,8 @@ class _AnimatedImageState extends State<AnimatedImage>
|
|||||||
_handleImageFrame,
|
_handleImageFrame,
|
||||||
onChunk: _handleImageChunk,
|
onChunk: _handleImageChunk,
|
||||||
onError: (Object error, StackTrace? stackTrace) {
|
onError: (Object error, StackTrace? stackTrace) {
|
||||||
|
// 图片加错错误回调
|
||||||
|
widget.onError?.call(error, stackTrace);
|
||||||
setState(() {
|
setState(() {
|
||||||
_lastException = error;
|
_lastException = error;
|
||||||
});
|
});
|
||||||
@@ -271,7 +276,7 @@ class _AnimatedImageState extends State<AnimatedImage>
|
|||||||
Widget result;
|
Widget result;
|
||||||
|
|
||||||
if (_imageInfo != null) {
|
if (_imageInfo != null) {
|
||||||
if(widget.part != null) {
|
if (widget.part != null) {
|
||||||
return CustomPaint(
|
return CustomPaint(
|
||||||
painter: ImagePainter(
|
painter: ImagePainter(
|
||||||
image: _imageInfo!.image,
|
image: _imageInfo!.image,
|
||||||
|
@@ -5,6 +5,7 @@ void showToast({
|
|||||||
required BuildContext context,
|
required BuildContext context,
|
||||||
Widget? icon,
|
Widget? icon,
|
||||||
Widget? trailing,
|
Widget? trailing,
|
||||||
|
int? seconds,
|
||||||
}) {
|
}) {
|
||||||
var newEntry = OverlayEntry(
|
var newEntry = OverlayEntry(
|
||||||
builder: (context) => _ToastOverlay(
|
builder: (context) => _ToastOverlay(
|
||||||
@@ -17,7 +18,7 @@ void showToast({
|
|||||||
|
|
||||||
state?.addOverlay(newEntry);
|
state?.addOverlay(newEntry);
|
||||||
|
|
||||||
Timer(const Duration(seconds: 2), () => state?.remove(newEntry));
|
Timer(Duration(seconds: seconds ?? 2), () => state?.remove(newEntry));
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ToastOverlay extends StatelessWidget {
|
class _ToastOverlay extends StatelessWidget {
|
||||||
@@ -48,7 +49,8 @@ class _ToastOverlay extends StatelessWidget {
|
|||||||
color: Theme.of(context).colorScheme.onInverseSurface),
|
color: Theme.of(context).colorScheme.onInverseSurface),
|
||||||
child: IntrinsicWidth(
|
child: IntrinsicWidth(
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 16),
|
padding:
|
||||||
|
const EdgeInsets.symmetric(vertical: 6, horizontal: 16),
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
maxWidth: context.width - 32,
|
maxWidth: context.width - 32,
|
||||||
),
|
),
|
||||||
@@ -241,13 +243,13 @@ LoadingDialogController showLoadingDialog(BuildContext context,
|
|||||||
class ContentDialog extends StatelessWidget {
|
class ContentDialog extends StatelessWidget {
|
||||||
const ContentDialog({
|
const ContentDialog({
|
||||||
super.key,
|
super.key,
|
||||||
required this.title,
|
this.title, // 如果不传 title 将不会展示
|
||||||
required this.content,
|
required this.content,
|
||||||
this.dismissible = true,
|
this.dismissible = true,
|
||||||
this.actions = const [],
|
this.actions = const [],
|
||||||
});
|
});
|
||||||
|
|
||||||
final String title;
|
final String? title;
|
||||||
|
|
||||||
final Widget content;
|
final Widget content;
|
||||||
|
|
||||||
@@ -261,14 +263,16 @@ class ContentDialog extends StatelessWidget {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Appbar(
|
title != null
|
||||||
leading: IconButton(
|
? Appbar(
|
||||||
icon: const Icon(Icons.close),
|
leading: IconButton(
|
||||||
onPressed: dismissible ? context.pop : null,
|
icon: const Icon(Icons.close),
|
||||||
),
|
onPressed: dismissible ? context.pop : null,
|
||||||
title: Text(title),
|
),
|
||||||
backgroundColor: Colors.transparent,
|
title: Text(title!),
|
||||||
),
|
backgroundColor: Colors.transparent,
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
this.content,
|
this.content,
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Row(
|
Row(
|
||||||
@@ -360,7 +364,7 @@ Future<void> showInputDialog({
|
|||||||
} else {
|
} else {
|
||||||
result = futureOr;
|
result = futureOr;
|
||||||
}
|
}
|
||||||
if(result == null) {
|
if (result == null) {
|
||||||
context.pop();
|
context.pop();
|
||||||
} else {
|
} else {
|
||||||
setState(() => error = result.toString());
|
setState(() => error = result.toString());
|
||||||
|
@@ -102,13 +102,36 @@ class _SmoothScrollProviderState extends State<SmoothScrollProvider> {
|
|||||||
duration: _fastAnimationDuration, curve: Curves.linear);
|
duration: _fastAnimationDuration, curve: Curves.linear);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: widget.builder(
|
child: ScrollControllerProvider._(
|
||||||
context,
|
controller: _controller,
|
||||||
_controller,
|
child: widget.builder(
|
||||||
_isMouseScroll
|
context,
|
||||||
? const NeverScrollableScrollPhysics()
|
_controller,
|
||||||
: const BouncingScrollPhysics(),
|
_isMouseScroll
|
||||||
|
? const NeverScrollableScrollPhysics()
|
||||||
|
: const BouncingScrollPhysics(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ScrollControllerProvider extends InheritedWidget {
|
||||||
|
const ScrollControllerProvider._({
|
||||||
|
required this.controller,
|
||||||
|
required super.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
final ScrollController controller;
|
||||||
|
|
||||||
|
static ScrollController of(BuildContext context) {
|
||||||
|
final ScrollControllerProvider? provider =
|
||||||
|
context.dependOnInheritedWidgetOfExactType<ScrollControllerProvider>();
|
||||||
|
return provider!.controller;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool updateShouldNotify(ScrollControllerProvider oldWidget) {
|
||||||
|
return oldWidget.controller != controller;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -143,6 +143,7 @@ class _Settings with ChangeNotifier {
|
|||||||
'quickFavorite': null,
|
'quickFavorite': null,
|
||||||
'enableTurnPageByVolumeKey': true,
|
'enableTurnPageByVolumeKey': true,
|
||||||
'enableClockAndBatteryInfoInReader': true,
|
'enableClockAndBatteryInfoInReader': true,
|
||||||
|
'quickCollectImage': 'No', // No, DoubleTap, Swipe
|
||||||
'authorizationRequired': false,
|
'authorizationRequired': false,
|
||||||
'onClickFavorite': 'viewDetail', // viewDetail, read
|
'onClickFavorite': 'viewDetail', // viewDetail, read
|
||||||
'enableDnsOverrides': false,
|
'enableDnsOverrides': false,
|
||||||
@@ -179,4 +180,4 @@ const _defaultCustomImageProcessing = '''
|
|||||||
async function processImage(image, cid, eid) {
|
async function processImage(image, cid, eid) {
|
||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
''';
|
''';
|
||||||
|
@@ -10,6 +10,10 @@ class FavoriteData {
|
|||||||
|
|
||||||
final bool multiFolder;
|
final bool multiFolder;
|
||||||
|
|
||||||
|
// 这个收藏时间新旧顺序, 是为了最小成本同步远端的收藏, 只拉取远程最新收藏的漫画, 就不需要全拉取一遍了
|
||||||
|
// 如果为 null, 当做从新到旧
|
||||||
|
final bool? isOldToNewSort;
|
||||||
|
|
||||||
final Future<Res<List<Comic>>> Function(int page, [String? folder])?
|
final Future<Res<List<Comic>>> Function(int page, [String? folder])?
|
||||||
loadComic;
|
loadComic;
|
||||||
|
|
||||||
@@ -44,6 +48,7 @@ class FavoriteData {
|
|||||||
this.addFolder,
|
this.addFolder,
|
||||||
this.allFavoritesId,
|
this.allFavoritesId,
|
||||||
this.addOrDelFavorite,
|
this.addOrDelFavorite,
|
||||||
|
this.isOldToNewSort,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -73,7 +73,8 @@ class Comic {
|
|||||||
this.sourceKey,
|
this.sourceKey,
|
||||||
this.maxPage,
|
this.maxPage,
|
||||||
this.language,
|
this.language,
|
||||||
): favoriteId = null, stars = null;
|
) : favoriteId = null,
|
||||||
|
stars = null;
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
return {
|
return {
|
||||||
@@ -231,6 +232,34 @@ class ComicDetails with HistoryMixin {
|
|||||||
String get id => comicId;
|
String get id => comicId;
|
||||||
|
|
||||||
ComicType get comicType => ComicType(sourceKey.hashCode);
|
ComicType get comicType => ComicType(sourceKey.hashCode);
|
||||||
|
|
||||||
|
/// Convert tags map to plain list
|
||||||
|
List<String> get plainTags {
|
||||||
|
var res = <String>[];
|
||||||
|
tags.forEach((key, value) {
|
||||||
|
res.addAll(value.map((e) => "$key:$e"));
|
||||||
|
});
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find the first author tag
|
||||||
|
String? findAuthor() {
|
||||||
|
var authorNamespaces = [
|
||||||
|
"author",
|
||||||
|
"authors",
|
||||||
|
"artist",
|
||||||
|
"artists",
|
||||||
|
"作者",
|
||||||
|
"画师"
|
||||||
|
];
|
||||||
|
for (var entry in tags.entries) {
|
||||||
|
if (authorNamespaces.contains(entry.key.toLowerCase()) &&
|
||||||
|
entry.value.isNotEmpty) {
|
||||||
|
return entry.value.first;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ArchiveInfo {
|
class ArchiveInfo {
|
||||||
@@ -242,4 +271,4 @@ class ArchiveInfo {
|
|||||||
: title = json["title"],
|
: title = json["title"],
|
||||||
description = json["description"],
|
description = json["description"],
|
||||||
id = json["id"];
|
id = json["id"];
|
||||||
}
|
}
|
||||||
|
@@ -193,7 +193,7 @@ class ComicSourceParser {
|
|||||||
login = (account, pwd) async {
|
login = (account, pwd) async {
|
||||||
try {
|
try {
|
||||||
await JsEngine().runCode("""
|
await JsEngine().runCode("""
|
||||||
ComicSource.sources.$_key.account.login(${jsonEncode(account)},
|
ComicSource.sources.$_key.account.login(${jsonEncode(account)},
|
||||||
${jsonEncode(pwd)})
|
${jsonEncode(pwd)})
|
||||||
""");
|
""");
|
||||||
var source = ComicSource.find(_key!)!;
|
var source = ComicSource.find(_key!)!;
|
||||||
@@ -502,9 +502,9 @@ class ComicSourceParser {
|
|||||||
try {
|
try {
|
||||||
var res = await JsEngine().runCode("""
|
var res = await JsEngine().runCode("""
|
||||||
ComicSource.sources.$_key.categoryComics.load(
|
ComicSource.sources.$_key.categoryComics.load(
|
||||||
${jsonEncode(category)},
|
${jsonEncode(category)},
|
||||||
${jsonEncode(param)},
|
${jsonEncode(param)},
|
||||||
${jsonEncode(options)},
|
${jsonEncode(options)},
|
||||||
${jsonEncode(page)}
|
${jsonEncode(page)}
|
||||||
)
|
)
|
||||||
""");
|
""");
|
||||||
@@ -618,6 +618,7 @@ class ComicSourceParser {
|
|||||||
if (!_checkExists("favorites")) return null;
|
if (!_checkExists("favorites")) return null;
|
||||||
|
|
||||||
final bool multiFolder = _getValue("favorites.multiFolder");
|
final bool multiFolder = _getValue("favorites.multiFolder");
|
||||||
|
final bool? isOldToNewSort = _getValue("favorites.isOldToNewSort");
|
||||||
|
|
||||||
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) {
|
||||||
@@ -770,6 +771,7 @@ class ComicSourceParser {
|
|||||||
addFolder: addFolder,
|
addFolder: addFolder,
|
||||||
deleteFolder: deleteFolder,
|
deleteFolder: deleteFolder,
|
||||||
addOrDelFavorite: addOrDelFavFunc,
|
addOrDelFavorite: addOrDelFavFunc,
|
||||||
|
isOldToNewSort: isOldToNewSort,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,6 +1,17 @@
|
|||||||
|
/// If window width is less than this value, it is considered as mobile.
|
||||||
const changePoint = 600;
|
const changePoint = 600;
|
||||||
|
|
||||||
|
/// If window width is less than this value, it is considered as tablet.
|
||||||
|
///
|
||||||
|
/// If it is more than this value, it is considered as desktop.
|
||||||
const changePoint2 = 1300;
|
const changePoint2 = 1300;
|
||||||
|
|
||||||
|
/// Default user agent for http requests.
|
||||||
const webUA =
|
const webUA =
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36";
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36";
|
||||||
|
|
||||||
|
/// Pages for all comics is started from this value.
|
||||||
|
const firstPage = 1;
|
||||||
|
|
||||||
|
/// Chapters for all comics is started from this value.
|
||||||
|
const firstChapter = 1;
|
@@ -36,6 +36,8 @@ extension Navigation on BuildContext {
|
|||||||
|
|
||||||
Brightness get brightness => Theme.of(this).brightness;
|
Brightness get brightness => Theme.of(this).brightness;
|
||||||
|
|
||||||
|
bool get isDarkMode => brightness == Brightness.dark;
|
||||||
|
|
||||||
void showMessage({required String message}) {
|
void showMessage({required String message}) {
|
||||||
showToast(message: message, context: this);
|
showToast(message: message, context: this);
|
||||||
}
|
}
|
||||||
|
@@ -1,12 +1,23 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:isolate';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter/widgets.dart' show ChangeNotifier;
|
import 'package:flutter/widgets.dart' show ChangeNotifier;
|
||||||
import 'package:sqlite3/sqlite3.dart';
|
import 'package:sqlite3/sqlite3.dart';
|
||||||
import 'package:venera/foundation/comic_source/comic_source.dart';
|
import 'package:venera/foundation/comic_source/comic_source.dart';
|
||||||
import 'package:venera/foundation/comic_type.dart';
|
import 'package:venera/foundation/comic_type.dart';
|
||||||
|
import 'package:venera/foundation/image_provider/image_favorites_provider.dart';
|
||||||
|
import 'package:venera/foundation/log.dart';
|
||||||
|
import 'package:venera/utils/ext.dart';
|
||||||
import 'package:venera/utils/translations.dart';
|
import 'package:venera/utils/translations.dart';
|
||||||
|
|
||||||
import 'app.dart';
|
import 'app.dart';
|
||||||
|
import 'consts.dart';
|
||||||
|
|
||||||
|
part "image_favorites.dart";
|
||||||
|
|
||||||
typedef HistoryType = ComicType;
|
typedef HistoryType = ComicType;
|
||||||
|
|
||||||
@@ -37,7 +48,7 @@ class History implements Comic {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String cover;
|
String cover;
|
||||||
|
|
||||||
int ep;
|
int ep;
|
||||||
|
|
||||||
int page;
|
int page;
|
||||||
@@ -201,7 +212,12 @@ class HistoryManager with ChangeNotifier {
|
|||||||
|
|
||||||
Map<String, bool>? _cachedHistory;
|
Map<String, bool>? _cachedHistory;
|
||||||
|
|
||||||
|
bool isInitialized = false;
|
||||||
|
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
|
if (isInitialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
_db = sqlite3.open("${App.dataPath}/history.db");
|
_db = sqlite3.open("${App.dataPath}/history.db");
|
||||||
|
|
||||||
_db.execute("""
|
_db.execute("""
|
||||||
@@ -220,6 +236,8 @@ class HistoryManager with ChangeNotifier {
|
|||||||
""");
|
""");
|
||||||
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
ImageFavoriteManager().init();
|
||||||
|
isInitialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// add history. if exists, update time.
|
/// add history. if exists, update time.
|
||||||
@@ -275,7 +293,7 @@ class HistoryManager with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
History? findSync(String id, ComicType type) {
|
History? findSync(String id, ComicType type) {
|
||||||
if(_cachedHistory == null) {
|
if (_cachedHistory == null) {
|
||||||
updateCache();
|
updateCache();
|
||||||
}
|
}
|
||||||
if (!_cachedHistory!.containsKey(id)) {
|
if (!_cachedHistory!.containsKey(id)) {
|
||||||
|
535
lib/foundation/image_favorites.dart
Normal file
535
lib/foundation/image_favorites.dart
Normal file
@@ -0,0 +1,535 @@
|
|||||||
|
part of "history.dart";
|
||||||
|
|
||||||
|
class ImageFavorite {
|
||||||
|
final String eid;
|
||||||
|
final String id; // 漫画id
|
||||||
|
final int ep;
|
||||||
|
final String epName;
|
||||||
|
final String sourceKey;
|
||||||
|
String imageKey;
|
||||||
|
int page;
|
||||||
|
bool? isAutoFavorite;
|
||||||
|
|
||||||
|
ImageFavorite(
|
||||||
|
this.page,
|
||||||
|
this.imageKey,
|
||||||
|
this.isAutoFavorite,
|
||||||
|
this.eid,
|
||||||
|
this.id,
|
||||||
|
this.ep,
|
||||||
|
this.sourceKey,
|
||||||
|
this.epName,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'page': page,
|
||||||
|
'imageKey': imageKey,
|
||||||
|
'isAutoFavorite': isAutoFavorite,
|
||||||
|
'eid': eid,
|
||||||
|
'id': id,
|
||||||
|
'ep': ep,
|
||||||
|
'sourceKey': sourceKey,
|
||||||
|
'epName': epName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageFavorite.fromJson(Map<String, dynamic> json)
|
||||||
|
: page = json['page'],
|
||||||
|
imageKey = json['imageKey'],
|
||||||
|
isAutoFavorite = json['isAutoFavorite'],
|
||||||
|
eid = json['eid'],
|
||||||
|
id = json['id'],
|
||||||
|
ep = json['ep'],
|
||||||
|
sourceKey = json['sourceKey'],
|
||||||
|
epName = json['epName'];
|
||||||
|
|
||||||
|
ImageFavorite copyWith({
|
||||||
|
int? page,
|
||||||
|
String? imageKey,
|
||||||
|
bool? isAutoFavorite,
|
||||||
|
String? eid,
|
||||||
|
String? id,
|
||||||
|
int? ep,
|
||||||
|
String? sourceKey,
|
||||||
|
String? epName,
|
||||||
|
}) {
|
||||||
|
return ImageFavorite(
|
||||||
|
page ?? this.page,
|
||||||
|
imageKey ?? this.imageKey,
|
||||||
|
isAutoFavorite ?? this.isAutoFavorite,
|
||||||
|
eid ?? this.eid,
|
||||||
|
id ?? this.id,
|
||||||
|
ep ?? this.ep,
|
||||||
|
sourceKey ?? this.sourceKey,
|
||||||
|
epName ?? this.epName,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is ImageFavorite &&
|
||||||
|
other.id == id &&
|
||||||
|
other.sourceKey == sourceKey &&
|
||||||
|
other.page == page &&
|
||||||
|
other.eid == eid &&
|
||||||
|
other.ep == ep;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(id, sourceKey, page, eid, ep);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ImageFavoritesEp {
|
||||||
|
// 小心拷贝等多章节的可能更新章节顺序
|
||||||
|
String eid;
|
||||||
|
final int ep;
|
||||||
|
int maxPage;
|
||||||
|
String epName;
|
||||||
|
List<ImageFavorite> imageFavorites;
|
||||||
|
|
||||||
|
ImageFavoritesEp(
|
||||||
|
this.eid, this.ep, this.imageFavorites, this.epName, this.maxPage);
|
||||||
|
|
||||||
|
// 是否有封面
|
||||||
|
bool get isHasFirstPage {
|
||||||
|
return imageFavorites[0].page == firstPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 是否都有imageKey
|
||||||
|
bool get isHasImageKey {
|
||||||
|
return imageFavorites.every((e) => e.imageKey != "");
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'eid': eid,
|
||||||
|
'ep': ep,
|
||||||
|
'maxPage': maxPage,
|
||||||
|
'epName': epName,
|
||||||
|
'imageFavorites': imageFavorites.map((e) => e.toJson()).toList(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ImageFavoritesComic {
|
||||||
|
final String id;
|
||||||
|
final String title;
|
||||||
|
String subTitle;
|
||||||
|
String author;
|
||||||
|
final String sourceKey;
|
||||||
|
|
||||||
|
// 不一定是真的这本漫画的所有页数, 如果是多章节的时候
|
||||||
|
int maxPage;
|
||||||
|
List<String> tags;
|
||||||
|
List<String> translatedTags;
|
||||||
|
final DateTime time;
|
||||||
|
List<ImageFavoritesEp> imageFavoritesEp;
|
||||||
|
final Map<String, dynamic> other;
|
||||||
|
|
||||||
|
ImageFavoritesComic(
|
||||||
|
this.id,
|
||||||
|
this.imageFavoritesEp,
|
||||||
|
this.title,
|
||||||
|
this.sourceKey,
|
||||||
|
this.tags,
|
||||||
|
this.translatedTags,
|
||||||
|
this.time,
|
||||||
|
this.author,
|
||||||
|
this.other,
|
||||||
|
this.subTitle,
|
||||||
|
this.maxPage,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 是否都有imageKey
|
||||||
|
bool get isAllHasImageKey {
|
||||||
|
return imageFavoritesEp
|
||||||
|
.every((e) => e.imageFavorites.every((j) => j.imageKey != ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
int get maxPageFromEp {
|
||||||
|
int temp = 0;
|
||||||
|
for (var e in imageFavoritesEp) {
|
||||||
|
temp += e.maxPage;
|
||||||
|
}
|
||||||
|
return temp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 是否都有封面
|
||||||
|
bool get isAllHasFirstPage {
|
||||||
|
return imageFavoritesEp.every((e) => e.isHasFirstPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
Iterable<ImageFavorite> get images sync*{
|
||||||
|
for (var e in imageFavoritesEp) {
|
||||||
|
yield* e.imageFavorites;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is ImageFavoritesComic &&
|
||||||
|
other.id == id &&
|
||||||
|
other.sourceKey == sourceKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(id, sourceKey);
|
||||||
|
|
||||||
|
factory ImageFavoritesComic.fromRow(Row r) {
|
||||||
|
var tempImageFavoritesEp = jsonDecode(r["image_favorites_ep"]);
|
||||||
|
List<ImageFavoritesEp> finalImageFavoritesEp = [];
|
||||||
|
tempImageFavoritesEp.forEach((i) {
|
||||||
|
List<ImageFavorite> temp = [];
|
||||||
|
i["imageFavorites"].forEach((j) {
|
||||||
|
temp.add(ImageFavorite(
|
||||||
|
j["page"],
|
||||||
|
j["imageKey"],
|
||||||
|
j["isAutoFavorite"],
|
||||||
|
i["eid"],
|
||||||
|
r["id"],
|
||||||
|
i["ep"],
|
||||||
|
r["source_key"],
|
||||||
|
i["epName"],
|
||||||
|
));
|
||||||
|
});
|
||||||
|
finalImageFavoritesEp.add(ImageFavoritesEp(
|
||||||
|
i["eid"], i["ep"], temp, i["epName"], i["maxPage"] ?? 1));
|
||||||
|
});
|
||||||
|
return ImageFavoritesComic(
|
||||||
|
r["id"],
|
||||||
|
finalImageFavoritesEp,
|
||||||
|
r["title"],
|
||||||
|
r["source_key"],
|
||||||
|
r["tags"].split(","),
|
||||||
|
r["translated_tags"].split(","),
|
||||||
|
DateTime.fromMillisecondsSinceEpoch(r["time"]),
|
||||||
|
r["author"],
|
||||||
|
jsonDecode(r["other"]),
|
||||||
|
r["sub_title"],
|
||||||
|
r["max_page"],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ImageFavoriteManager with ChangeNotifier {
|
||||||
|
Database get _db => HistoryManager()._db;
|
||||||
|
|
||||||
|
List<ImageFavoritesComic> get comics => getAll();
|
||||||
|
|
||||||
|
static ImageFavoriteManager? _cache;
|
||||||
|
|
||||||
|
ImageFavoriteManager._();
|
||||||
|
|
||||||
|
factory ImageFavoriteManager() => (_cache ??= ImageFavoriteManager._());
|
||||||
|
|
||||||
|
/// 检查表image_favorites是否存在, 不存在则创建
|
||||||
|
void init() {
|
||||||
|
_db.execute("CREATE TABLE IF NOT EXISTS image_favorites ("
|
||||||
|
"id TEXT,"
|
||||||
|
"title TEXT NOT NULL,"
|
||||||
|
"sub_title TEXT,"
|
||||||
|
"author TEXT,"
|
||||||
|
"tags TEXT,"
|
||||||
|
"translated_tags TEXT,"
|
||||||
|
"time int,"
|
||||||
|
"max_page int,"
|
||||||
|
"source_key TEXT NOT NULL,"
|
||||||
|
"image_favorites_ep TEXT NOT NULL,"
|
||||||
|
"other TEXT NOT NULL,"
|
||||||
|
"PRIMARY KEY (id,source_key)"
|
||||||
|
");");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 做排序和去重的操作
|
||||||
|
void addOrUpdateOrDelete(ImageFavoritesComic favorite, [bool notify = true]) {
|
||||||
|
// 没有章节了就删掉
|
||||||
|
if (favorite.imageFavoritesEp.isEmpty) {
|
||||||
|
_db.execute("""
|
||||||
|
delete from image_favorites
|
||||||
|
where id == ? and source_key == ?;
|
||||||
|
""", [favorite.id, favorite.sourceKey]);
|
||||||
|
} else {
|
||||||
|
// 去重章节
|
||||||
|
List<ImageFavoritesEp> tempImageFavoritesEp = [];
|
||||||
|
for (var e in favorite.imageFavoritesEp) {
|
||||||
|
int index = tempImageFavoritesEp.indexWhere((i) {
|
||||||
|
return i.ep == e.ep;
|
||||||
|
});
|
||||||
|
// 再做一层保险, 防止出现ep为0的脏数据
|
||||||
|
if (index == -1 && e.ep > 0) {
|
||||||
|
tempImageFavoritesEp.add(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tempImageFavoritesEp.sort((a, b) => a.ep.compareTo(b.ep));
|
||||||
|
List<dynamic> finalImageFavoritesEp =
|
||||||
|
jsonDecode(jsonEncode(tempImageFavoritesEp));
|
||||||
|
for (var e in tempImageFavoritesEp) {
|
||||||
|
List<Map> finalImageFavorites = [];
|
||||||
|
int epIndex = tempImageFavoritesEp.indexOf(e);
|
||||||
|
for (ImageFavorite j in e.imageFavorites) {
|
||||||
|
int index =
|
||||||
|
finalImageFavorites.indexWhere((i) => i["page"] == j.page);
|
||||||
|
if (index == -1 && j.page > 0) {
|
||||||
|
// isAutoFavorite 为 null 不写入数据库, 同时只保留需要的属性, 避免增加太多重复字段在数据库里
|
||||||
|
if (j.isAutoFavorite != null) {
|
||||||
|
finalImageFavorites.add({
|
||||||
|
"page": j.page,
|
||||||
|
"imageKey": j.imageKey,
|
||||||
|
"isAutoFavorite": j.isAutoFavorite
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
finalImageFavorites.add({"page": j.page, "imageKey": j.imageKey});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finalImageFavorites.sort((a, b) => a["page"].compareTo(b["page"]));
|
||||||
|
finalImageFavoritesEp[epIndex]["imageFavorites"] = finalImageFavorites;
|
||||||
|
}
|
||||||
|
if (tempImageFavoritesEp.isEmpty) {
|
||||||
|
throw "Error: No ImageFavoritesEp";
|
||||||
|
}
|
||||||
|
_db.execute("""
|
||||||
|
insert or replace into image_favorites(id, title, sub_title, author, tags, translated_tags, time, max_page, source_key, image_favorites_ep, other)
|
||||||
|
values(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
|
||||||
|
""", [
|
||||||
|
favorite.id,
|
||||||
|
favorite.title,
|
||||||
|
favorite.subTitle,
|
||||||
|
favorite.author,
|
||||||
|
favorite.tags.join(","),
|
||||||
|
favorite.translatedTags.join(","),
|
||||||
|
favorite.time.millisecondsSinceEpoch,
|
||||||
|
favorite.maxPage,
|
||||||
|
favorite.sourceKey,
|
||||||
|
jsonEncode(finalImageFavoritesEp),
|
||||||
|
jsonEncode(favorite.other)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
if (notify) {
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool has(String id, String sourceKey, String eid, int page, int ep) {
|
||||||
|
var comic = find(id, sourceKey);
|
||||||
|
if (comic == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var epIndex = comic.imageFavoritesEp.where((e) => e.eid == eid).firstOrNull;
|
||||||
|
if (epIndex == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return epIndex.imageFavorites.any((e) => e.page == page && e.ep == ep);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ImageFavoritesComic> getAll([String? keyword]) {
|
||||||
|
ResultSet res;
|
||||||
|
if (keyword == null || keyword == "") {
|
||||||
|
res = _db.select("select * from image_favorites;");
|
||||||
|
} else {
|
||||||
|
res = _db.select(
|
||||||
|
"""
|
||||||
|
select * from image_favorites
|
||||||
|
WHERE title LIKE ?
|
||||||
|
OR sub_title LIKE ?
|
||||||
|
OR LOWER(tags) LIKE LOWER(?)
|
||||||
|
OR LOWER(translated_tags) LIKE LOWER(?)
|
||||||
|
OR author LIKE ?;
|
||||||
|
""",
|
||||||
|
['%$keyword%', '%$keyword%', '%$keyword%', '%$keyword%', '%$keyword%'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return res.map((e) => ImageFavoritesComic.fromRow(e)).toList();
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
Log.error("Unhandled Exception", e.toString(), stackTrace);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void deleteImageFavorite(Iterable<ImageFavorite> imageFavoriteList) {
|
||||||
|
if (imageFavoriteList.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (var i in imageFavoriteList) {
|
||||||
|
ImageFavoritesProvider.deleteFromCache(i);
|
||||||
|
}
|
||||||
|
var comics = <ImageFavoritesComic>{};
|
||||||
|
for (var i in imageFavoriteList) {
|
||||||
|
var comic = comics
|
||||||
|
.where((c) => c.id == i.id && c.sourceKey == i.sourceKey)
|
||||||
|
.firstOrNull ??
|
||||||
|
find(i.id, i.sourceKey);
|
||||||
|
if (comic == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var ep = comic.imageFavoritesEp.firstWhereOrNull((e) => e.ep == i.ep);
|
||||||
|
if (ep == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ep.imageFavorites.remove(i);
|
||||||
|
if (ep.imageFavorites.isEmpty) {
|
||||||
|
comic.imageFavoritesEp.remove(ep);
|
||||||
|
}
|
||||||
|
comics.add(comic);
|
||||||
|
}
|
||||||
|
for (var i in comics) {
|
||||||
|
addOrUpdateOrDelete(i, false);
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
int get length {
|
||||||
|
var res = _db.select("select count(*) from image_favorites;");
|
||||||
|
return res.first.values.first! as int;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ImageFavoritesComic> search(String keyword) {
|
||||||
|
if (keyword == "") {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return getAll(keyword);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<ImageFavoritesComputed> computeImageFavorites() {
|
||||||
|
var token = ServicesBinding.rootIsolateToken!;
|
||||||
|
var count = ImageFavoriteManager().length;
|
||||||
|
if (count == 0) {
|
||||||
|
return Future.value(ImageFavoritesComputed([], [], []));
|
||||||
|
} else if (count > 100) {
|
||||||
|
return Isolate.run(() async {
|
||||||
|
BackgroundIsolateBinaryMessenger.ensureInitialized(token);
|
||||||
|
await App.init();
|
||||||
|
await HistoryManager().init();
|
||||||
|
return _computeImageFavorites();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return Future.value(_computeImageFavorites());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static ImageFavoritesComputed _computeImageFavorites() {
|
||||||
|
const maxLength = 20;
|
||||||
|
|
||||||
|
var comics = ImageFavoriteManager().getAll();
|
||||||
|
// 去掉这些没有意义的标签
|
||||||
|
const List<String> exceptTags = [
|
||||||
|
'連載中',
|
||||||
|
'',
|
||||||
|
'translated',
|
||||||
|
'chinese',
|
||||||
|
'sole male',
|
||||||
|
'sole female',
|
||||||
|
'original',
|
||||||
|
'doujinshi',
|
||||||
|
'manga',
|
||||||
|
'multi-work series',
|
||||||
|
'mosaic censorship',
|
||||||
|
'dilf',
|
||||||
|
'bbm',
|
||||||
|
'uncensored',
|
||||||
|
'full censorship'
|
||||||
|
];
|
||||||
|
|
||||||
|
Map<String, int> tagCount = {};
|
||||||
|
Map<String, int> authorCount = {};
|
||||||
|
Map<ImageFavoritesComic, int> comicImageCount = {};
|
||||||
|
Map<ImageFavoritesComic, int> comicMaxPages = {};
|
||||||
|
|
||||||
|
for (var comic in comics) {
|
||||||
|
for (var tag in comic.tags) {
|
||||||
|
String finalTag = tag;
|
||||||
|
tagCount[finalTag] = (tagCount[finalTag] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comic.author != "") {
|
||||||
|
String finalAuthor = comic.author;
|
||||||
|
authorCount[finalAuthor] =
|
||||||
|
(authorCount[finalAuthor] ?? 0) + comic.images.length;
|
||||||
|
}
|
||||||
|
// 小于10页的漫画不统计
|
||||||
|
if (comic.maxPageFromEp < 10) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
comicImageCount[comic] =
|
||||||
|
(comicImageCount[comic] ?? 0) + comic.images.length;
|
||||||
|
comicMaxPages[comic] = (comicMaxPages[comic] ?? 0) + comic.maxPageFromEp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按数量排序标签
|
||||||
|
List<String> sortedTags = tagCount.keys.toList()
|
||||||
|
..sort((a, b) => tagCount[b]!.compareTo(tagCount[a]!));
|
||||||
|
|
||||||
|
// 按数量排序作者
|
||||||
|
List<String> sortedAuthors = authorCount.keys.toList()
|
||||||
|
..sort((a, b) => authorCount[b]!.compareTo(authorCount[a]!));
|
||||||
|
|
||||||
|
// 按收藏数量排序漫画
|
||||||
|
List<MapEntry<ImageFavoritesComic, int>> sortedComicsByNum =
|
||||||
|
comicImageCount.entries.toList()
|
||||||
|
..sort((a, b) => b.value.compareTo(a.value));
|
||||||
|
|
||||||
|
validateTag(String tag) {
|
||||||
|
if (tag.startsWith("Category:")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !exceptTags.contains(tag.split(":").last.toLowerCase()) &&
|
||||||
|
!tag.isNum;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ImageFavoritesComputed(
|
||||||
|
sortedTags
|
||||||
|
.where(validateTag)
|
||||||
|
.map((tag) => TextWithCount(tag, tagCount[tag]!))
|
||||||
|
.take(maxLength)
|
||||||
|
.toList(),
|
||||||
|
sortedAuthors
|
||||||
|
.map((author) => TextWithCount(author, authorCount[author]!))
|
||||||
|
.take(maxLength)
|
||||||
|
.toList(),
|
||||||
|
sortedComicsByNum
|
||||||
|
.map((comic) => TextWithCount(comic.key.title, comic.value))
|
||||||
|
.take(maxLength)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageFavoritesComic? find(String id, String sourceKey) {
|
||||||
|
var row = _db.select("""
|
||||||
|
select * from image_favorites
|
||||||
|
where id == ? and source_key == ?;
|
||||||
|
""", [id, sourceKey]);
|
||||||
|
if (row.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return ImageFavoritesComic.fromRow(row.first);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TextWithCount {
|
||||||
|
final String text;
|
||||||
|
final int count;
|
||||||
|
|
||||||
|
const TextWithCount(this.text, this.count);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ImageFavoritesComputed {
|
||||||
|
/// 基于收藏的标签数排序
|
||||||
|
final List<TextWithCount> tags;
|
||||||
|
|
||||||
|
/// 基于收藏的作者数排序
|
||||||
|
final List<TextWithCount> authors;
|
||||||
|
|
||||||
|
/// 基于喜欢的图片数排序
|
||||||
|
final List<TextWithCount> comics;
|
||||||
|
|
||||||
|
/// 计算后的图片收藏数据
|
||||||
|
const ImageFavoritesComputed(
|
||||||
|
this.tags,
|
||||||
|
this.authors,
|
||||||
|
this.comics,
|
||||||
|
);
|
||||||
|
|
||||||
|
bool get isEmpty => tags.isEmpty && authors.isEmpty && comics.isEmpty;
|
||||||
|
}
|
@@ -6,6 +6,7 @@ import 'dart:ui';
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:venera/foundation/cache_manager.dart';
|
import 'package:venera/foundation/cache_manager.dart';
|
||||||
|
import 'package:venera/foundation/log.dart';
|
||||||
|
|
||||||
abstract class BaseImageProvider<T extends BaseImageProvider<T>>
|
abstract class BaseImageProvider<T extends BaseImageProvider<T>>
|
||||||
extends ImageProvider<T> {
|
extends ImageProvider<T> {
|
||||||
@@ -126,10 +127,11 @@ abstract class BaseImageProvider<T extends BaseImageProvider<T>>
|
|||||||
}
|
}
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e, s) {
|
||||||
scheduleMicrotask(() {
|
scheduleMicrotask(() {
|
||||||
PaintingBinding.instance.imageCache.evict(key);
|
PaintingBinding.instance.imageCache.evict(key);
|
||||||
});
|
});
|
||||||
|
Log.error("Image Loading", e, s);
|
||||||
rethrow;
|
rethrow;
|
||||||
} finally {
|
} finally {
|
||||||
chunkEvents.close();
|
chunkEvents.close();
|
||||||
|
146
lib/foundation/image_provider/image_favorites_provider.dart
Normal file
146
lib/foundation/image_provider/image_favorites_provider.dart
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
import 'dart:async' show Future, StreamController;
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:crypto/crypto.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:venera/foundation/app.dart';
|
||||||
|
import 'package:venera/foundation/comic_source/comic_source.dart';
|
||||||
|
import 'package:venera/foundation/comic_type.dart';
|
||||||
|
import 'package:venera/foundation/local.dart';
|
||||||
|
import 'package:venera/network/images.dart';
|
||||||
|
import 'package:venera/utils/io.dart';
|
||||||
|
import '../history.dart';
|
||||||
|
import 'base_image_provider.dart';
|
||||||
|
import 'image_favorites_provider.dart' as image_provider;
|
||||||
|
|
||||||
|
class ImageFavoritesProvider
|
||||||
|
extends BaseImageProvider<image_provider.ImageFavoritesProvider> {
|
||||||
|
/// Image provider for imageFavorites
|
||||||
|
const ImageFavoritesProvider(this.imageFavorite);
|
||||||
|
|
||||||
|
final ImageFavorite imageFavorite;
|
||||||
|
|
||||||
|
int get page => imageFavorite.page;
|
||||||
|
|
||||||
|
String get sourceKey => imageFavorite.sourceKey;
|
||||||
|
|
||||||
|
String get cid => imageFavorite.id;
|
||||||
|
|
||||||
|
String get eid => imageFavorite.eid;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Uint8List> load(StreamController<ImageChunkEvent>? chunkEvents) async {
|
||||||
|
var imageKey = imageFavorite.imageKey;
|
||||||
|
var localImage = await getImageFromLocal();
|
||||||
|
if (localImage != null) {
|
||||||
|
return localImage;
|
||||||
|
}
|
||||||
|
var cacheImage = await readFromCache();
|
||||||
|
if (cacheImage != null) {
|
||||||
|
return cacheImage;
|
||||||
|
}
|
||||||
|
var gotImageKey = false;
|
||||||
|
if (imageKey == "") {
|
||||||
|
imageKey = await getImageKey();
|
||||||
|
gotImageKey = true;
|
||||||
|
}
|
||||||
|
Uint8List image;
|
||||||
|
try {
|
||||||
|
image = await getImageFromNetwork(imageKey, chunkEvents);
|
||||||
|
} catch (e) {
|
||||||
|
if (gotImageKey) {
|
||||||
|
rethrow;
|
||||||
|
} else {
|
||||||
|
imageKey = await getImageKey();
|
||||||
|
image = await getImageFromNetwork(imageKey, chunkEvents);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await writeToCache(image);
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> writeToCache(Uint8List image) async {
|
||||||
|
var fileName = md5.convert(key.codeUnits).toString();
|
||||||
|
var file = File(FilePath.join(App.cachePath, 'image_favorites', fileName));
|
||||||
|
if (!file.existsSync()) {
|
||||||
|
file.createSync(recursive: true);
|
||||||
|
}
|
||||||
|
await file.writeAsBytes(image);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Uint8List?> readFromCache() async {
|
||||||
|
var fileName = md5.convert(key.codeUnits).toString();
|
||||||
|
var file = File(FilePath.join(App.cachePath, 'image_favorites', fileName));
|
||||||
|
if (!file.existsSync()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return await file.readAsBytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete a image favorite cache
|
||||||
|
static Future<void> deleteFromCache(ImageFavorite imageFavorite) async {
|
||||||
|
var fileName = md5.convert(imageFavorite.imageKey.codeUnits).toString();
|
||||||
|
var file = File(FilePath.join(App.cachePath, 'image_favorites', fileName));
|
||||||
|
if (file.existsSync()) {
|
||||||
|
await file.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Uint8List?> getImageFromLocal() async {
|
||||||
|
var localComic =
|
||||||
|
LocalManager().find(sourceKey, ComicType.fromKey(sourceKey));
|
||||||
|
if (localComic == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var epIndex = localComic.chapters?.keys.toList().indexOf(eid) ?? -1;
|
||||||
|
if (epIndex == -1 && localComic.hasChapters) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var images = await LocalManager().getImages(
|
||||||
|
sourceKey,
|
||||||
|
ComicType.fromKey(sourceKey),
|
||||||
|
epIndex,
|
||||||
|
);
|
||||||
|
var data = await File(images[page]).readAsBytes();
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Uint8List> getImageFromNetwork(
|
||||||
|
String imageKey, StreamController<ImageChunkEvent>? chunkEvents) async {
|
||||||
|
await for (var progress
|
||||||
|
in ImageDownloader.loadComicImage(imageKey, sourceKey, cid, eid)) {
|
||||||
|
if (chunkEvents != null) {
|
||||||
|
chunkEvents.add(ImageChunkEvent(
|
||||||
|
cumulativeBytesLoaded: progress.currentBytes,
|
||||||
|
expectedTotalBytes: progress.totalBytes,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if (progress.imageBytes != null) {
|
||||||
|
return progress.imageBytes!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw "Error: Empty response body.";
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> getImageKey() async {
|
||||||
|
String sourceKey = imageFavorite.sourceKey;
|
||||||
|
String cid = imageFavorite.id;
|
||||||
|
String eid = imageFavorite.eid;
|
||||||
|
var page = imageFavorite.page;
|
||||||
|
var comicSource = ComicSource.find(sourceKey);
|
||||||
|
if (comicSource == null) {
|
||||||
|
throw "Error: Comic source not found.";
|
||||||
|
}
|
||||||
|
var res = await comicSource.loadComicPages!(cid, eid);
|
||||||
|
return res.data[page - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<ImageFavoritesProvider> obtainKey(ImageConfiguration configuration) {
|
||||||
|
return SynchronousFuture(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get key =>
|
||||||
|
"ImageFavorites ${imageFavorite.imageKey}@${imageFavorite.sourceKey}@${imageFavorite.id}@${imageFavorite.eid}";
|
||||||
|
}
|
@@ -22,7 +22,7 @@ class LocalFavoriteImageProvider
|
|||||||
static void delete(String id, int intKey) {
|
static void delete(String id, int intKey) {
|
||||||
var fileName = (id + intKey.toString()).hashCode.toString();
|
var fileName = (id + intKey.toString()).hashCode.toString();
|
||||||
var file = File(FilePath.join(App.dataPath, 'favorite_cover', fileName));
|
var file = File(FilePath.join(App.dataPath, 'favorite_cover', fileName));
|
||||||
if(file.existsSync()) {
|
if (file.existsSync()) {
|
||||||
file.delete();
|
file.delete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -42,7 +42,7 @@ class LocalFavoriteImageProvider
|
|||||||
cumulativeBytesLoaded: progress.currentBytes,
|
cumulativeBytesLoaded: progress.currentBytes,
|
||||||
expectedTotalBytes: progress.totalBytes,
|
expectedTotalBytes: progress.totalBytes,
|
||||||
));
|
));
|
||||||
if(progress.imageBytes != null) {
|
if (progress.imageBytes != null) {
|
||||||
var data = progress.imageBytes!;
|
var data = progress.imageBytes!;
|
||||||
await file.writeAsBytes(data);
|
await file.writeAsBytes(data);
|
||||||
return data;
|
return data;
|
||||||
@@ -52,7 +52,8 @@ class LocalFavoriteImageProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<LocalFavoriteImageProvider> obtainKey(ImageConfiguration configuration) {
|
Future<LocalFavoriteImageProvider> obtainKey(
|
||||||
|
ImageConfiguration configuration) {
|
||||||
return SynchronousFuture(this);
|
return SynchronousFuture(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -36,6 +36,8 @@ class LocalComic with HistoryMixin implements Comic {
|
|||||||
/// chapter id is the name of the directory in `LocalManager.path/$directory`
|
/// chapter id is the name of the directory in `LocalManager.path/$directory`
|
||||||
final Map<String, String>? chapters;
|
final Map<String, String>? chapters;
|
||||||
|
|
||||||
|
bool get hasChapters => chapters != null;
|
||||||
|
|
||||||
/// relative path to the cover image
|
/// relative path to the cover image
|
||||||
@override
|
@override
|
||||||
final String cover;
|
final String cover;
|
||||||
@@ -119,6 +121,8 @@ class LocalComic with HistoryMixin implements Comic {
|
|||||||
ep: 0,
|
ep: 0,
|
||||||
page: 0,
|
page: 0,
|
||||||
),
|
),
|
||||||
|
author: subtitle,
|
||||||
|
tags: tags,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -266,7 +270,7 @@ class LocalManager with ChangeNotifier {
|
|||||||
String findValidId(ComicType type) {
|
String findValidId(ComicType type) {
|
||||||
final res = _db.select(
|
final res = _db.select(
|
||||||
'''
|
'''
|
||||||
SELECT id FROM comics WHERE comic_type = ?
|
SELECT id FROM comics WHERE comic_type = ?
|
||||||
ORDER BY CAST(id AS INTEGER) DESC
|
ORDER BY CAST(id AS INTEGER) DESC
|
||||||
LIMIT 1;
|
LIMIT 1;
|
||||||
''',
|
''',
|
||||||
@@ -318,8 +322,8 @@ class LocalManager with ChangeNotifier {
|
|||||||
List<LocalComic> getComics(LocalSortType sortType) {
|
List<LocalComic> getComics(LocalSortType sortType) {
|
||||||
var res = _db.select('''
|
var res = _db.select('''
|
||||||
SELECT * FROM comics
|
SELECT * FROM comics
|
||||||
ORDER BY
|
ORDER BY
|
||||||
${sortType.value == 'name' ? 'title' : 'created_at'}
|
${sortType.value == 'name' ? 'title' : 'created_at'}
|
||||||
${sortType.value == 'time_asc' ? 'ASC' : 'DESC'}
|
${sortType.value == 'time_asc' ? 'ASC' : 'DESC'}
|
||||||
;
|
;
|
||||||
''');
|
''');
|
||||||
@@ -361,7 +365,7 @@ class LocalManager with ChangeNotifier {
|
|||||||
|
|
||||||
LocalComic? findByName(String name) {
|
LocalComic? findByName(String name) {
|
||||||
final res = _db.select('''
|
final res = _db.select('''
|
||||||
SELECT * FROM comics
|
SELECT * FROM comics
|
||||||
WHERE title = ? OR directory = ?;
|
WHERE title = ? OR directory = ?;
|
||||||
''', [name, name]);
|
''', [name, name]);
|
||||||
if (res.isEmpty) {
|
if (res.isEmpty) {
|
||||||
@@ -385,7 +389,7 @@ class LocalManager with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
var comic = find(id, type) ?? (throw "Comic Not Found");
|
var comic = find(id, type) ?? (throw "Comic Not Found");
|
||||||
var directory = Directory(comic.baseDir);
|
var directory = Directory(comic.baseDir);
|
||||||
if (comic.chapters != null) {
|
if (comic.hasChapters) {
|
||||||
var cid =
|
var cid =
|
||||||
ep is int ? comic.chapters!.keys.elementAt(ep - 1) : (ep as String);
|
ep is int ? comic.chapters!.keys.elementAt(ep - 1) : (ep as String);
|
||||||
directory = Directory(FilePath.join(directory.path, cid));
|
directory = Directory(FilePath.join(directory.path, cid));
|
||||||
|
@@ -145,6 +145,8 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
ep: 0,
|
ep: 0,
|
||||||
page: 0,
|
page: 0,
|
||||||
),
|
),
|
||||||
|
author: localComic.subTitle ?? '',
|
||||||
|
tags: localComic.tags,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
App.mainNavigatorKey!.currentContext!.pop();
|
App.mainNavigatorKey!.currentContext!.pop();
|
||||||
@@ -663,6 +665,8 @@ abstract mixin class _ComicPageActions {
|
|||||||
initialChapter: ep,
|
initialChapter: ep,
|
||||||
initialPage: page,
|
initialPage: page,
|
||||||
history: History.fromModel(model: comic, ep: 0, page: 0),
|
history: History.fromModel(model: comic, ep: 0, page: 0),
|
||||||
|
author: comic.findAuthor() ?? '',
|
||||||
|
tags: comic.plainTags,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -147,13 +147,13 @@ Future<List<FavoriteItem>> updateComicsInfo(String folder) async {
|
|||||||
var newInfo = (await comicSource.loadComicInfo!(c.id)).data;
|
var newInfo = (await comicSource.loadComicInfo!(c.id)).data;
|
||||||
|
|
||||||
var newTags = <String>[];
|
var newTags = <String>[];
|
||||||
for(var entry in newInfo.tags.entries) {
|
for (var entry in newInfo.tags.entries) {
|
||||||
const shouldIgnore = ['author', 'artist', 'time'];
|
const shouldIgnore = ['author', 'artist', 'time'];
|
||||||
var namespace = entry.key;
|
var namespace = entry.key;
|
||||||
if (shouldIgnore.contains(namespace.toLowerCase())) {
|
if (shouldIgnore.contains(namespace.toLowerCase())) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for(var tag in entry.value) {
|
for (var tag in entry.value) {
|
||||||
newTags.add("$namespace:$tag");
|
newTags.add("$namespace:$tag");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -305,6 +305,7 @@ Future<void> sortFolders() async {
|
|||||||
|
|
||||||
Future<void> importNetworkFolder(
|
Future<void> importNetworkFolder(
|
||||||
String source,
|
String source,
|
||||||
|
int updatePageNum,
|
||||||
String? folder,
|
String? folder,
|
||||||
String? folderID,
|
String? folderID,
|
||||||
) async {
|
) async {
|
||||||
@@ -312,7 +313,7 @@ Future<void> importNetworkFolder(
|
|||||||
if (comicSource == null) {
|
if (comicSource == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(folder != null && folder.isEmpty) {
|
if (folder != null && folder.isEmpty) {
|
||||||
folder = null;
|
folder = null;
|
||||||
}
|
}
|
||||||
var resultName = folder ?? comicSource.name;
|
var resultName = folder ?? comicSource.name;
|
||||||
@@ -324,7 +325,7 @@ Future<void> importNetworkFolder(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(!exists) {
|
if (!exists) {
|
||||||
LocalFavoritesManager().createFolder(resultName);
|
LocalFavoritesManager().createFolder(resultName);
|
||||||
LocalFavoritesManager().linkFolderToNetwork(
|
LocalFavoritesManager().linkFolderToNetwork(
|
||||||
resultName,
|
resultName,
|
||||||
@@ -332,37 +333,46 @@ Future<void> importNetworkFolder(
|
|||||||
folderID ?? "",
|
folderID ?? "",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
bool isOldToNewSort = comicSource.favoriteData?.isOldToNewSort ?? false;
|
||||||
var current = 0;
|
var current = 0;
|
||||||
|
int receivedComics = 0;
|
||||||
|
int requestCount = 0;
|
||||||
var isFinished = false;
|
var isFinished = false;
|
||||||
|
int maxPage = 1;
|
||||||
|
List<FavoriteItem> comics = [];
|
||||||
String? next;
|
String? next;
|
||||||
|
// 如果是从旧到新, 先取一下maxPage
|
||||||
|
if (isOldToNewSort) {
|
||||||
|
var res = await comicSource.favoriteData?.loadComic!(1, folderID);
|
||||||
|
maxPage = res?.subData ?? 1;
|
||||||
|
}
|
||||||
Future<void> fetchNext() async {
|
Future<void> fetchNext() async {
|
||||||
var retry = 3;
|
var retry = 3;
|
||||||
|
while (updatePageNum > requestCount && !isFinished) {
|
||||||
while (true) {
|
|
||||||
try {
|
try {
|
||||||
if (comicSource.favoriteData?.loadComic != null) {
|
if (comicSource.favoriteData?.loadComic != null) {
|
||||||
next ??= '1';
|
// 从旧到新的情况下, 假设有10页, 更新3页, 则从第8页开始, 8, 9, 10 三页
|
||||||
|
next ??=
|
||||||
|
isOldToNewSort ? (maxPage - updatePageNum + 1).toString() : '1';
|
||||||
var page = int.parse(next!);
|
var page = int.parse(next!);
|
||||||
var res = await comicSource.favoriteData!.loadComic!(page, folderID);
|
var res = await comicSource.favoriteData!.loadComic!(page, folderID);
|
||||||
var count = 0;
|
var count = 0;
|
||||||
|
receivedComics += res.data.length;
|
||||||
for (var c in res.data) {
|
for (var c in res.data) {
|
||||||
var result = LocalFavoritesManager().addComic(
|
if (!LocalFavoritesManager()
|
||||||
resultName,
|
.comicExists(resultName, c.id, ComicType(source.hashCode))) {
|
||||||
FavoriteItem(
|
count++;
|
||||||
|
comics.add(FavoriteItem(
|
||||||
id: c.id,
|
id: c.id,
|
||||||
name: c.title,
|
name: c.title,
|
||||||
coverPath: c.cover,
|
coverPath: c.cover,
|
||||||
type: ComicType(source.hashCode),
|
type: ComicType(source.hashCode),
|
||||||
author: c.subtitle ?? '',
|
author: c.subtitle ?? '',
|
||||||
tags: c.tags ?? [],
|
tags: c.tags ?? [],
|
||||||
),
|
));
|
||||||
);
|
|
||||||
if (result) {
|
|
||||||
count++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
requestCount++;
|
||||||
current += count;
|
current += count;
|
||||||
if (res.data.isEmpty || res.subData == page) {
|
if (res.data.isEmpty || res.subData == page) {
|
||||||
isFinished = true;
|
isFinished = true;
|
||||||
@@ -373,22 +383,22 @@ Future<void> importNetworkFolder(
|
|||||||
} else if (comicSource.favoriteData?.loadNext != null) {
|
} else if (comicSource.favoriteData?.loadNext != null) {
|
||||||
var res = await comicSource.favoriteData!.loadNext!(next, folderID);
|
var res = await comicSource.favoriteData!.loadNext!(next, folderID);
|
||||||
var count = 0;
|
var count = 0;
|
||||||
|
receivedComics += res.data.length;
|
||||||
for (var c in res.data) {
|
for (var c in res.data) {
|
||||||
var result = LocalFavoritesManager().addComic(
|
if (!LocalFavoritesManager()
|
||||||
resultName,
|
.comicExists(resultName, c.id, ComicType(source.hashCode))) {
|
||||||
FavoriteItem(
|
count++;
|
||||||
|
comics.add(FavoriteItem(
|
||||||
id: c.id,
|
id: c.id,
|
||||||
name: c.title,
|
name: c.title,
|
||||||
coverPath: c.cover,
|
coverPath: c.cover,
|
||||||
type: ComicType(source.hashCode),
|
type: ComicType(source.hashCode),
|
||||||
author: c.subtitle ?? '',
|
author: c.subtitle ?? '',
|
||||||
tags: c.tags ?? [],
|
tags: c.tags ?? [],
|
||||||
),
|
));
|
||||||
);
|
|
||||||
if (result) {
|
|
||||||
count++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
requestCount++;
|
||||||
current += count;
|
current += count;
|
||||||
if (res.data.isEmpty || res.subData == null) {
|
if (res.data.isEmpty || res.subData == null) {
|
||||||
isFinished = true;
|
isFinished = true;
|
||||||
@@ -408,6 +418,8 @@ Future<void> importNetworkFolder(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 跳出循环, 表示已经完成, 强制为 true, 避免死循环
|
||||||
|
isFinished = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isCanceled = false;
|
bool isCanceled = false;
|
||||||
@@ -415,6 +427,7 @@ Future<void> importNetworkFolder(
|
|||||||
bool isErrored() => errorMsg != null;
|
bool isErrored() => errorMsg != null;
|
||||||
|
|
||||||
void Function()? updateDialog;
|
void Function()? updateDialog;
|
||||||
|
void Function()? closeDialog;
|
||||||
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: App.rootContext,
|
context: App.rootContext,
|
||||||
@@ -422,6 +435,7 @@ Future<void> importNetworkFolder(
|
|||||||
return StatefulBuilder(
|
return StatefulBuilder(
|
||||||
builder: (context, setState) {
|
builder: (context, setState) {
|
||||||
updateDialog = () => setState(() {});
|
updateDialog = () => setState(() {});
|
||||||
|
closeDialog = () => Navigator.pop(context);
|
||||||
return ContentDialog(
|
return ContentDialog(
|
||||||
title: isFinished
|
title: isFinished
|
||||||
? "Finished".tl
|
? "Finished".tl
|
||||||
@@ -437,8 +451,11 @@ Future<void> importNetworkFolder(
|
|||||||
value: isFinished ? 1 : null,
|
value: isFinished ? 1 : null,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text("Imported @c comics".tlParams({
|
Text("Imported @a comics, loaded @b pages, received @c comics"
|
||||||
"c": current,
|
.tlParams({
|
||||||
|
"a": current,
|
||||||
|
"b": requestCount,
|
||||||
|
"c": receivedComics,
|
||||||
})),
|
})),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
if (isErrored()) Text("Error: $errorMsg"),
|
if (isErrored()) Text("Error: $errorMsg"),
|
||||||
@@ -476,4 +493,18 @@ Future<void> importNetworkFolder(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
if (appdata.settings['newFavoriteAddTo'] == "start" && !isOldToNewSort) {
|
||||||
|
// 如果是插到最前, 并且是从新到旧, 反转一下
|
||||||
|
comics = comics.reversed.toList();
|
||||||
|
}
|
||||||
|
for (var c in comics) {
|
||||||
|
LocalFavoritesManager().addComic(resultName, c);
|
||||||
|
}
|
||||||
|
// 延迟一点, 让用户看清楚到底新增了多少
|
||||||
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
|
closeDialog?.call();
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
Log.error("Unhandled Exception", e.toString(), stackTrace);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -11,6 +11,7 @@ import 'package:venera/foundation/comic_type.dart';
|
|||||||
import 'package:venera/foundation/consts.dart';
|
import 'package:venera/foundation/consts.dart';
|
||||||
import 'package:venera/foundation/favorites.dart';
|
import 'package:venera/foundation/favorites.dart';
|
||||||
import 'package:venera/foundation/local.dart';
|
import 'package:venera/foundation/local.dart';
|
||||||
|
import 'package:venera/foundation/log.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/comic_page.dart';
|
import 'package:venera/pages/comic_page.dart';
|
||||||
@@ -35,7 +36,7 @@ class FavoritesPage extends StatefulWidget {
|
|||||||
State<FavoritesPage> createState() => _FavoritesPageState();
|
State<FavoritesPage> createState() => _FavoritesPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FavoritesPageState extends State<FavoritesPage> {
|
class _FavoritesPageState extends State<FavoritesPage> {
|
||||||
String? folder;
|
String? folder;
|
||||||
|
|
||||||
bool isNetwork = false;
|
bool isNetwork = false;
|
||||||
@@ -58,7 +59,7 @@ class _FavoritesPageState extends State<FavoritesPage> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
var data = appdata.implicitData['favoriteFolder'];
|
var data = appdata.implicitData['favoriteFolder'];
|
||||||
if(data != null){
|
if (data != null) {
|
||||||
folder = data['name'];
|
folder = data['name'];
|
||||||
isNetwork = data['isNetwork'] ?? false;
|
isNetwork = data['isNetwork'] ?? false;
|
||||||
}
|
}
|
||||||
@@ -101,7 +102,7 @@ class _FavoritesPageState extends State<FavoritesPage> {
|
|||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: Material(
|
child: Material(
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: min(300, context.width-16),
|
width: min(300, context.width - 16),
|
||||||
child: _LeftBar(
|
child: _LeftBar(
|
||||||
withAppbar: true,
|
withAppbar: true,
|
||||||
favPage: this,
|
favPage: this,
|
||||||
@@ -153,14 +154,16 @@ class _FavoritesPageState extends State<FavoritesPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!isNetwork) {
|
if (!isNetwork) {
|
||||||
return _LocalFavoritesPage(folder: folder!, key: PageStorageKey("local_$folder"));
|
return _LocalFavoritesPage(
|
||||||
|
folder: folder!, key: PageStorageKey("local_$folder"));
|
||||||
} else {
|
} else {
|
||||||
var favoriteData = getFavoriteDataOrNull(folder!);
|
var favoriteData = getFavoriteDataOrNull(folder!);
|
||||||
if (favoriteData == null) {
|
if (favoriteData == null) {
|
||||||
folder = null;
|
folder = null;
|
||||||
return buildBody();
|
return buildBody();
|
||||||
} else {
|
} else {
|
||||||
return NetworkFavoritePage(favoriteData, key: PageStorageKey("network_$folder"));
|
return NetworkFavoritePage(favoriteData,
|
||||||
|
key: PageStorageKey("network_$folder"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -136,17 +136,17 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
|||||||
message: "Sync".tl,
|
message: "Sync".tl,
|
||||||
child: Flyout(
|
child: Flyout(
|
||||||
flyoutBuilder: (context) {
|
flyoutBuilder: (context) {
|
||||||
var sourceName = ComicSource.find(networkSource!)?.name ??
|
final GlobalKey<_SelectUpdatePageNumState>
|
||||||
networkSource!;
|
selectUpdatePageNumKey =
|
||||||
var text = "The folder is Linked to @source".tlParams({
|
GlobalKey<_SelectUpdatePageNumState>();
|
||||||
"source": sourceName,
|
var updatePageWidget = _SelectUpdatePageNum(
|
||||||
});
|
networkSource: networkSource!,
|
||||||
if (networkFolder != null && networkFolder!.isNotEmpty) {
|
networkFolder: networkFolder,
|
||||||
text += "\n${"Source Folder".tl}: $networkFolder";
|
key: selectUpdatePageNumKey,
|
||||||
}
|
);
|
||||||
return FlyoutContent(
|
return FlyoutContent(
|
||||||
title: "Sync".tl,
|
title: "Sync".tl,
|
||||||
content: Text(text),
|
content: updatePageWidget,
|
||||||
actions: [
|
actions: [
|
||||||
Button.filled(
|
Button.filled(
|
||||||
child: Text("Update".tl),
|
child: Text("Update".tl),
|
||||||
@@ -154,6 +154,8 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
|||||||
context.pop();
|
context.pop();
|
||||||
importNetworkFolder(
|
importNetworkFolder(
|
||||||
networkSource!,
|
networkSource!,
|
||||||
|
selectUpdatePageNumKey
|
||||||
|
.currentState!.updatePageNum,
|
||||||
widget.folder,
|
widget.folder,
|
||||||
networkFolder!,
|
networkFolder!,
|
||||||
).then(
|
).then(
|
||||||
@@ -741,6 +743,17 @@ class _ReorderComicsPageState extends State<_ReorderComicsPage> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.swap_vert),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
comics = comics.reversed.toList();
|
||||||
|
changed = true;
|
||||||
|
showToast(
|
||||||
|
message: "Reversed successfully".tl, context: context);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: ReorderableBuilder<FavoriteItem>(
|
body: ReorderableBuilder<FavoriteItem>(
|
||||||
@@ -776,3 +789,76 @@ class _ReorderComicsPageState extends State<_ReorderComicsPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _SelectUpdatePageNum extends StatefulWidget {
|
||||||
|
const _SelectUpdatePageNum({
|
||||||
|
required this.networkSource,
|
||||||
|
this.networkFolder,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String? networkFolder;
|
||||||
|
final String networkSource;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_SelectUpdatePageNum> createState() => _SelectUpdatePageNumState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SelectUpdatePageNumState extends State<_SelectUpdatePageNum> {
|
||||||
|
int updatePageNum = 9999999;
|
||||||
|
|
||||||
|
String get _allPageText => 'All'.tl;
|
||||||
|
|
||||||
|
List<String> get pageNumList =>
|
||||||
|
['1', '2', '3', '5', '10', '20', '50', '100', '200', _allPageText];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
updatePageNum =
|
||||||
|
appdata.implicitData["local_favorites_update_page_num"] ?? 9999999;
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var source = ComicSource.find(widget.networkSource);
|
||||||
|
var sourceName = source?.name ?? widget.networkSource;
|
||||||
|
var text = "The folder is Linked to @source".tlParams({
|
||||||
|
"source": sourceName,
|
||||||
|
});
|
||||||
|
if (widget.networkFolder != null && widget.networkFolder!.isNotEmpty) {
|
||||||
|
text += "\n${"Source Folder".tl}: ${widget.networkFolder}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [Text(text)],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text("Update the page number by the latest collection".tl),
|
||||||
|
Spacer(),
|
||||||
|
Select(
|
||||||
|
current: updatePageNum.toString() == '9999999'
|
||||||
|
? _allPageText
|
||||||
|
: updatePageNum.toString(),
|
||||||
|
values: pageNumList,
|
||||||
|
minWidth: 48,
|
||||||
|
onTap: (index) {
|
||||||
|
setState(() {
|
||||||
|
updatePageNum = int.parse(pageNumList[index] == _allPageText
|
||||||
|
? '9999999'
|
||||||
|
: pageNumList[index]);
|
||||||
|
appdata.implicitData["local_favorites_update_page_num"] =
|
||||||
|
updatePageNum;
|
||||||
|
appdata.writeImplicitData();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -20,8 +20,7 @@ Future<bool> _deleteComic(
|
|||||||
return StatefulBuilder(builder: (context, setState) {
|
return StatefulBuilder(builder: (context, setState) {
|
||||||
return ContentDialog(
|
return ContentDialog(
|
||||||
title: "Remove".tl,
|
title: "Remove".tl,
|
||||||
content: Text("Remove comic from favorite?".tl)
|
content: Text("Remove comic from favorite?".tl).paddingHorizontal(16),
|
||||||
.paddingHorizontal(16),
|
|
||||||
actions: [
|
actions: [
|
||||||
Button.filled(
|
Button.filled(
|
||||||
isLoading: loading,
|
isLoading: loading,
|
||||||
@@ -94,9 +93,8 @@ class _NormalFavoritePageState extends State<_NormalFavoritePage> {
|
|||||||
return ComicList(
|
return ComicList(
|
||||||
key: comicListKey,
|
key: comicListKey,
|
||||||
leadingSliver: SliverAppbar(
|
leadingSliver: SliverAppbar(
|
||||||
style: context.width < changePoint
|
style:
|
||||||
? AppbarStyle.shadow
|
context.width < changePoint ? AppbarStyle.shadow : AppbarStyle.blur,
|
||||||
: AppbarStyle.blur,
|
|
||||||
leading: Tooltip(
|
leading: Tooltip(
|
||||||
message: "Folders".tl,
|
message: "Folders".tl,
|
||||||
child: context.width <= _kTwoPanelChangeWidth
|
child: context.width <= _kTwoPanelChangeWidth
|
||||||
@@ -117,7 +115,7 @@ class _NormalFavoritePageState extends State<_NormalFavoritePage> {
|
|||||||
icon: Icons.sync,
|
icon: Icons.sync,
|
||||||
text: "Convert to local".tl,
|
text: "Convert to local".tl,
|
||||||
onClick: () {
|
onClick: () {
|
||||||
importNetworkFolder(widget.data.key, null, null);
|
importNetworkFolder(widget.data.key, 9999999, null, null);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
]),
|
]),
|
||||||
@@ -215,9 +213,8 @@ class _MultiFolderFavoritesPageState extends State<_MultiFolderFavoritesPage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var sliverAppBar = SliverAppbar(
|
var sliverAppBar = SliverAppbar(
|
||||||
style: context.width < changePoint
|
style:
|
||||||
? AppbarStyle.shadow
|
context.width < changePoint ? AppbarStyle.shadow : AppbarStyle.blur,
|
||||||
: AppbarStyle.blur,
|
|
||||||
leading: Tooltip(
|
leading: Tooltip(
|
||||||
message: "Folders".tl,
|
message: "Folders".tl,
|
||||||
child: context.width <= _kTwoPanelChangeWidth
|
child: context.width <= _kTwoPanelChangeWidth
|
||||||
@@ -431,8 +428,7 @@ class _FolderTile extends StatelessWidget {
|
|||||||
return StatefulBuilder(builder: (context, setState) {
|
return StatefulBuilder(builder: (context, setState) {
|
||||||
return ContentDialog(
|
return ContentDialog(
|
||||||
title: "Delete".tl,
|
title: "Delete".tl,
|
||||||
content: Text("Delete folder?".tl)
|
content: Text("Delete folder?".tl).paddingHorizontal(16),
|
||||||
.paddingHorizontal(16),
|
|
||||||
actions: [
|
actions: [
|
||||||
Button.filled(
|
Button.filled(
|
||||||
isLoading: loading,
|
isLoading: loading,
|
||||||
@@ -558,7 +554,7 @@ class _FavoriteFolder extends StatelessWidget {
|
|||||||
icon: Icons.sync,
|
icon: Icons.sync,
|
||||||
text: "Convert to local".tl,
|
text: "Convert to local".tl,
|
||||||
onClick: () {
|
onClick: () {
|
||||||
importNetworkFolder(data.key, title, folderID);
|
importNetworkFolder(data.key, 9999999, title, folderID);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
]),
|
]),
|
||||||
|
@@ -9,14 +9,17 @@ import 'package:venera/foundation/history.dart';
|
|||||||
import 'package:venera/foundation/image_provider/history_image_provider.dart';
|
import 'package:venera/foundation/image_provider/history_image_provider.dart';
|
||||||
import 'package:venera/foundation/image_provider/local_comic_image.dart';
|
import 'package:venera/foundation/image_provider/local_comic_image.dart';
|
||||||
import 'package:venera/foundation/local.dart';
|
import 'package:venera/foundation/local.dart';
|
||||||
|
import 'package:venera/foundation/log.dart';
|
||||||
import 'package:venera/pages/accounts_page.dart';
|
import 'package:venera/pages/accounts_page.dart';
|
||||||
import 'package:venera/pages/comic_page.dart';
|
import 'package:venera/pages/comic_page.dart';
|
||||||
import 'package:venera/pages/comic_source_page.dart';
|
import 'package:venera/pages/comic_source_page.dart';
|
||||||
import 'package:venera/pages/downloading_page.dart';
|
import 'package:venera/pages/downloading_page.dart';
|
||||||
import 'package:venera/pages/history_page.dart';
|
import 'package:venera/pages/history_page.dart';
|
||||||
|
import 'package:venera/pages/image_favorites_page/image_favorites_page.dart';
|
||||||
import 'package:venera/pages/search_page.dart';
|
import 'package:venera/pages/search_page.dart';
|
||||||
import 'package:venera/utils/data_sync.dart';
|
import 'package:venera/utils/data_sync.dart';
|
||||||
import 'package:venera/utils/import_comic.dart';
|
import 'package:venera/utils/import_comic.dart';
|
||||||
|
import 'package:venera/utils/tags_translation.dart';
|
||||||
import 'package:venera/utils/translations.dart';
|
import 'package:venera/utils/translations.dart';
|
||||||
|
|
||||||
import 'local_comics_page.dart';
|
import 'local_comics_page.dart';
|
||||||
@@ -35,6 +38,7 @@ class HomePage extends StatelessWidget {
|
|||||||
const _Local(),
|
const _Local(),
|
||||||
const _ComicSourceWidget(),
|
const _ComicSourceWidget(),
|
||||||
const _AccountsWidget(),
|
const _AccountsWidget(),
|
||||||
|
const ImageFavorites(),
|
||||||
SliverPadding(padding: EdgeInsets.only(top: context.padding.bottom)),
|
SliverPadding(padding: EdgeInsets.only(top: context.padding.bottom)),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -83,7 +87,8 @@ class _SyncDataWidget extends StatefulWidget {
|
|||||||
State<_SyncDataWidget> createState() => _SyncDataWidgetState();
|
State<_SyncDataWidget> createState() => _SyncDataWidgetState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SyncDataWidgetState extends State<_SyncDataWidget> with WidgetsBindingObserver {
|
class _SyncDataWidgetState extends State<_SyncDataWidget>
|
||||||
|
with WidgetsBindingObserver {
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -93,7 +98,7 @@ class _SyncDataWidgetState extends State<_SyncDataWidget> with WidgetsBindingObs
|
|||||||
}
|
}
|
||||||
|
|
||||||
void update() {
|
void update() {
|
||||||
if(mounted) {
|
if (mounted) {
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -110,8 +115,8 @@ class _SyncDataWidgetState extends State<_SyncDataWidget> with WidgetsBindingObs
|
|||||||
@override
|
@override
|
||||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
super.didChangeAppLifecycleState(state);
|
super.didChangeAppLifecycleState(state);
|
||||||
if(state == AppLifecycleState.resumed) {
|
if (state == AppLifecycleState.resumed) {
|
||||||
if(DateTime.now().difference(lastCheck) > const Duration(minutes: 10)) {
|
if (DateTime.now().difference(lastCheck) > const Duration(minutes: 10)) {
|
||||||
lastCheck = DateTime.now();
|
lastCheck = DateTime.now();
|
||||||
DataSync().downloadData();
|
DataSync().downloadData();
|
||||||
}
|
}
|
||||||
@@ -121,7 +126,7 @@ class _SyncDataWidgetState extends State<_SyncDataWidget> with WidgetsBindingObs
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Widget child;
|
Widget child;
|
||||||
if(!DataSync().isEnabled) {
|
if (!DataSync().isEnabled) {
|
||||||
child = const SliverPadding(padding: EdgeInsets.zero);
|
child = const SliverPadding(padding: EdgeInsets.zero);
|
||||||
} else if (DataSync().isUploading || DataSync().isDownloading) {
|
} else if (DataSync().isUploading || DataSync().isDownloading) {
|
||||||
child = SliverToBoxAdapter(
|
child = SliverToBoxAdapter(
|
||||||
@@ -159,17 +164,15 @@ class _SyncDataWidgetState extends State<_SyncDataWidget> with WidgetsBindingObs
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.cloud_upload_outlined),
|
icon: const Icon(Icons.cloud_upload_outlined),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
DataSync().uploadData();
|
DataSync().uploadData();
|
||||||
}
|
}),
|
||||||
),
|
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.cloud_download_outlined),
|
icon: const Icon(Icons.cloud_download_outlined),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
DataSync().downloadData();
|
DataSync().downloadData();
|
||||||
}
|
}),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -518,50 +521,50 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
: Column(
|
: Column(
|
||||||
key: key,
|
key: key,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(width: 600),
|
const SizedBox(width: 600),
|
||||||
...List.generate(importMethods.length, (index) {
|
...List.generate(importMethods.length, (index) {
|
||||||
return RadioListTile(
|
return RadioListTile(
|
||||||
title: Text(importMethods[index]),
|
title: Text(importMethods[index]),
|
||||||
value: index,
|
value: index,
|
||||||
groupValue: type,
|
groupValue: type,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
|
||||||
type = value as int;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
if(type != 3)
|
|
||||||
ListTile(
|
|
||||||
title: Text("Add to favorites".tl),
|
|
||||||
trailing: Select(
|
|
||||||
current: selectedFolder,
|
|
||||||
values: folders,
|
|
||||||
minWidth: 112,
|
|
||||||
onTap: (v) {
|
|
||||||
setState(() {
|
setState(() {
|
||||||
selectedFolder = folders[v];
|
type = value as int;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
);
|
||||||
).paddingHorizontal(8),
|
}),
|
||||||
if(!App.isIOS && !App.isMacOS)
|
if (type != 3)
|
||||||
CheckboxListTile(
|
ListTile(
|
||||||
enabled: true,
|
title: Text("Add to favorites".tl),
|
||||||
title: Text("Copy to app local path".tl),
|
trailing: Select(
|
||||||
value: copyToLocalFolder,
|
current: selectedFolder,
|
||||||
onChanged:(v) {
|
values: folders,
|
||||||
setState(() {
|
minWidth: 112,
|
||||||
copyToLocalFolder = !copyToLocalFolder;
|
onTap: (v) {
|
||||||
});
|
setState(() {
|
||||||
}).paddingHorizontal(8),
|
selectedFolder = folders[v];
|
||||||
const SizedBox(height: 8),
|
});
|
||||||
Text(info).paddingHorizontal(24),
|
},
|
||||||
],
|
),
|
||||||
),
|
).paddingHorizontal(8),
|
||||||
|
if (!App.isIOS && !App.isMacOS)
|
||||||
|
CheckboxListTile(
|
||||||
|
enabled: true,
|
||||||
|
title: Text("Copy to app local path".tl),
|
||||||
|
value: copyToLocalFolder,
|
||||||
|
onChanged: (v) {
|
||||||
|
setState(() {
|
||||||
|
copyToLocalFolder = !copyToLocalFolder;
|
||||||
|
});
|
||||||
|
}).paddingHorizontal(8),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(info).paddingHorizontal(24),
|
||||||
|
],
|
||||||
|
),
|
||||||
actions: [
|
actions: [
|
||||||
Button.text(
|
Button.text(
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -591,7 +594,9 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> {
|
|||||||
help +=
|
help +=
|
||||||
"The directory name will be used as the comic title. And the name of chapter directories will be used as the chapter titles.\n"
|
"The directory name will be used as the comic title. And the name of chapter directories will be used as the chapter titles.\n"
|
||||||
.tl;
|
.tl;
|
||||||
help +="If you import an EhViewer's database, program will automatically create folders according to the download label in that database.".tl;
|
help +=
|
||||||
|
"If you import an EhViewer's database, program will automatically create folders according to the download label in that database."
|
||||||
|
.tl;
|
||||||
return ContentDialog(
|
return ContentDialog(
|
||||||
title: "Help".tl,
|
title: "Help".tl,
|
||||||
content: Text(help).paddingHorizontal(16),
|
content: Text(help).paddingHorizontal(16),
|
||||||
@@ -624,9 +629,8 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> {
|
|||||||
loading = true;
|
loading = true;
|
||||||
});
|
});
|
||||||
var importer = ImportComic(
|
var importer = ImportComic(
|
||||||
selectedFolder: selectedFolder,
|
selectedFolder: selectedFolder, copyToLocal: copyToLocalFolder);
|
||||||
copyToLocal: copyToLocalFolder);
|
var result = switch (type) {
|
||||||
var result = switch(type) {
|
|
||||||
0 => await importer.directory(true),
|
0 => await importer.directory(true),
|
||||||
1 => await importer.directory(false),
|
1 => await importer.directory(false),
|
||||||
2 => await importer.cbz(),
|
2 => await importer.cbz(),
|
||||||
@@ -634,7 +638,7 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> {
|
|||||||
4 => await importer.ehViewer(),
|
4 => await importer.ehViewer(),
|
||||||
int() => true,
|
int() => true,
|
||||||
};
|
};
|
||||||
if(result) {
|
if (result) {
|
||||||
context.pop();
|
context.pop();
|
||||||
} else {
|
} else {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -911,3 +915,281 @@ class __AnimatedDownloadingIconState extends State<_AnimatedDownloadingIcon>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ImageFavorites extends StatefulWidget {
|
||||||
|
const ImageFavorites({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ImageFavorites> createState() => _ImageFavoritesState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ImageFavoritesState extends State<ImageFavorites> {
|
||||||
|
ImageFavoritesComputed? imageFavoritesCompute;
|
||||||
|
|
||||||
|
int displayType = 0;
|
||||||
|
|
||||||
|
void refreshImageFavorites() async {
|
||||||
|
try {
|
||||||
|
imageFavoritesCompute =
|
||||||
|
await ImageFavoriteManager.computeImageFavorites();
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
Log.error("Unhandled Exception", e.toString(), stackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
refreshImageFavorites();
|
||||||
|
ImageFavoriteManager().addListener(refreshImageFavorites);
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
ImageFavoriteManager().removeListener(refreshImageFavorites);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
bool hasData =
|
||||||
|
imageFavoritesCompute != null && !imageFavoritesCompute!.isEmpty;
|
||||||
|
return SliverToBoxAdapter(
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context).colorScheme.outlineVariant,
|
||||||
|
width: 0.6,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
onTap: () {
|
||||||
|
context.to(() => const ImageFavoritesPage());
|
||||||
|
},
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
height: 56,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Center(
|
||||||
|
child: Text('Image Favorites'.tl, style: ts.s18),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
const Icon(Icons.arrow_right),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
).paddingHorizontal(16),
|
||||||
|
if (hasData)
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Spacer(),
|
||||||
|
buildTypeButton(0, "Tags".tl),
|
||||||
|
const Spacer(),
|
||||||
|
buildTypeButton(1, "Authors".tl),
|
||||||
|
const Spacer(),
|
||||||
|
buildTypeButton(2, "Comics".tl),
|
||||||
|
const Spacer(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (hasData) const SizedBox(height: 8),
|
||||||
|
if (hasData)
|
||||||
|
buildChart(switch (displayType) {
|
||||||
|
0 => imageFavoritesCompute!.tags,
|
||||||
|
1 => imageFavoritesCompute!.authors,
|
||||||
|
2 => imageFavoritesCompute!.comics,
|
||||||
|
_ => [],
|
||||||
|
})
|
||||||
|
.paddingHorizontal(16)
|
||||||
|
.paddingBottom(16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildTypeButton(int type, String text) {
|
||||||
|
const radius = 24.0;
|
||||||
|
return InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(radius),
|
||||||
|
onTap: () async {
|
||||||
|
setState(() {
|
||||||
|
displayType = type;
|
||||||
|
});
|
||||||
|
await Future.delayed(const Duration(milliseconds: 20));
|
||||||
|
var scrollController = ScrollControllerProvider.of(context);
|
||||||
|
scrollController.animateTo(
|
||||||
|
scrollController.position.maxScrollExtent,
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
curve: Curves.ease,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: AnimatedContainer(
|
||||||
|
width: 96,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color:
|
||||||
|
displayType == type ? context.colorScheme.primaryContainer : null,
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context).colorScheme.outlineVariant,
|
||||||
|
width: 0.6,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(radius),
|
||||||
|
),
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
style: ts.s16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildChart(List<TextWithCount> data) {
|
||||||
|
if (data.isEmpty) {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
var maxCount = data.map((e) => e.count).reduce((a, b) => a > b ? a : b);
|
||||||
|
return ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxHeight: 164,
|
||||||
|
),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
key: ValueKey(displayType),
|
||||||
|
children: data.map((e) {
|
||||||
|
return _ChartLine(
|
||||||
|
text: e.text,
|
||||||
|
count: e.count,
|
||||||
|
maxCount: maxCount,
|
||||||
|
enableTranslation: displayType != 2,
|
||||||
|
onTap: (text) {
|
||||||
|
context.to(() => ImageFavoritesPage(initialKeyword: text));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChartLine extends StatefulWidget {
|
||||||
|
const _ChartLine({
|
||||||
|
required this.text,
|
||||||
|
required this.count,
|
||||||
|
required this.maxCount,
|
||||||
|
required this.enableTranslation,
|
||||||
|
this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String text;
|
||||||
|
|
||||||
|
final int count;
|
||||||
|
|
||||||
|
final int maxCount;
|
||||||
|
|
||||||
|
final bool enableTranslation;
|
||||||
|
|
||||||
|
final void Function(String text)? onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_ChartLine> createState() => __ChartLineState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class __ChartLineState extends State<_ChartLine>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late AnimationController _controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
value: 0,
|
||||||
|
)..forward();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var text = widget.text;
|
||||||
|
var enableTranslation =
|
||||||
|
App.locale.countryCode == 'CN' && widget.enableTranslation;
|
||||||
|
if (enableTranslation) {
|
||||||
|
text = text.translateTagsToCN;
|
||||||
|
}
|
||||||
|
if (widget.enableTranslation && text.contains(':')) {
|
||||||
|
text = text.split(':').last;
|
||||||
|
}
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
onTap: () {
|
||||||
|
widget.onTap?.call(widget.text);
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
)
|
||||||
|
.paddingHorizontal(4)
|
||||||
|
.toAlign(Alignment.centerLeft)
|
||||||
|
.fixWidth(context.width > 600 ? 120 : 80)
|
||||||
|
.fixHeight(double.infinity),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: LayoutBuilder(builder: (context, constrains) {
|
||||||
|
var width = constrains.maxWidth * widget.count / widget.maxCount;
|
||||||
|
return AnimatedBuilder(
|
||||||
|
animation: _controller,
|
||||||
|
builder: (context, child) {
|
||||||
|
return Container(
|
||||||
|
width: width * _controller.value,
|
||||||
|
height: 18,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(2),
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: context.isDarkMode
|
||||||
|
? [
|
||||||
|
Colors.blue.shade800,
|
||||||
|
Colors.blue.shade500,
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
Colors.blue.shade300,
|
||||||
|
Colors.blue.shade600,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).toAlign(Alignment.centerLeft);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
widget.count.toString(),
|
||||||
|
style: ts.s12,
|
||||||
|
).fixWidth(context.width > 600 ? 60 : 30),
|
||||||
|
],
|
||||||
|
).fixHeight(28);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
287
lib/pages/image_favorites_page/image_favorites_item.dart
Normal file
287
lib/pages/image_favorites_page/image_favorites_item.dart
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
part of 'image_favorites_page.dart';
|
||||||
|
|
||||||
|
class _ImageFavoritesItem extends StatefulWidget {
|
||||||
|
const _ImageFavoritesItem({
|
||||||
|
required this.imageFavoritesComic,
|
||||||
|
required this.selectedImageFavorites,
|
||||||
|
required this.addSelected,
|
||||||
|
required this.multiSelectMode,
|
||||||
|
required this.finalImageFavoritesComicList,
|
||||||
|
});
|
||||||
|
|
||||||
|
final ImageFavoritesComic imageFavoritesComic;
|
||||||
|
final Function(ImageFavorite) addSelected;
|
||||||
|
final Map<ImageFavorite, bool> selectedImageFavorites;
|
||||||
|
final List<ImageFavoritesComic> finalImageFavoritesComicList;
|
||||||
|
final bool multiSelectMode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_ImageFavoritesItem> createState() => _ImageFavoritesItemState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ImageFavoritesItemState extends State<_ImageFavoritesItem> {
|
||||||
|
late final imageFavorites = widget.imageFavoritesComic.images.toList();
|
||||||
|
|
||||||
|
void goComicInfo(ImageFavoritesComic comic) {
|
||||||
|
App.mainNavigatorKey?.currentContext?.to(() => ComicPage(
|
||||||
|
id: comic.id,
|
||||||
|
sourceKey: comic.sourceKey,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
void goReaderPage(ImageFavoritesComic comic, int ep, int page) {
|
||||||
|
App.rootContext.to(
|
||||||
|
() => ReaderWithLoading(
|
||||||
|
id: comic.id,
|
||||||
|
sourceKey: comic.sourceKey,
|
||||||
|
initialEp: ep,
|
||||||
|
initialPage: page,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void goPhotoView(ImageFavorite imageFavorite) {
|
||||||
|
Navigator.of(App.rootContext).push(MaterialPageRoute(
|
||||||
|
builder: (context) => ImageFavoritesPhotoView(
|
||||||
|
comic: widget.imageFavoritesComic,
|
||||||
|
imageFavorite: imageFavorite,
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void copyTitle() {
|
||||||
|
Clipboard.setData(ClipboardData(text: widget.imageFavoritesComic.title));
|
||||||
|
App.rootContext.showMessage(message: 'Copy the title successfully'.tl);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onLongPress() {
|
||||||
|
var renderBox = context.findRenderObject() as RenderBox;
|
||||||
|
var size = renderBox.size;
|
||||||
|
var location = renderBox.localToGlobal(
|
||||||
|
Offset((size.width - 242) / 2, size.height / 2),
|
||||||
|
);
|
||||||
|
showMenu(location, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onSecondaryTap(TapDownDetails details) {
|
||||||
|
showMenu(details.globalPosition, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
void showMenu(Offset location, BuildContext context) {
|
||||||
|
showMenuX(
|
||||||
|
App.rootContext,
|
||||||
|
location,
|
||||||
|
[
|
||||||
|
MenuEntry(
|
||||||
|
icon: Icons.chrome_reader_mode_outlined,
|
||||||
|
text: 'Details'.tl,
|
||||||
|
onClick: () {
|
||||||
|
goComicInfo(widget.imageFavoritesComic);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
MenuEntry(
|
||||||
|
icon: Icons.copy,
|
||||||
|
text: 'Copy Title'.tl,
|
||||||
|
onClick: () {
|
||||||
|
copyTitle();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
MenuEntry(
|
||||||
|
icon: Icons.select_all,
|
||||||
|
text: 'Select All'.tl,
|
||||||
|
onClick: () {
|
||||||
|
for (var ele in widget.imageFavoritesComic.images) {
|
||||||
|
widget.addSelected(ele);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
MenuEntry(
|
||||||
|
icon: Icons.read_more,
|
||||||
|
text: 'Photo View'.tl,
|
||||||
|
onClick: () {
|
||||||
|
goPhotoView(widget.imageFavoritesComic.images.first);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context).colorScheme.outlineVariant,
|
||||||
|
width: 0.6,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
onSecondaryTapDown: onSecondaryTap,
|
||||||
|
onLongPress: onLongPress,
|
||||||
|
onTap: () {
|
||||||
|
if (widget.multiSelectMode) {
|
||||||
|
for (var ele in widget.imageFavoritesComic.images) {
|
||||||
|
widget.addSelected(ele);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 单击跳转漫画详情
|
||||||
|
goComicInfo(widget.imageFavoritesComic);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
buildTop(),
|
||||||
|
SizedBox(
|
||||||
|
height: 145,
|
||||||
|
child: ListView.builder(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
itemBuilder: buildItem,
|
||||||
|
itemCount: imageFavorites.length,
|
||||||
|
),
|
||||||
|
).paddingHorizontal(8),
|
||||||
|
buildBottom(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildItem(BuildContext context, int index) {
|
||||||
|
var image = imageFavorites[index];
|
||||||
|
bool isSelected = widget.selectedImageFavorites[image] ?? false;
|
||||||
|
int curPage = image.page;
|
||||||
|
String pageText = curPage == firstPage
|
||||||
|
? '@a Cover'.tlParams({"a": image.epName})
|
||||||
|
: curPage.toString();
|
||||||
|
|
||||||
|
return InkWell(
|
||||||
|
onTap: () {
|
||||||
|
// 单击去阅读页面, 跳转到当前点击的page
|
||||||
|
if (widget.multiSelectMode) {
|
||||||
|
widget.addSelected(image);
|
||||||
|
} else {
|
||||||
|
goReaderPage(widget.imageFavoritesComic, image.ep, curPage);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLongPress: () {
|
||||||
|
goPhotoView(image);
|
||||||
|
},
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: Container(
|
||||||
|
width: 98,
|
||||||
|
height: 128,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
color: isSelected
|
||||||
|
? Theme.of(context).colorScheme.primaryContainer
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
height: 128,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
|
),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: Hero(
|
||||||
|
tag: "${image.sourceKey}${image.ep}${image.page}",
|
||||||
|
child: AnimatedImage(
|
||||||
|
image: ImageFavoritesProvider(image),
|
||||||
|
width: 96,
|
||||||
|
height: 128,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
filterQuality: FilterQuality.medium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
pageText,
|
||||||
|
style: ts.s10,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).paddingHorizontal(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildTop() {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
widget.imageFavoritesComic.title,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
fontSize: 16.0,
|
||||||
|
),
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
softWrap: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
"${imageFavorites.length}/${widget.imageFavoritesComic.maxPageFromEp}",
|
||||||
|
style: ts.s12),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).paddingHorizontal(16).paddingVertical(8);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildBottom() {
|
||||||
|
var enableTranslate = App.locale.languageCode == 'zh';
|
||||||
|
String time =
|
||||||
|
DateFormat('yyyy-MM-dd').format(widget.imageFavoritesComic.time);
|
||||||
|
List<String> tags = [];
|
||||||
|
for (var tag in widget.imageFavoritesComic.tags) {
|
||||||
|
var text = enableTranslate ? tag.translateTagsToCN : tag;
|
||||||
|
if (text.contains(':')) {
|
||||||
|
text = text.split(':').last;
|
||||||
|
}
|
||||||
|
tags.add(text);
|
||||||
|
if (tags.length == 5) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var comicSource = ComicSource.find(widget.imageFavoritesComic.sourceKey);
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"$time | ${comicSource?.name ?? "Unknown"}",
|
||||||
|
textAlign: TextAlign.left,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12.0,
|
||||||
|
),
|
||||||
|
).paddingRight(8),
|
||||||
|
if (tags.isNotEmpty)
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
tags
|
||||||
|
.map((e) => enableTranslate ? e.translateTagsToCN : e)
|
||||||
|
.join(" "),
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12.0,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
).paddingHorizontal(8).paddingBottom(8);
|
||||||
|
}
|
||||||
|
}
|
539
lib/pages/image_favorites_page/image_favorites_page.dart
Normal file
539
lib/pages/image_favorites_page/image_favorites_page.dart
Normal file
@@ -0,0 +1,539 @@
|
|||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:photo_view/photo_view.dart';
|
||||||
|
import 'package:photo_view/photo_view_gallery.dart';
|
||||||
|
import 'package:venera/components/components.dart';
|
||||||
|
import 'package:venera/foundation/app.dart';
|
||||||
|
import 'package:venera/foundation/appdata.dart';
|
||||||
|
import 'package:venera/foundation/comic_source/comic_source.dart';
|
||||||
|
import 'package:venera/foundation/consts.dart';
|
||||||
|
import 'package:venera/foundation/history.dart';
|
||||||
|
import 'package:venera/foundation/image_provider/image_favorites_provider.dart';
|
||||||
|
import 'package:venera/pages/comic_page.dart';
|
||||||
|
import 'package:venera/pages/image_favorites_page/type.dart';
|
||||||
|
import 'package:venera/pages/reader/reader.dart';
|
||||||
|
import 'package:venera/utils/ext.dart';
|
||||||
|
import 'package:venera/utils/file_type.dart';
|
||||||
|
import 'package:venera/utils/io.dart';
|
||||||
|
import 'package:venera/utils/tags_translation.dart';
|
||||||
|
import 'package:venera/utils/translations.dart';
|
||||||
|
|
||||||
|
part "image_favorites_item.dart";
|
||||||
|
|
||||||
|
part "image_favorites_photo_view.dart";
|
||||||
|
|
||||||
|
class ImageFavoritesPage extends StatefulWidget {
|
||||||
|
const ImageFavoritesPage({super.key, this.initialKeyword});
|
||||||
|
|
||||||
|
final String? initialKeyword;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ImageFavoritesPage> createState() => _ImageFavoritesPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ImageFavoritesPageState extends State<ImageFavoritesPage> {
|
||||||
|
late ImageFavoriteSortType sortType;
|
||||||
|
late TimeRange timeFilterSelect;
|
||||||
|
late int numFilterSelect;
|
||||||
|
|
||||||
|
// 所有的图片收藏
|
||||||
|
List<ImageFavoritesComic> comics = [];
|
||||||
|
|
||||||
|
late var controller =
|
||||||
|
TextEditingController(text: widget.initialKeyword ?? "");
|
||||||
|
|
||||||
|
String get keyword => controller.text;
|
||||||
|
|
||||||
|
// 进入关键词搜索模式
|
||||||
|
bool searchMode = false;
|
||||||
|
|
||||||
|
bool multiSelectMode = false;
|
||||||
|
|
||||||
|
// 多选的时候选中的图片
|
||||||
|
Map<ImageFavorite, bool> selectedImageFavorites = {};
|
||||||
|
|
||||||
|
void update() {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateImageFavorites() async {
|
||||||
|
comics = searchMode
|
||||||
|
? ImageFavoriteManager().search(keyword)
|
||||||
|
: ImageFavoriteManager().getAll();
|
||||||
|
sortImageFavorites();
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void sortImageFavorites() {
|
||||||
|
comics = searchMode
|
||||||
|
? ImageFavoriteManager().search(keyword)
|
||||||
|
: ImageFavoriteManager().getAll();
|
||||||
|
// 筛选到最终列表
|
||||||
|
comics = comics.where((ele) {
|
||||||
|
bool isFilter = true;
|
||||||
|
if (timeFilterSelect != TimeRange.all) {
|
||||||
|
isFilter = timeFilterSelect.contains(ele.time);
|
||||||
|
}
|
||||||
|
if (numFilterSelect != numFilterList[0]) {
|
||||||
|
isFilter = ele.images.length > numFilterSelect;
|
||||||
|
}
|
||||||
|
return isFilter;
|
||||||
|
}).toList();
|
||||||
|
// 给列表排序
|
||||||
|
switch (sortType) {
|
||||||
|
case ImageFavoriteSortType.title:
|
||||||
|
comics.sort((a, b) => a.title.compareTo(b.title));
|
||||||
|
case ImageFavoriteSortType.timeAsc:
|
||||||
|
comics.sort((a, b) => a.time.compareTo(b.time));
|
||||||
|
case ImageFavoriteSortType.timeDesc:
|
||||||
|
comics.sort((a, b) => b.time.compareTo(a.time));
|
||||||
|
case ImageFavoriteSortType.maxFavorites:
|
||||||
|
comics.sort((a, b) => b.images.length
|
||||||
|
.compareTo(a.images.length));
|
||||||
|
case ImageFavoriteSortType.favoritesCompareComicPages:
|
||||||
|
comics.sort((a, b) {
|
||||||
|
double tempA = a.images.length / a.maxPageFromEp;
|
||||||
|
double tempB = b.images.length / b.maxPageFromEp;
|
||||||
|
return tempB.compareTo(tempA);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
if (widget.initialKeyword != null) {
|
||||||
|
searchMode = true;
|
||||||
|
}
|
||||||
|
sortType = ImageFavoriteSortType.values.firstWhereOrNull(
|
||||||
|
(e) => e.value == appdata.implicitData["image_favorites_sort"]) ??
|
||||||
|
ImageFavoriteSortType.title;
|
||||||
|
timeFilterSelect = TimeRange.fromString(
|
||||||
|
appdata.implicitData["image_favorites_time_filter"]);
|
||||||
|
numFilterSelect = appdata.implicitData["image_favorites_number_filter"] ??
|
||||||
|
numFilterList[0];
|
||||||
|
updateImageFavorites();
|
||||||
|
ImageFavoriteManager().addListener(updateImageFavorites);
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
ImageFavoriteManager().removeListener(updateImageFavorites);
|
||||||
|
scrollController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildMultiSelectMenu() {
|
||||||
|
return MenuButton(entries: [
|
||||||
|
MenuEntry(
|
||||||
|
icon: Icons.delete_outline,
|
||||||
|
text: "Delete".tl,
|
||||||
|
onClick: () {
|
||||||
|
ImageFavoriteManager()
|
||||||
|
.deleteImageFavorite(selectedImageFavorites.keys);
|
||||||
|
setState(() {
|
||||||
|
multiSelectMode = false;
|
||||||
|
selectedImageFavorites.clear();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
var scrollController = ScrollController();
|
||||||
|
|
||||||
|
void selectAll() {
|
||||||
|
for (var c in comics) {
|
||||||
|
for (var i in c.images) {
|
||||||
|
selectedImageFavorites[i] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void deSelect() {
|
||||||
|
setState(() {
|
||||||
|
selectedImageFavorites.clear();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void addSelected(ImageFavorite i) {
|
||||||
|
if (selectedImageFavorites[i] == null) {
|
||||||
|
selectedImageFavorites[i] = true;
|
||||||
|
} else {
|
||||||
|
selectedImageFavorites.remove(i);
|
||||||
|
}
|
||||||
|
if (selectedImageFavorites.isEmpty) {
|
||||||
|
multiSelectMode = false;
|
||||||
|
} else {
|
||||||
|
multiSelectMode = true;
|
||||||
|
}
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
List<Widget> selectActions = [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.select_all),
|
||||||
|
tooltip: "Select All".tl,
|
||||||
|
onPressed: selectAll),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.deselect),
|
||||||
|
tooltip: "Deselect".tl,
|
||||||
|
onPressed: deSelect),
|
||||||
|
buildMultiSelectMenu(),
|
||||||
|
];
|
||||||
|
|
||||||
|
var scrollWidget = SmoothCustomScrollView(
|
||||||
|
controller: scrollController,
|
||||||
|
slivers: [
|
||||||
|
if (!searchMode && !multiSelectMode)
|
||||||
|
SliverAppbar(
|
||||||
|
title: Text("Image Favorites".tl),
|
||||||
|
actions: [
|
||||||
|
Tooltip(
|
||||||
|
message: "Search".tl,
|
||||||
|
child: IconButton(
|
||||||
|
icon: const Icon(Icons.search),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
searchMode = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Tooltip(
|
||||||
|
message: "Sort".tl,
|
||||||
|
child: IconButton(
|
||||||
|
isSelected: timeFilterSelect != TimeRange.all ||
|
||||||
|
numFilterSelect != numFilterList[0],
|
||||||
|
icon: const Icon(Icons.sort_rounded),
|
||||||
|
onPressed: sort,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Tooltip(
|
||||||
|
message: multiSelectMode
|
||||||
|
? "Exit Multi-Select".tl
|
||||||
|
: "Multi-Select".tl,
|
||||||
|
child: IconButton(
|
||||||
|
icon: const Icon(Icons.checklist),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
multiSelectMode = !multiSelectMode;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
else if (multiSelectMode)
|
||||||
|
SliverAppbar(
|
||||||
|
leading: Tooltip(
|
||||||
|
message: "Cancel".tl,
|
||||||
|
child: IconButton(
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
multiSelectMode = false;
|
||||||
|
selectedImageFavorites.clear();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Text(selectedImageFavorites.length.toString()),
|
||||||
|
actions: selectActions,
|
||||||
|
)
|
||||||
|
else if (searchMode)
|
||||||
|
SliverAppbar(
|
||||||
|
leading: Tooltip(
|
||||||
|
message: "Cancel".tl,
|
||||||
|
child: IconButton(
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
|
onPressed: () {
|
||||||
|
controller.clear();
|
||||||
|
setState(() {
|
||||||
|
searchMode = false;
|
||||||
|
controller.clear();
|
||||||
|
updateImageFavorites();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: TextField(
|
||||||
|
autofocus: true,
|
||||||
|
controller: controller,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: "Search".tl,
|
||||||
|
border: InputBorder.none,
|
||||||
|
),
|
||||||
|
onChanged: (v) {
|
||||||
|
updateImageFavorites();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SliverList(
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(context, index) {
|
||||||
|
return _ImageFavoritesItem(
|
||||||
|
imageFavoritesComic: comics[index],
|
||||||
|
selectedImageFavorites: selectedImageFavorites,
|
||||||
|
addSelected: addSelected,
|
||||||
|
multiSelectMode: multiSelectMode,
|
||||||
|
finalImageFavoritesComicList: comics,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
childCount: comics.length,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SliverPadding(padding: EdgeInsets.only(top: context.padding.bottom)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
Widget body = Scrollbar(
|
||||||
|
controller: scrollController,
|
||||||
|
thickness: App.isDesktop ? 8 : 12,
|
||||||
|
radius: const Radius.circular(8),
|
||||||
|
interactive: true,
|
||||||
|
child: ScrollConfiguration(
|
||||||
|
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
|
||||||
|
child: context.width > changePoint
|
||||||
|
? scrollWidget.paddingHorizontal(8)
|
||||||
|
: scrollWidget,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return PopScope(
|
||||||
|
canPop: !multiSelectMode && !searchMode,
|
||||||
|
onPopInvokedWithResult: (didPop, result) {
|
||||||
|
if (multiSelectMode) {
|
||||||
|
setState(() {
|
||||||
|
multiSelectMode = false;
|
||||||
|
selectedImageFavorites.clear();
|
||||||
|
});
|
||||||
|
} else if (searchMode) {
|
||||||
|
controller.clear();
|
||||||
|
searchMode = false;
|
||||||
|
updateImageFavorites();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: body,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void sort() {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return _ImageFavoritesDialog(
|
||||||
|
initSortType: sortType,
|
||||||
|
initTimeFilterSelect: timeFilterSelect,
|
||||||
|
initNumFilterSelect: numFilterSelect,
|
||||||
|
updateConfig: (sortType, timeFilter, numFilter) {
|
||||||
|
setState(() {
|
||||||
|
this.sortType = sortType;
|
||||||
|
timeFilterSelect = timeFilter;
|
||||||
|
numFilterSelect = numFilter;
|
||||||
|
});
|
||||||
|
sortImageFavorites();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ImageFavoritesDialog extends StatefulWidget {
|
||||||
|
const _ImageFavoritesDialog({
|
||||||
|
required this.initSortType,
|
||||||
|
required this.initTimeFilterSelect,
|
||||||
|
required this.initNumFilterSelect,
|
||||||
|
required this.updateConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
final ImageFavoriteSortType initSortType;
|
||||||
|
final TimeRange initTimeFilterSelect;
|
||||||
|
final int initNumFilterSelect;
|
||||||
|
final Function updateConfig;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_ImageFavoritesDialog> createState() => _ImageFavoritesDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ImageFavoritesDialogState extends State<_ImageFavoritesDialog> {
|
||||||
|
List<String> optionTypes = ['Sort', 'Filter'];
|
||||||
|
late var sortType = widget.initSortType;
|
||||||
|
late var numFilter = widget.initNumFilterSelect;
|
||||||
|
late TimeRangeType timeRangeType;
|
||||||
|
DateTime? start;
|
||||||
|
DateTime? end;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
timeRangeType = switch (widget.initTimeFilterSelect) {
|
||||||
|
TimeRange.all => TimeRangeType.all,
|
||||||
|
TimeRange.lastWeek => TimeRangeType.lastWeek,
|
||||||
|
TimeRange.lastMonth => TimeRangeType.lastMonth,
|
||||||
|
TimeRange.lastHalfYear => TimeRangeType.lastHalfYear,
|
||||||
|
TimeRange.lastYear => TimeRangeType.lastYear,
|
||||||
|
_ => TimeRangeType.custom,
|
||||||
|
};
|
||||||
|
if (timeRangeType == TimeRangeType.custom) {
|
||||||
|
end = widget.initTimeFilterSelect.end;
|
||||||
|
start = end!.subtract(widget.initTimeFilterSelect.duration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
Widget tabBar = Material(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: FilledTabBar(
|
||||||
|
key: PageStorageKey(optionTypes),
|
||||||
|
tabs: optionTypes.map((e) => Tab(text: e.tl, key: Key(e))).toList(),
|
||||||
|
),
|
||||||
|
).paddingTop(context.padding.top);
|
||||||
|
return ContentDialog(
|
||||||
|
content: DefaultTabController(
|
||||||
|
length: 2,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
tabBar,
|
||||||
|
TabViewBody(children: [
|
||||||
|
Column(
|
||||||
|
children: ImageFavoriteSortType.values
|
||||||
|
.map(
|
||||||
|
(e) => RadioListTile<ImageFavoriteSortType>(
|
||||||
|
title: Text(e.value.tl),
|
||||||
|
value: e,
|
||||||
|
groupValue: sortType,
|
||||||
|
onChanged: (v) {
|
||||||
|
setState(() {
|
||||||
|
sortType = v!;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
title: Text("Time Filter".tl),
|
||||||
|
trailing: Select(
|
||||||
|
current: timeRangeType.value.tl,
|
||||||
|
values:
|
||||||
|
TimeRangeType.values.map((e) => e.value.tl).toList(),
|
||||||
|
minWidth: 64,
|
||||||
|
onTap: (index) {
|
||||||
|
setState(() {
|
||||||
|
timeRangeType = TimeRangeType.values[index];
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (timeRangeType == TimeRangeType.custom)
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
title: Text("Start Time".tl),
|
||||||
|
trailing: TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
final date = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: start ?? DateTime.now(),
|
||||||
|
firstDate: DateTime(2000),
|
||||||
|
lastDate: end ?? DateTime.now(),
|
||||||
|
);
|
||||||
|
if (date != null) {
|
||||||
|
setState(() {
|
||||||
|
start = date;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text(start == null
|
||||||
|
? "Select Date".tl
|
||||||
|
: DateFormat("yyyy-MM-dd").format(start!)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text("End Time".tl),
|
||||||
|
trailing: TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
final date = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: end ?? DateTime.now(),
|
||||||
|
firstDate: start ?? DateTime(2000),
|
||||||
|
lastDate: DateTime.now(),
|
||||||
|
);
|
||||||
|
if (date != null) {
|
||||||
|
setState(() {
|
||||||
|
end = date;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text(end == null
|
||||||
|
? "Select Date".tl
|
||||||
|
: DateFormat("yyyy-MM-dd").format(end!)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text("Image Favorites Greater Than".tl),
|
||||||
|
trailing: Select(
|
||||||
|
current: numFilter.toString(),
|
||||||
|
values: numFilterList.map((e) => e.toString()).toList(),
|
||||||
|
minWidth: 64,
|
||||||
|
onTap: (index) {
|
||||||
|
setState(() {
|
||||||
|
numFilter = numFilterList[index];
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () {
|
||||||
|
appdata.implicitData["image_favorites_sort"] = sortType.value;
|
||||||
|
TimeRange timeRange;
|
||||||
|
if (timeRangeType == TimeRangeType.custom) {
|
||||||
|
timeRange = TimeRange(
|
||||||
|
end: end,
|
||||||
|
duration: end!.difference(start!),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
timeRange = switch (timeRangeType) {
|
||||||
|
TimeRangeType.all => TimeRange.all,
|
||||||
|
TimeRangeType.lastWeek => TimeRange.lastWeek,
|
||||||
|
TimeRangeType.lastMonth => TimeRange.lastMonth,
|
||||||
|
TimeRangeType.lastHalfYear => TimeRange.lastHalfYear,
|
||||||
|
TimeRangeType.lastYear => TimeRange.lastYear,
|
||||||
|
_ => TimeRange.all,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
appdata.implicitData["image_favorites_time_filter"] =
|
||||||
|
timeRange.toString();
|
||||||
|
appdata.implicitData["image_favorites_number_filter"] = numFilter;
|
||||||
|
appdata.writeImplicitData();
|
||||||
|
if (mounted) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
widget.updateConfig(sortType, timeRange, numFilter);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text("Confirm".tl),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
253
lib/pages/image_favorites_page/image_favorites_photo_view.dart
Normal file
253
lib/pages/image_favorites_page/image_favorites_photo_view.dart
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
part of 'image_favorites_page.dart';
|
||||||
|
|
||||||
|
class ImageFavoritesPhotoView extends StatefulWidget {
|
||||||
|
const ImageFavoritesPhotoView({
|
||||||
|
super.key,
|
||||||
|
required this.comic,
|
||||||
|
required this.imageFavorite,
|
||||||
|
});
|
||||||
|
|
||||||
|
final ImageFavoritesComic comic;
|
||||||
|
final ImageFavorite imageFavorite;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ImageFavoritesPhotoView> createState() =>
|
||||||
|
_ImageFavoritesPhotoViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ImageFavoritesPhotoViewState extends State<ImageFavoritesPhotoView> {
|
||||||
|
late PageController controller;
|
||||||
|
Map<ImageFavorite, bool> cancelImageFavorites = {};
|
||||||
|
|
||||||
|
var images = <ImageFavorite>[];
|
||||||
|
|
||||||
|
int currentPage = 0;
|
||||||
|
|
||||||
|
bool isAppBarShow = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
var current = 0;
|
||||||
|
for (var ep in widget.comic.imageFavoritesEp) {
|
||||||
|
for (var image in ep.imageFavorites) {
|
||||||
|
images.add(image);
|
||||||
|
if (image == widget.imageFavorite) {
|
||||||
|
current = images.length - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentPage = current;
|
||||||
|
controller = PageController(initialPage: current);
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
void onPop() {
|
||||||
|
List<ImageFavorite> tempList = cancelImageFavorites.entries
|
||||||
|
.where((e) => e.value == true)
|
||||||
|
.map((e) => e.key)
|
||||||
|
.toList();
|
||||||
|
if (tempList.isNotEmpty) {
|
||||||
|
ImageFavoriteManager().deleteImageFavorite(tempList);
|
||||||
|
showToast(
|
||||||
|
message: "Delete @a images".tlParams({'a': tempList.length}),
|
||||||
|
context: context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PhotoViewGalleryPageOptions _buildItem(BuildContext context, int index) {
|
||||||
|
var image = images[index];
|
||||||
|
return PhotoViewGalleryPageOptions(
|
||||||
|
// 图片加载器 支持本地、网络
|
||||||
|
imageProvider: ImageFavoritesProvider(image),
|
||||||
|
// 初始化大小 全部展示
|
||||||
|
minScale: PhotoViewComputedScale.contained * 1.0,
|
||||||
|
maxScale: PhotoViewComputedScale.covered * 10.0,
|
||||||
|
onTapUp: (context, details, controllerValue) {
|
||||||
|
setState(() {
|
||||||
|
isAppBarShow = !isAppBarShow;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
heroAttributes: PhotoViewHeroAttributes(
|
||||||
|
tag: "${image.sourceKey}${image.ep}${image.page}",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return PopScope(
|
||||||
|
onPopInvokedWithResult: (bool didPop, Object? result) async {
|
||||||
|
if (didPop) {
|
||||||
|
onPop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Listener(
|
||||||
|
onPointerSignal: (event) {
|
||||||
|
if (HardwareKeyboard.instance.isControlPressed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event is PointerScrollEvent) {
|
||||||
|
if (event.scrollDelta.dy > 0) {
|
||||||
|
if (controller.page! >= images.length - 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
controller.nextPage(
|
||||||
|
duration: Duration(milliseconds: 180), curve: Curves.ease);
|
||||||
|
} else {
|
||||||
|
if (controller.page! <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
controller.previousPage(
|
||||||
|
duration: Duration(milliseconds: 180), curve: Curves.ease);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Stack(children: [
|
||||||
|
Positioned.fill(
|
||||||
|
child: PhotoViewGallery.builder(
|
||||||
|
backgroundDecoration: BoxDecoration(
|
||||||
|
color: context.colorScheme.surface,
|
||||||
|
),
|
||||||
|
builder: _buildItem,
|
||||||
|
itemCount: images.length,
|
||||||
|
loadingBuilder: (context, event) => Center(
|
||||||
|
child: SizedBox(
|
||||||
|
width: 20.0,
|
||||||
|
height: 20.0,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
backgroundColor: context.colorScheme.surfaceContainerHigh,
|
||||||
|
value: event == null || event.expectedTotalBytes == null
|
||||||
|
? null
|
||||||
|
: event.cumulativeBytesLoaded /
|
||||||
|
event.expectedTotalBytes!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
pageController: controller,
|
||||||
|
onPageChanged: (index) {
|
||||||
|
setState(() {
|
||||||
|
currentPage = index;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
buildPageInfo(),
|
||||||
|
AnimatedPositioned(
|
||||||
|
top: isAppBarShow ? 0 : -(context.padding.top + 52),
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
duration: Duration(milliseconds: 180),
|
||||||
|
child: buildAppBar(),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildPageInfo() {
|
||||||
|
var text = "${currentPage + 1}/${images.length}";
|
||||||
|
return Positioned(
|
||||||
|
height: 40,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
child: Center(
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
text,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
foreground: Paint()
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeWidth = 1.4
|
||||||
|
..color = context.colorScheme.onInverseSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(text),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildAppBar() {
|
||||||
|
return Material(
|
||||||
|
color: context.colorScheme.surface.toOpacity(0.72),
|
||||||
|
child: BlurEffect(
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
bottom: BorderSide(
|
||||||
|
color: context.colorScheme.outlineVariant,
|
||||||
|
width: 0.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
height: 52,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.close),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
widget.comic.title,
|
||||||
|
style: TextStyle(fontSize: 18),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.more_vert),
|
||||||
|
onPressed: showMenu,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
).paddingTop(context.padding.top),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void showMenu() {
|
||||||
|
showMenuX(
|
||||||
|
context,
|
||||||
|
Offset(context.width, context.padding.top),
|
||||||
|
[
|
||||||
|
MenuEntry(
|
||||||
|
icon: Icons.image_outlined,
|
||||||
|
text: "Save Image".tl,
|
||||||
|
onClick: () async {
|
||||||
|
var temp = images[currentPage];
|
||||||
|
var imageProvider = ImageFavoritesProvider(temp);
|
||||||
|
var data = await imageProvider.load(null);
|
||||||
|
var fileType = detectFileType(data);
|
||||||
|
var fileName = "${currentPage + 1}.${fileType.ext}";
|
||||||
|
await saveFile(filename: fileName, data: data);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
MenuEntry(
|
||||||
|
icon: Icons.menu_book_outlined,
|
||||||
|
text: "Read".tl,
|
||||||
|
onClick: () async {
|
||||||
|
var comic = widget.comic;
|
||||||
|
var ep = images[currentPage].ep;
|
||||||
|
var page = images[currentPage].page;
|
||||||
|
App.rootContext.to(
|
||||||
|
() => ReaderWithLoading(
|
||||||
|
id: comic.id,
|
||||||
|
sourceKey: comic.sourceKey,
|
||||||
|
initialEp: ep,
|
||||||
|
initialPage: page,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
101
lib/pages/image_favorites_page/type.dart
Normal file
101
lib/pages/image_favorites_page/type.dart
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import 'package:venera/utils/ext.dart';
|
||||||
|
|
||||||
|
enum ImageFavoriteSortType {
|
||||||
|
title("Title"),
|
||||||
|
timeAsc("Time Asc"),
|
||||||
|
timeDesc("Time Desc"),
|
||||||
|
maxFavorites("Favorite Num"), // 单本收藏数最多排序
|
||||||
|
favoritesCompareComicPages("Favorite Num Compare Comic Pages"); // 单本收藏数比上总页数
|
||||||
|
|
||||||
|
final String value;
|
||||||
|
|
||||||
|
const ImageFavoriteSortType(this.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const numFilterList = [0, 1, 2, 5, 10, 20, 50, 100];
|
||||||
|
|
||||||
|
class TimeRange {
|
||||||
|
/// End of the range, null means now
|
||||||
|
final DateTime? end;
|
||||||
|
|
||||||
|
/// Duration of the range
|
||||||
|
final Duration duration;
|
||||||
|
|
||||||
|
/// Create a time range
|
||||||
|
const TimeRange({this.end, required this.duration});
|
||||||
|
|
||||||
|
static const all = TimeRange(end: null, duration: Duration.zero);
|
||||||
|
|
||||||
|
static const lastWeek = TimeRange(end: null, duration: Duration(days: 7));
|
||||||
|
|
||||||
|
static const lastMonth = TimeRange(end: null, duration: Duration(days: 30));
|
||||||
|
|
||||||
|
static const lastHalfYear =
|
||||||
|
TimeRange(end: null, duration: Duration(days: 180));
|
||||||
|
|
||||||
|
static const lastYear = TimeRange(end: null, duration: Duration(days: 365));
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return "${end?.millisecond}:${duration.inMilliseconds}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a time range from a string, return [TimeRange.all] if failed
|
||||||
|
factory TimeRange.fromString(String? str) {
|
||||||
|
if (str == null) {
|
||||||
|
return TimeRange.all;
|
||||||
|
}
|
||||||
|
final parts = str.split(":");
|
||||||
|
if (parts.length != 2 || !parts[0].isInt || !parts[1].isInt) {
|
||||||
|
return TimeRange.all;
|
||||||
|
}
|
||||||
|
final end = parts[0] == "null"
|
||||||
|
? null
|
||||||
|
: DateTime.fromMillisecondsSinceEpoch(int.parse(parts[0]));
|
||||||
|
final duration = Duration(milliseconds: int.parse(parts[1]));
|
||||||
|
return TimeRange(end: end, duration: duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a time is in the range
|
||||||
|
bool contains(DateTime time) {
|
||||||
|
if (end != null && time.isAfter(end!)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (duration == Duration.zero) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
final start = end == null
|
||||||
|
? DateTime.now().subtract(duration)
|
||||||
|
: end!.subtract(duration);
|
||||||
|
return time.isAfter(start);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is TimeRange && other.end == end && other.duration == duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => end.hashCode ^ duration.hashCode;
|
||||||
|
|
||||||
|
static const List<TimeRange> values = [
|
||||||
|
all,
|
||||||
|
lastWeek,
|
||||||
|
lastMonth,
|
||||||
|
lastHalfYear,
|
||||||
|
lastYear,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TimeRangeType {
|
||||||
|
all("All"),
|
||||||
|
lastWeek("Last Week"),
|
||||||
|
lastMonth("Last Month"),
|
||||||
|
lastHalfYear("Last Half Year"),
|
||||||
|
lastYear("Last Year"),
|
||||||
|
custom("Custom");
|
||||||
|
|
||||||
|
final String value;
|
||||||
|
|
||||||
|
const TimeRangeType(this.value);
|
||||||
|
}
|
@@ -20,7 +20,7 @@ class _ReaderGestureDetectorState extends State<_ReaderGestureDetector> {
|
|||||||
|
|
||||||
static const _kTapToTurnPagePercent = 0.3;
|
static const _kTapToTurnPagePercent = 0.3;
|
||||||
|
|
||||||
_DragListener? dragListener;
|
final _dragListeners = <_DragListener>[];
|
||||||
|
|
||||||
int fingers = 0;
|
int fingers = 0;
|
||||||
|
|
||||||
@@ -44,19 +44,23 @@ class _ReaderGestureDetectorState extends State<_ReaderGestureDetector> {
|
|||||||
_lastTapPointer = event.pointer;
|
_lastTapPointer = event.pointer;
|
||||||
_lastTapMoveDistance = Offset.zero;
|
_lastTapMoveDistance = Offset.zero;
|
||||||
_tapGestureRecognizer.addPointer(event);
|
_tapGestureRecognizer.addPointer(event);
|
||||||
if(_dragInProgress) {
|
if (_dragInProgress) {
|
||||||
dragListener?.onEnd?.call();
|
for (var dragListener in _dragListeners) {
|
||||||
|
dragListener.onStart?.call(event.position);
|
||||||
|
}
|
||||||
_dragInProgress = false;
|
_dragInProgress = false;
|
||||||
}
|
}
|
||||||
Future.delayed(_kLongPressMinTime, () {
|
Future.delayed(_kLongPressMinTime, () {
|
||||||
if (_lastTapPointer == event.pointer && fingers == 1) {
|
if (_lastTapPointer == event.pointer && fingers == 1) {
|
||||||
if(_lastTapMoveDistance!.distanceSquared < 20.0 * 20.0) {
|
if (_lastTapMoveDistance!.distanceSquared < 20.0 * 20.0) {
|
||||||
onLongPressedDown(event.position);
|
onLongPressedDown(event.position);
|
||||||
_longPressInProgress = true;
|
_longPressInProgress = true;
|
||||||
} else {
|
} else {
|
||||||
_dragInProgress = true;
|
_dragInProgress = true;
|
||||||
dragListener?.onStart?.call(event.position);
|
for (var dragListener in _dragListeners) {
|
||||||
dragListener?.onMove?.call(_lastTapMoveDistance!);
|
dragListener.onStart?.call(event.position);
|
||||||
|
dragListener.onMove?.call(_lastTapMoveDistance!);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -65,8 +69,10 @@ class _ReaderGestureDetectorState extends State<_ReaderGestureDetector> {
|
|||||||
if (event.pointer == _lastTapPointer) {
|
if (event.pointer == _lastTapPointer) {
|
||||||
_lastTapMoveDistance = event.delta + _lastTapMoveDistance!;
|
_lastTapMoveDistance = event.delta + _lastTapMoveDistance!;
|
||||||
}
|
}
|
||||||
if(_dragInProgress) {
|
if (_dragInProgress) {
|
||||||
dragListener?.onMove?.call(event.delta);
|
for (var dragListener in _dragListeners) {
|
||||||
|
dragListener.onMove?.call(event.delta);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onPointerUp: (event) {
|
onPointerUp: (event) {
|
||||||
@@ -74,8 +80,10 @@ class _ReaderGestureDetectorState extends State<_ReaderGestureDetector> {
|
|||||||
if (_longPressInProgress) {
|
if (_longPressInProgress) {
|
||||||
onLongPressedUp(event.position);
|
onLongPressedUp(event.position);
|
||||||
}
|
}
|
||||||
if(_dragInProgress) {
|
if (_dragInProgress) {
|
||||||
dragListener?.onEnd?.call();
|
for (var dragListener in _dragListeners) {
|
||||||
|
dragListener.onEnd?.call();
|
||||||
|
}
|
||||||
_dragInProgress = false;
|
_dragInProgress = false;
|
||||||
}
|
}
|
||||||
_lastTapPointer = null;
|
_lastTapPointer = null;
|
||||||
@@ -86,8 +94,10 @@ class _ReaderGestureDetectorState extends State<_ReaderGestureDetector> {
|
|||||||
if (_longPressInProgress) {
|
if (_longPressInProgress) {
|
||||||
onLongPressedUp(event.position);
|
onLongPressedUp(event.position);
|
||||||
}
|
}
|
||||||
if(_dragInProgress) {
|
if (_dragInProgress) {
|
||||||
dragListener?.onEnd?.call();
|
for (var dragListener in _dragListeners) {
|
||||||
|
dragListener.onEnd?.call();
|
||||||
|
}
|
||||||
_dragInProgress = false;
|
_dragInProgress = false;
|
||||||
}
|
}
|
||||||
_lastTapPointer = null;
|
_lastTapPointer = null;
|
||||||
@@ -261,6 +271,14 @@ class _ReaderGestureDetectorState extends State<_ReaderGestureDetector> {
|
|||||||
void onLongPressedDown(Offset location) {
|
void onLongPressedDown(Offset location) {
|
||||||
context.reader._imageViewController?.handleLongPressDown(location);
|
context.reader._imageViewController?.handleLongPressDown(location);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void addDragListener(_DragListener listener) {
|
||||||
|
_dragListeners.add(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeDragListener(_DragListener listener) {
|
||||||
|
_dragListeners.remove(listener);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DragListener {
|
class _DragListener {
|
||||||
@@ -269,4 +287,4 @@ class _DragListener {
|
|||||||
void Function()? onEnd;
|
void Function()? onEnd;
|
||||||
|
|
||||||
_DragListener({this.onMove, this.onEnd});
|
_DragListener({this.onMove, this.onEnd});
|
||||||
}
|
}
|
||||||
|
@@ -263,6 +263,10 @@ class _GalleryModeState extends State<_GalleryMode>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void handleDoubleTap(Offset location) {
|
void handleDoubleTap(Offset location) {
|
||||||
|
if (appdata.settings['quickCollectImage'] == 'DoubleTap') {
|
||||||
|
context.readerScaffold.addImageFavorite();
|
||||||
|
return;
|
||||||
|
}
|
||||||
var controller = photoViewControllers[reader.page]!;
|
var controller = photoViewControllers[reader.page]!;
|
||||||
controller.onDoubleClick?.call();
|
controller.onDoubleClick?.call();
|
||||||
}
|
}
|
||||||
@@ -461,7 +465,7 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
|||||||
widget = Listener(
|
widget = Listener(
|
||||||
onPointerDown: (event) {
|
onPointerDown: (event) {
|
||||||
fingers++;
|
fingers++;
|
||||||
if(fingers > 1 && !disableScroll) {
|
if (fingers > 1 && !disableScroll) {
|
||||||
setState(() {
|
setState(() {
|
||||||
disableScroll = true;
|
disableScroll = true;
|
||||||
});
|
});
|
||||||
@@ -475,7 +479,7 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
|||||||
},
|
},
|
||||||
onPointerUp: (event) {
|
onPointerUp: (event) {
|
||||||
fingers--;
|
fingers--;
|
||||||
if(fingers <= 1 && disableScroll) {
|
if (fingers <= 1 && disableScroll) {
|
||||||
setState(() {
|
setState(() {
|
||||||
disableScroll = false;
|
disableScroll = false;
|
||||||
});
|
});
|
||||||
@@ -564,6 +568,10 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void handleDoubleTap(Offset location) {
|
void handleDoubleTap(Offset location) {
|
||||||
|
if (appdata.settings['quickCollectImage'] == 'DoubleTap') {
|
||||||
|
context.readerScaffold.addImageFavorite();
|
||||||
|
return;
|
||||||
|
}
|
||||||
double target;
|
double target;
|
||||||
if (photoViewController.scale !=
|
if (photoViewController.scale !=
|
||||||
photoViewController.getInitialScale?.call()) {
|
photoViewController.getInitialScale?.call()) {
|
||||||
|
@@ -5,12 +5,18 @@ class ReaderWithLoading extends StatefulWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.sourceKey,
|
required this.sourceKey,
|
||||||
|
this.initialEp,
|
||||||
|
this.initialPage,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String id;
|
final String id;
|
||||||
|
|
||||||
final String sourceKey;
|
final String sourceKey;
|
||||||
|
|
||||||
|
final int? initialEp;
|
||||||
|
|
||||||
|
final int? initialPage;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ReaderWithLoading> createState() => _ReaderWithLoadingState();
|
State<ReaderWithLoading> createState() => _ReaderWithLoadingState();
|
||||||
}
|
}
|
||||||
@@ -25,8 +31,10 @@ class _ReaderWithLoadingState
|
|||||||
name: data.name,
|
name: data.name,
|
||||||
chapters: data.chapters,
|
chapters: data.chapters,
|
||||||
history: data.history,
|
history: data.history,
|
||||||
initialChapter: data.history.ep,
|
initialChapter: widget.initialEp ?? data.history.ep,
|
||||||
initialPage: data.history.page,
|
initialPage: widget.initialPage ?? data.history.page,
|
||||||
|
author: data.author,
|
||||||
|
tags: data.tags,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,6 +65,8 @@ class _ReaderWithLoadingState
|
|||||||
ep: 0,
|
ep: 0,
|
||||||
page: 0,
|
page: 0,
|
||||||
),
|
),
|
||||||
|
author: localComic.subtitle,
|
||||||
|
tags: localComic.tags,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -76,6 +86,8 @@ class _ReaderWithLoadingState
|
|||||||
ep: 0,
|
ep: 0,
|
||||||
page: 0,
|
page: 0,
|
||||||
),
|
),
|
||||||
|
author: comic.data.findAuthor() ?? "",
|
||||||
|
tags: comic.data.plainTags,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -93,11 +105,17 @@ class ReaderProps {
|
|||||||
|
|
||||||
final History history;
|
final History history;
|
||||||
|
|
||||||
|
final String author;
|
||||||
|
|
||||||
|
final List<String> tags;
|
||||||
|
|
||||||
const ReaderProps({
|
const ReaderProps({
|
||||||
required this.type,
|
required this.type,
|
||||||
required this.cid,
|
required this.cid,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.chapters,
|
required this.chapters,
|
||||||
required this.history,
|
required this.history,
|
||||||
|
required this.author,
|
||||||
|
required this.tags,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@@ -20,6 +20,7 @@ import 'package:venera/foundation/appdata.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';
|
||||||
import 'package:venera/foundation/comic_type.dart';
|
import 'package:venera/foundation/comic_type.dart';
|
||||||
|
import 'package:venera/foundation/consts.dart';
|
||||||
import 'package:venera/foundation/history.dart';
|
import 'package:venera/foundation/history.dart';
|
||||||
import 'package:venera/foundation/image_provider/reader_image.dart';
|
import 'package:venera/foundation/image_provider/reader_image.dart';
|
||||||
import 'package:venera/foundation/local.dart';
|
import 'package:venera/foundation/local.dart';
|
||||||
@@ -27,8 +28,10 @@ import 'package:venera/foundation/log.dart';
|
|||||||
import 'package:venera/foundation/res.dart';
|
import 'package:venera/foundation/res.dart';
|
||||||
import 'package:venera/pages/settings/settings_page.dart';
|
import 'package:venera/pages/settings/settings_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/file_type.dart';
|
import 'package:venera/utils/file_type.dart';
|
||||||
import 'package:venera/utils/io.dart';
|
import 'package:venera/utils/io.dart';
|
||||||
|
import 'package:venera/utils/tags_translation.dart';
|
||||||
import 'package:venera/utils/translations.dart';
|
import 'package:venera/utils/translations.dart';
|
||||||
import 'package:venera/utils/volume.dart';
|
import 'package:venera/utils/volume.dart';
|
||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
@@ -57,10 +60,16 @@ class Reader extends StatefulWidget {
|
|||||||
required this.history,
|
required this.history,
|
||||||
this.initialPage,
|
this.initialPage,
|
||||||
this.initialChapter,
|
this.initialChapter,
|
||||||
|
required this.author,
|
||||||
|
required this.tags,
|
||||||
});
|
});
|
||||||
|
|
||||||
final ComicType type;
|
final ComicType type;
|
||||||
|
|
||||||
|
final String author;
|
||||||
|
|
||||||
|
final List<String> tags;
|
||||||
|
|
||||||
final String cid;
|
final String cid;
|
||||||
|
|
||||||
final String name;
|
final String name;
|
||||||
@@ -114,12 +123,14 @@ class _ReaderState extends State<Reader> with _ReaderLocation, _ReaderWindow {
|
|||||||
void _checkImagesPerPageChange() {
|
void _checkImagesPerPageChange() {
|
||||||
int currentImagesPerPage = imagesPerPage;
|
int currentImagesPerPage = imagesPerPage;
|
||||||
if (_lastImagesPerPage != currentImagesPerPage) {
|
if (_lastImagesPerPage != currentImagesPerPage) {
|
||||||
_adjustPageForImagesPerPageChange(_lastImagesPerPage, currentImagesPerPage);
|
_adjustPageForImagesPerPageChange(
|
||||||
|
_lastImagesPerPage, currentImagesPerPage);
|
||||||
_lastImagesPerPage = currentImagesPerPage;
|
_lastImagesPerPage = currentImagesPerPage;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _adjustPageForImagesPerPageChange(int oldImagesPerPage, int newImagesPerPage) {
|
void _adjustPageForImagesPerPageChange(
|
||||||
|
int oldImagesPerPage, int newImagesPerPage) {
|
||||||
int previousImageIndex = (page - 1) * oldImagesPerPage;
|
int previousImageIndex = (page - 1) * oldImagesPerPage;
|
||||||
int newPage = (previousImageIndex ~/ newImagesPerPage) + 1;
|
int newPage = (previousImageIndex ~/ newImagesPerPage) + 1;
|
||||||
page = newPage;
|
page = newPage;
|
||||||
@@ -150,7 +161,7 @@ class _ReaderState extends State<Reader> with _ReaderLocation, _ReaderWindow {
|
|||||||
updateHistory();
|
updateHistory();
|
||||||
});
|
});
|
||||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
|
||||||
if(appdata.settings['enableTurnPageByVolumeKey']) {
|
if (appdata.settings['enableTurnPageByVolumeKey']) {
|
||||||
handleVolumeEvent();
|
handleVolumeEvent();
|
||||||
}
|
}
|
||||||
setImageCacheSize();
|
setImageCacheSize();
|
||||||
@@ -170,7 +181,8 @@ class _ReaderState extends State<Reader> with _ReaderLocation, _ReaderWindow {
|
|||||||
} else {
|
} else {
|
||||||
maxImageCacheSize = 500 << 20;
|
maxImageCacheSize = 500 << 20;
|
||||||
}
|
}
|
||||||
Log.info("Reader", "Detect available RAM: $availableRAM, set image cache size to $maxImageCacheSize");
|
Log.info("Reader",
|
||||||
|
"Detect available RAM: $availableRAM, set image cache size to $maxImageCacheSize");
|
||||||
PaintingBinding.instance.imageCache.maximumSizeBytes = maxImageCacheSize;
|
PaintingBinding.instance.imageCache.maximumSizeBytes = maxImageCacheSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,7 +227,7 @@ class _ReaderState extends State<Reader> with _ReaderLocation, _ReaderWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void updateHistory() {
|
void updateHistory() {
|
||||||
if(history != null) {
|
if (history != null) {
|
||||||
history!.page = page;
|
history!.page = page;
|
||||||
history!.ep = chapter;
|
history!.ep = chapter;
|
||||||
if (maxPage > 1) {
|
if (maxPage > 1) {
|
||||||
@@ -228,11 +240,11 @@ class _ReaderState extends State<Reader> with _ReaderLocation, _ReaderWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void handleVolumeEvent() {
|
void handleVolumeEvent() {
|
||||||
if(!App.isAndroid) {
|
if (!App.isAndroid) {
|
||||||
// Currently only support Android
|
// Currently only support Android
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(volumeListener != null) {
|
if (volumeListener != null) {
|
||||||
volumeListener?.cancel();
|
volumeListener?.cancel();
|
||||||
}
|
}
|
||||||
volumeListener = VolumeListener(
|
volumeListener = VolumeListener(
|
||||||
@@ -246,7 +258,7 @@ class _ReaderState extends State<Reader> with _ReaderLocation, _ReaderWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void stopVolumeEvent() {
|
void stopVolumeEvent() {
|
||||||
if(volumeListener != null) {
|
if (volumeListener != null) {
|
||||||
volumeListener?.cancel();
|
volumeListener?.cancel();
|
||||||
volumeListener = null;
|
volumeListener = null;
|
||||||
}
|
}
|
||||||
@@ -306,7 +318,8 @@ abstract mixin class _ReaderLocation {
|
|||||||
bool toPage(int page) {
|
bool toPage(int page) {
|
||||||
if (_validatePage(page)) {
|
if (_validatePage(page)) {
|
||||||
if (page == this.page) {
|
if (page == this.page) {
|
||||||
if(!(chapter == 1 && page == 1) && !(chapter == maxChapter && page == maxPage)) {
|
if (!(chapter == 1 && page == 1) &&
|
||||||
|
!(chapter == maxChapter && page == maxPage)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -18,8 +18,9 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
|
|
||||||
bool get isOpen => _isOpen;
|
bool get isOpen => _isOpen;
|
||||||
|
|
||||||
bool get isReversed => context.reader.mode == ReaderMode.galleryRightToLeft ||
|
bool get isReversed =>
|
||||||
context.reader.mode == ReaderMode.continuousRightToLeft;
|
context.reader.mode == ReaderMode.galleryRightToLeft ||
|
||||||
|
context.reader.mode == ReaderMode.continuousRightToLeft;
|
||||||
|
|
||||||
int showFloatingButtonValue = 0;
|
int showFloatingButtonValue = 0;
|
||||||
|
|
||||||
@@ -29,6 +30,8 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
|
|
||||||
_ReaderGestureDetectorState? _gestureDetectorState;
|
_ReaderGestureDetectorState? _gestureDetectorState;
|
||||||
|
|
||||||
|
_DragListener? _floatingButtonDragListener;
|
||||||
|
|
||||||
void setFloatingButton(int value) {
|
void setFloatingButton(int value) {
|
||||||
lastValue = showFloatingButtonValue;
|
lastValue = showFloatingButtonValue;
|
||||||
if (value == 0) {
|
if (value == 0) {
|
||||||
@@ -37,12 +40,15 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
fABValue.value = 0;
|
fABValue.value = 0;
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
_gestureDetectorState!.dragListener = null;
|
if (_floatingButtonDragListener != null) {
|
||||||
|
_gestureDetectorState!.removeDragListener(_floatingButtonDragListener!);
|
||||||
|
_floatingButtonDragListener = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
var readerMode = context.reader.mode;
|
var readerMode = context.reader.mode;
|
||||||
if (value == 1 && showFloatingButtonValue == 0) {
|
if (value == 1 && showFloatingButtonValue == 0) {
|
||||||
showFloatingButtonValue = 1;
|
showFloatingButtonValue = 1;
|
||||||
_gestureDetectorState!.dragListener = _DragListener(
|
_floatingButtonDragListener = _DragListener(
|
||||||
onMove: (offset) {
|
onMove: (offset) {
|
||||||
if (readerMode == ReaderMode.continuousTopToBottom) {
|
if (readerMode == ReaderMode.continuousTopToBottom) {
|
||||||
fABValue.value -= offset.dy;
|
fABValue.value -= offset.dy;
|
||||||
@@ -62,10 +68,11 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
fABValue.value = 0;
|
fABValue.value = 0;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
_gestureDetectorState!.addDragListener(_floatingButtonDragListener!);
|
||||||
update();
|
update();
|
||||||
} else if (value == -1 && showFloatingButtonValue == 0) {
|
} else if (value == -1 && showFloatingButtonValue == 0) {
|
||||||
showFloatingButtonValue = -1;
|
showFloatingButtonValue = -1;
|
||||||
_gestureDetectorState!.dragListener = _DragListener(
|
_floatingButtonDragListener = _DragListener(
|
||||||
onMove: (offset) {
|
onMove: (offset) {
|
||||||
if (readerMode == ReaderMode.continuousTopToBottom) {
|
if (readerMode == ReaderMode.continuousTopToBottom) {
|
||||||
fABValue.value += offset.dy;
|
fABValue.value += offset.dy;
|
||||||
@@ -85,10 +92,48 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
fABValue.value = 0;
|
fABValue.value = 0;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
_gestureDetectorState!.addDragListener(_floatingButtonDragListener!);
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_DragListener? _imageFavoriteDragListener;
|
||||||
|
|
||||||
|
void addDragListener() async {
|
||||||
|
if (!mounted) return;
|
||||||
|
var readerMode = context.reader.mode;
|
||||||
|
|
||||||
|
// 横向阅读的时候, 如果纵向滑就触发收藏, 纵向阅读的时候, 如果横向滑动就触发收藏
|
||||||
|
if (appdata.settings['quickCollectImage'] == 'Swipe') {
|
||||||
|
if (_imageFavoriteDragListener == null) {
|
||||||
|
double distance = 0;
|
||||||
|
_imageFavoriteDragListener = _DragListener(
|
||||||
|
onMove: (offset) {
|
||||||
|
switch (readerMode) {
|
||||||
|
case ReaderMode.continuousTopToBottom:
|
||||||
|
case ReaderMode.galleryTopToBottom:
|
||||||
|
distance += offset.dx;
|
||||||
|
case ReaderMode.continuousLeftToRight:
|
||||||
|
case ReaderMode.galleryLeftToRight:
|
||||||
|
case ReaderMode.galleryRightToLeft:
|
||||||
|
case ReaderMode.continuousRightToLeft:
|
||||||
|
distance += offset.dy;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onEnd: () {
|
||||||
|
if (distance.abs() > 150) {
|
||||||
|
addImageFavorite();
|
||||||
|
}
|
||||||
|
distance = 0;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_gestureDetectorState!.addDragListener(_imageFavoriteDragListener!);
|
||||||
|
} else if (_imageFavoriteDragListener != null) {
|
||||||
|
_gestureDetectorState!.removeDragListener(_imageFavoriteDragListener!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
sliderFocus.canRequestFocus = false;
|
sliderFocus.canRequestFocus = false;
|
||||||
@@ -101,6 +146,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
SystemChrome.setPreferredOrientations(DeviceOrientation.values);
|
SystemChrome.setPreferredOrientations(DeviceOrientation.values);
|
||||||
}
|
}
|
||||||
super.initState();
|
super.initState();
|
||||||
|
Future.delayed(const Duration(milliseconds: 200), addDragListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -203,6 +249,123 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool isLiked() {
|
||||||
|
return ImageFavoriteManager().has(
|
||||||
|
context.reader.cid,
|
||||||
|
context.reader.type.sourceKey,
|
||||||
|
context.reader.eid,
|
||||||
|
context.reader.page,
|
||||||
|
context.reader.chapter,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void addImageFavorite() {
|
||||||
|
try {
|
||||||
|
if (context.reader.images![0].contains('file://')) {
|
||||||
|
showToast(
|
||||||
|
message: "Local comic collection is not supported at present".tl,
|
||||||
|
context: context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String id = context.reader.cid;
|
||||||
|
int ep = context.reader.chapter;
|
||||||
|
String eid = context.reader.eid;
|
||||||
|
String title = context.reader.history!.title;
|
||||||
|
String subTitle = context.reader.history!.subtitle;
|
||||||
|
int maxPage = context.reader.images!.length;
|
||||||
|
int page = context.reader.page;
|
||||||
|
String sourceKey = context.reader.type.sourceKey;
|
||||||
|
String imageKey = context.reader.images![page - 1];
|
||||||
|
List<String> tags = context.reader.widget.tags;
|
||||||
|
String author = context.reader.widget.author;
|
||||||
|
|
||||||
|
var epName = context.reader.widget.chapters?.values
|
||||||
|
.elementAtOrNull(context.reader.chapter - 1) ??
|
||||||
|
"E${context.reader.chapter}";
|
||||||
|
var translatedTags = tags.map((e) => e.translateTagsToCN).toList();
|
||||||
|
|
||||||
|
if (isLiked()) {
|
||||||
|
if (page == firstPage) {
|
||||||
|
showToast(
|
||||||
|
message: "The cover cannot be uncollected here".tl,
|
||||||
|
context: context,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ImageFavoriteManager().deleteImageFavorite([
|
||||||
|
ImageFavorite(page, imageKey, null, eid, id, ep, sourceKey, epName)
|
||||||
|
]);
|
||||||
|
showToast(
|
||||||
|
message: "Uncollected the image".tl,
|
||||||
|
context: context,
|
||||||
|
seconds: 1,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
var imageFavoritesComic = ImageFavoriteManager().find(id, sourceKey) ??
|
||||||
|
ImageFavoritesComic(
|
||||||
|
id,
|
||||||
|
[],
|
||||||
|
title,
|
||||||
|
sourceKey,
|
||||||
|
tags,
|
||||||
|
translatedTags,
|
||||||
|
DateTime.now(),
|
||||||
|
author,
|
||||||
|
{},
|
||||||
|
subTitle,
|
||||||
|
maxPage,
|
||||||
|
);
|
||||||
|
ImageFavorite imageFavorite =
|
||||||
|
ImageFavorite(page, imageKey, null, eid, id, ep, sourceKey, epName);
|
||||||
|
ImageFavoritesEp? imageFavoritesEp =
|
||||||
|
imageFavoritesComic.imageFavoritesEp.firstWhereOrNull((e) {
|
||||||
|
return e.ep == ep;
|
||||||
|
});
|
||||||
|
if (imageFavoritesEp == null) {
|
||||||
|
if (page != firstPage) {
|
||||||
|
var copy = imageFavorite.copyWith(
|
||||||
|
page: firstPage,
|
||||||
|
isAutoFavorite: true,
|
||||||
|
imageKey: context.reader.images![0],
|
||||||
|
);
|
||||||
|
// 不是第一页的话, 自动塞一个封面进去
|
||||||
|
imageFavoritesEp = ImageFavoritesEp(
|
||||||
|
eid, ep, [copy, imageFavorite], epName, maxPage);
|
||||||
|
} else {
|
||||||
|
imageFavoritesEp =
|
||||||
|
ImageFavoritesEp(eid, ep, [imageFavorite], epName, maxPage);
|
||||||
|
}
|
||||||
|
imageFavoritesComic.imageFavoritesEp.add(imageFavoritesEp);
|
||||||
|
} else {
|
||||||
|
if (imageFavoritesEp.eid != eid) {
|
||||||
|
// 空字符串说明是从pica导入的, 那我们就手动刷一遍保证一致
|
||||||
|
if (imageFavoritesEp.eid == "") {
|
||||||
|
imageFavoritesEp.eid == eid;
|
||||||
|
} else {
|
||||||
|
// 避免多章节漫画源的章节顺序发生变化, 如果情况比较多, 做一个以eid为准更新ep的功能
|
||||||
|
showToast(
|
||||||
|
message:
|
||||||
|
"The chapter order of the comic may have changed, temporarily not supported for collection"
|
||||||
|
.tl,
|
||||||
|
context: context,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imageFavoritesEp.imageFavorites.add(imageFavorite);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageFavoriteManager().addOrUpdateOrDelete(imageFavoritesComic);
|
||||||
|
showToast(
|
||||||
|
message: "Successfully collected".tl, context: context, seconds: 1);
|
||||||
|
}
|
||||||
|
update();
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
Log.error("Image Favorite", e, stackTrace);
|
||||||
|
showToast(message: e.toString(), context: context, seconds: 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Widget buildBottom() {
|
Widget buildBottom() {
|
||||||
var text = "E${context.reader.chapter} : P${context.reader.page}";
|
var text = "E${context.reader.chapter} : P${context.reader.page}";
|
||||||
if (context.reader.widget.chapters == null) {
|
if (context.reader.widget.chapters == null) {
|
||||||
@@ -233,13 +396,13 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
child: buildSlider(),
|
child: buildSlider(),
|
||||||
),
|
),
|
||||||
IconButton.filledTonal(
|
IconButton.filledTonal(
|
||||||
onPressed: () => !isReversed
|
onPressed: () => !isReversed
|
||||||
? context.reader.chapter < context.reader.maxChapter
|
? context.reader.chapter < context.reader.maxChapter
|
||||||
? context.reader.toNextChapter()
|
? context.reader.toNextChapter()
|
||||||
: context.reader.toPage(context.reader.maxPage)
|
: context.reader.toPage(context.reader.maxPage)
|
||||||
: context.reader.chapter > 1
|
: context.reader.chapter > 1
|
||||||
? context.reader.toPrevChapter()
|
? context.reader.toPrevChapter()
|
||||||
: context.reader.toPage(1),
|
: context.reader.toPage(1),
|
||||||
icon: const Icon(Icons.last_page)),
|
icon: const Icon(Icons.last_page)),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
width: 8,
|
width: 8,
|
||||||
@@ -263,6 +426,13 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
|
Tooltip(
|
||||||
|
message: "Collect the image".tl,
|
||||||
|
child: IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
isLiked() ? Icons.favorite : Icons.favorite_border),
|
||||||
|
onPressed: addImageFavorite),
|
||||||
|
),
|
||||||
if (App.isWindows)
|
if (App.isWindows)
|
||||||
Tooltip(
|
Tooltip(
|
||||||
message: "${"Full Screen".tl}(F12)",
|
message: "${"Full Screen".tl}(F12)",
|
||||||
@@ -358,12 +528,14 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: context.colorScheme.surface.toOpacity(0.82),
|
color: context.colorScheme.surface.toOpacity(0.82),
|
||||||
border: Border(
|
border: isOpen
|
||||||
top: BorderSide(
|
? Border(
|
||||||
color: Colors.grey.toOpacity(0.5),
|
top: BorderSide(
|
||||||
width: 0.5,
|
color: Colors.grey.toOpacity(0.5),
|
||||||
),
|
width: 0.5,
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
padding: EdgeInsets.only(bottom: context.padding.bottom),
|
padding: EdgeInsets.only(bottom: context.padding.bottom),
|
||||||
child: child,
|
child: child,
|
||||||
@@ -559,7 +731,6 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
onChanged: (key) {
|
onChanged: (key) {
|
||||||
if (key == "readerMode") {
|
if (key == "readerMode") {
|
||||||
context.reader.mode = ReaderMode.fromKey(appdata.settings[key]);
|
context.reader.mode = ReaderMode.fromKey(appdata.settings[key]);
|
||||||
App.rootContext.pop();
|
|
||||||
}
|
}
|
||||||
if (key == "enableTurnPageByVolumeKey") {
|
if (key == "enableTurnPageByVolumeKey") {
|
||||||
if (appdata.settings[key]) {
|
if (appdata.settings[key]) {
|
||||||
@@ -568,6 +739,9 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
context.reader.stopVolumeEvent();
|
context.reader.stopVolumeEvent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (key == "quickCollectImage") {
|
||||||
|
addDragListener();
|
||||||
|
}
|
||||||
context.reader.update();
|
context.reader.update();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@@ -107,15 +107,16 @@ class _AppSettingsState extends State<AppSettings> {
|
|||||||
actionTitle: 'Export'.tl,
|
actionTitle: 'Export'.tl,
|
||||||
).toSliver(),
|
).toSliver(),
|
||||||
_CallbackSetting(
|
_CallbackSetting(
|
||||||
title: "Import App Data".tl,
|
title: "Import App Data (Please restart after success)".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']);
|
||||||
if (file != null) {
|
if (file != null) {
|
||||||
var cacheFile = File(FilePath.join(App.cachePath, "import_data_temp"));
|
var cacheFile =
|
||||||
|
File(FilePath.join(App.cachePath, "import_data_temp"));
|
||||||
await file.saveTo(cacheFile.path);
|
await file.saveTo(cacheFile.path);
|
||||||
try {
|
try {
|
||||||
if(file.name.endsWith('picadata')) {
|
if (file.name.endsWith('picadata')) {
|
||||||
await importPicaData(cacheFile);
|
await importPicaData(cacheFile);
|
||||||
} else {
|
} else {
|
||||||
await importAppData(cacheFile);
|
await importAppData(cacheFile);
|
||||||
@@ -123,8 +124,7 @@ class _AppSettingsState extends State<AppSettings> {
|
|||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Log.error("Import data", e.toString(), s);
|
Log.error("Import data", e.toString(), s);
|
||||||
context.showMessage(message: "Failed to import data".tl);
|
context.showMessage(message: "Failed to import data".tl);
|
||||||
}
|
} finally {
|
||||||
finally {
|
|
||||||
cacheFile.deleteIgnoreError();
|
cacheFile.deleteIgnoreError();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -33,7 +33,9 @@ class _LocalFavoritesSettingsState extends State<LocalFavoritesSettings> {
|
|||||||
SelectSetting(
|
SelectSetting(
|
||||||
title: "Quick Favorite".tl,
|
title: "Quick Favorite".tl,
|
||||||
settingKey: "quickFavorite",
|
settingKey: "quickFavorite",
|
||||||
help: "Long press on the favorite button to quickly add to this folder".tl,
|
help:
|
||||||
|
"Long press on the favorite button to quickly add to this folder"
|
||||||
|
.tl,
|
||||||
optionTranslation: {
|
optionTranslation: {
|
||||||
for (var e in LocalFavoritesManager().folderNames) e: e
|
for (var e in LocalFavoritesManager().folderNames) e: e
|
||||||
},
|
},
|
||||||
@@ -44,7 +46,8 @@ class _LocalFavoritesSettingsState extends State<LocalFavoritesSettings> {
|
|||||||
var controller = showLoadingDialog(context);
|
var controller = showLoadingDialog(context);
|
||||||
var count = await LocalFavoritesManager().removeInvalid();
|
var count = await LocalFavoritesManager().removeInvalid();
|
||||||
controller.close();
|
controller.close();
|
||||||
context.showMessage(message: "Deleted @a favorite items".tlParams({'a': count}));
|
context.showMessage(
|
||||||
|
message: "Deleted @a favorite items".tlParams({'a': count}));
|
||||||
},
|
},
|
||||||
actionTitle: 'Delete'.tl,
|
actionTitle: 'Delete'.tl,
|
||||||
).toSliver(),
|
).toSliver(),
|
||||||
|
@@ -116,6 +116,21 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
|||||||
widget.onChanged?.call("enableClockAndBatteryInfoInReader");
|
widget.onChanged?.call("enableClockAndBatteryInfoInReader");
|
||||||
},
|
},
|
||||||
).toSliver(),
|
).toSliver(),
|
||||||
|
SelectSetting(
|
||||||
|
title: "Quick collect image".tl,
|
||||||
|
settingKey: "quickCollectImage",
|
||||||
|
optionTranslation: {
|
||||||
|
"No": "Not enable".tl,
|
||||||
|
"DoubleTap": "Double Tap".tl,
|
||||||
|
"Swipe": "Swipe".tl,
|
||||||
|
},
|
||||||
|
onChanged: () {
|
||||||
|
widget.onChanged?.call("quickCollectImage");
|
||||||
|
},
|
||||||
|
help:
|
||||||
|
"On the image browsing page, you can quickly collect images by sliding horizontally or vertically according to your reading mode"
|
||||||
|
.tl,
|
||||||
|
).toSliver(),
|
||||||
_PopupWindowSetting(
|
_PopupWindowSetting(
|
||||||
title: "Custom Image Processing".tl,
|
title: "Custom Image Processing".tl,
|
||||||
builder: () => _CustomImageProcessing(),
|
builder: () => _CustomImageProcessing(),
|
||||||
|
@@ -10,6 +10,7 @@ import 'package:venera/foundation/favorites.dart';
|
|||||||
import 'package:venera/foundation/history.dart';
|
import 'package:venera/foundation/history.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/ext.dart';
|
||||||
import 'package:zip_flutter/zip_flutter.dart';
|
import 'package:zip_flutter/zip_flutter.dart';
|
||||||
|
|
||||||
import 'io.dart';
|
import 'io.dart';
|
||||||
@@ -128,7 +129,24 @@ Future<void> importPicaData(File file) async {
|
|||||||
.select("SELECT name FROM sqlite_master WHERE type='table';")
|
.select("SELECT name FROM sqlite_master WHERE type='table';")
|
||||||
.map((e) => e["name"] as String)
|
.map((e) => e["name"] as String)
|
||||||
.toList();
|
.toList();
|
||||||
folderNames.removeWhere((e) => e == "folder_order" || e == "folder_sync");
|
folderNames
|
||||||
|
.removeWhere((e) => e == "folder_order" || e == "folder_sync");
|
||||||
|
for (var folderSyncValue in db.select("SELECT * FROM folder_sync;")) {
|
||||||
|
var folderName = folderSyncValue["folder_name"];
|
||||||
|
String sourceKey = folderSyncValue["key"];
|
||||||
|
sourceKey =
|
||||||
|
sourceKey.toLowerCase() == "htmanga" ? "wnacg" : sourceKey;
|
||||||
|
// 有值就跳过
|
||||||
|
if (LocalFavoritesManager().findLinked(folderName).$1 != null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
LocalFavoritesManager().linkFolderToNetwork(folderName, sourceKey,
|
||||||
|
jsonDecode(folderSyncValue["sync_data"])["folderId"]);
|
||||||
|
} catch (e, stack) {
|
||||||
|
Log.error(e.toString(), stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
for (var folderName in folderNames) {
|
for (var folderName in folderNames) {
|
||||||
if (!LocalFavoritesManager().existsFolder(folderName)) {
|
if (!LocalFavoritesManager().existsFolder(folderName)) {
|
||||||
LocalFavoritesManager().createFolder(folderName);
|
LocalFavoritesManager().createFolder(folderName);
|
||||||
@@ -141,7 +159,7 @@ Future<void> importPicaData(File file) async {
|
|||||||
name: comic['name'],
|
name: comic['name'],
|
||||||
coverPath: comic['cover_path'],
|
coverPath: comic['cover_path'],
|
||||||
author: comic['author'],
|
author: comic['author'],
|
||||||
type: ComicType(switch(comic['type']) {
|
type: ComicType(switch (comic['type']) {
|
||||||
0 => 'picacg'.hashCode,
|
0 => 'picacg'.hashCode,
|
||||||
1 => 'ehentai'.hashCode,
|
1 => 'ehentai'.hashCode,
|
||||||
2 => 'jm'.hashCode,
|
2 => 'jm'.hashCode,
|
||||||
@@ -155,11 +173,9 @@ Future<void> importPicaData(File file) async {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} catch (e) {
|
||||||
catch(e) {
|
|
||||||
Log.error("Import Data", "Failed to import local favorite: $e");
|
Log.error("Import Data", "Failed to import local favorite: $e");
|
||||||
}
|
} finally {
|
||||||
finally {
|
|
||||||
db.dispose();
|
db.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -170,31 +186,80 @@ Future<void> importPicaData(File file) async {
|
|||||||
for (var comic in db.select("SELECT * FROM history;")) {
|
for (var comic in db.select("SELECT * FROM history;")) {
|
||||||
HistoryManager().addHistory(
|
HistoryManager().addHistory(
|
||||||
History.fromMap({
|
History.fromMap({
|
||||||
"type": switch(comic['type']) {
|
"type": switch (comic['type']) {
|
||||||
0 => 'picacg'.hashCode,
|
0 => 'picacg'.hashCode,
|
||||||
1 => 'ehentai'.hashCode,
|
1 => 'ehentai'.hashCode,
|
||||||
2 => 'jm'.hashCode,
|
2 => 'jm'.hashCode,
|
||||||
3 => 'hitomi'.hashCode,
|
3 => 'hitomi'.hashCode,
|
||||||
4 => 'wnacg'.hashCode,
|
4 => 'wnacg'.hashCode,
|
||||||
6 => 'nhentai'.hashCode,
|
5 => 'nhentai'.hashCode,
|
||||||
_ => comic['type']
|
_ => comic['type']
|
||||||
},
|
},
|
||||||
"id": comic['target'],
|
"id": comic['target'],
|
||||||
"maxPage": comic["max_page"],
|
"max_page": comic["max_page"],
|
||||||
"ep": comic["ep"],
|
"ep": comic["ep"],
|
||||||
"page": comic["page"],
|
"page": comic["page"],
|
||||||
"time": comic["time"],
|
"time": comic["time"],
|
||||||
"title": comic["title"],
|
"title": comic["title"],
|
||||||
"subtitle": comic["subtitle"],
|
"subtitle": comic["subtitle"],
|
||||||
"cover": comic["cover"],
|
"cover": comic["cover"],
|
||||||
|
"readEpisode": [comic["ep"]],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
List<ImageFavoritesComic> imageFavoritesComicList =
|
||||||
catch(e) {
|
ImageFavoriteManager().comics;
|
||||||
Log.error("Import Data", "Failed to import history: $e");
|
for (var comic in db.select("SELECT * FROM image_favorites;")) {
|
||||||
}
|
String sourceKey = comic["id"].split("-")[0];
|
||||||
finally {
|
// 换名字了, 绅士漫画
|
||||||
|
if (sourceKey.toLowerCase() == "htmanga") {
|
||||||
|
sourceKey = "wnacg";
|
||||||
|
}
|
||||||
|
if (ComicSource.find(sourceKey) == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String id = comic["id"].split("-")[1];
|
||||||
|
int page = comic["page"];
|
||||||
|
// 章节和page是从1开始的, pica 可能有从 0 开始的, 得转一下
|
||||||
|
int ep = comic["ep"] == 0 ? 1 : comic["ep"];
|
||||||
|
String title = comic["title"];
|
||||||
|
String epName = "";
|
||||||
|
ImageFavoritesComic? tempComic = imageFavoritesComicList
|
||||||
|
.firstWhereOrNull((e) => e.id == id && e.sourceKey == sourceKey);
|
||||||
|
ImageFavorite curImageFavorite =
|
||||||
|
ImageFavorite(page, "", null, "", id, ep, sourceKey, epName);
|
||||||
|
if (tempComic == null) {
|
||||||
|
tempComic = ImageFavoritesComic(id, [], title, sourceKey, [], [],
|
||||||
|
DateTime.now(), "", {}, "", 1);
|
||||||
|
tempComic.imageFavoritesEp = [
|
||||||
|
ImageFavoritesEp("", ep, [curImageFavorite], epName, 1)
|
||||||
|
];
|
||||||
|
imageFavoritesComicList.add(tempComic);
|
||||||
|
} else {
|
||||||
|
ImageFavoritesEp? tempEp =
|
||||||
|
tempComic.imageFavoritesEp.firstWhereOrNull((e) => e.ep == ep);
|
||||||
|
if (tempEp == null) {
|
||||||
|
tempComic.imageFavoritesEp
|
||||||
|
.add(ImageFavoritesEp("", ep, [curImageFavorite], epName, 1));
|
||||||
|
} else {
|
||||||
|
// 如果已经有这个page了, 就不添加了
|
||||||
|
if (tempEp.imageFavorites
|
||||||
|
.firstWhereOrNull((e) => e.page == page) ==
|
||||||
|
null) {
|
||||||
|
tempEp.imageFavorites.add(curImageFavorite);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (var temp in imageFavoritesComicList) {
|
||||||
|
ImageFavoriteManager().addOrUpdateOrDelete(
|
||||||
|
temp,
|
||||||
|
temp == imageFavoritesComicList.last,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e, stack) {
|
||||||
|
Log.error("Import Data", "Failed to import history: $e", stack);
|
||||||
|
} finally {
|
||||||
db.dispose();
|
db.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -95,6 +95,8 @@ extension StringExt on String{
|
|||||||
bool get isURL => _isURL();
|
bool get isURL => _isURL();
|
||||||
|
|
||||||
bool get isNum => double.tryParse(this) != null;
|
bool get isNum => double.tryParse(this) != null;
|
||||||
|
|
||||||
|
bool get isInt => int.tryParse(this) != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class ListOrNull{
|
abstract class ListOrNull{
|
||||||
|
@@ -376,7 +376,6 @@ class _IOOverrides extends IOOverrides {
|
|||||||
return super.createFile(path);
|
return super.createFile(path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
T overrideIO<T>(T Function() f) {
|
T overrideIO<T>(T Function() f) {
|
||||||
|
@@ -1147,4 +1147,4 @@ packages:
|
|||||||
version: "0.0.6"
|
version: "0.0.6"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.6.0 <4.0.0"
|
dart: ">=3.6.0 <4.0.0"
|
||||||
flutter: ">=3.27.1"
|
flutter: ">=3.27.1"
|
Reference in New Issue
Block a user