mirror of
https://github.com/venera-app/venera.git
synced 2025-09-29 00:37:24 +00:00
Compare commits
23 Commits
Author | SHA1 | Date | |
---|---|---|---|
b8bdda16c6 | |||
1a50b8bc27 | |||
546f619063 | |||
![]() |
0e831468ee | ||
a4cc0a3af2 | |||
80811bf12d | |||
21bf9d72c0 | |||
035a84380c | |||
5ddb6f47ca | |||
c1672d01f8 | |||
![]() |
66ebdb03b1 | ||
df2ba6efd1 | |||
705c448cfe | |||
a711335012 | |||
305ef9263d | |||
f8b8811aaa | |||
a868fe3fff | |||
873cbd779e | |||
d56e3fd59f | |||
d96b36414d | |||
b30bd11d1a | |||
![]() |
72507d907a | ||
![]() |
06094fc5fc |
45
.github/workflows/main.yml
vendored
45
.github/workflows/main.yml
vendored
@@ -2,6 +2,9 @@ name: Build ALL
|
|||||||
run-name: Build ALL
|
run-name: Build ALL
|
||||||
on:
|
on:
|
||||||
workflow_dispatch: {}
|
workflow_dispatch: {}
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
Build_MacOS:
|
Build_MacOS:
|
||||||
runs-on: macos-15
|
runs-on: macos-15
|
||||||
@@ -139,3 +142,45 @@ jobs:
|
|||||||
name: arch_build
|
name: arch_build
|
||||||
path: build/linux/arch/
|
path: build/linux/arch/
|
||||||
|
|
||||||
|
Release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [Build_MacOS, Build_IOS, Build_Android, Build_Windows, Build_Linux]
|
||||||
|
if: github.event_name == 'release' # 仅在 push 事件时执行
|
||||||
|
steps:
|
||||||
|
- uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: venera.dmg
|
||||||
|
path: outputs
|
||||||
|
- uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: app-ios.ipa
|
||||||
|
path: outputs
|
||||||
|
- uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: apks
|
||||||
|
path: outputs
|
||||||
|
- uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: windows_build
|
||||||
|
path: outputs
|
||||||
|
- uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deb_build
|
||||||
|
path: outputs
|
||||||
|
- uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: arch_build
|
||||||
|
path: outputs
|
||||||
|
- uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
tag_name: ${{ github.ref_name }}
|
||||||
|
files: |
|
||||||
|
outputs/*.ipa
|
||||||
|
outputs/*.dmg
|
||||||
|
outputs/*.apk
|
||||||
|
outputs/*.zip
|
||||||
|
outputs/*.exe
|
||||||
|
outputs/*.deb
|
||||||
|
outputs/*.zst
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
@@ -9,6 +9,7 @@ import android.net.Uri
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
|
import android.util.Log
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import androidx.activity.result.ActivityResultCallback
|
import androidx.activity.result.ActivityResultCallback
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
@@ -324,8 +325,25 @@ class MainActivity : FlutterFragmentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// use copy method
|
// use copy method
|
||||||
val filePath = FileUtils.getPathFromCopyOfFileFromUri(this, uri)
|
val tmp = File(cacheDir, fileName)
|
||||||
result.success(filePath)
|
if(tmp.exists()) {
|
||||||
|
tmp.delete()
|
||||||
|
}
|
||||||
|
Log.i("Venera", "copy file (${fileName}) to ${tmp.absolutePath}")
|
||||||
|
Thread {
|
||||||
|
try {
|
||||||
|
contentResolver.openInputStream(uri)?.use { input ->
|
||||||
|
FileOutputStream(tmp).use { output ->
|
||||||
|
input.copyTo(output, bufferSize = DEFAULT_BUFFER_SIZE)
|
||||||
|
output.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.success(tmp.absolutePath)
|
||||||
|
}
|
||||||
|
catch (e: Exception) {
|
||||||
|
result.error("copy error", e.message, null)
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -877,6 +877,8 @@ function Comic({id, title, subtitle, subTitle, cover, tags, description, maxPage
|
|||||||
/**
|
/**
|
||||||
* Create a comic details object
|
* Create a comic details object
|
||||||
* @param title {string}
|
* @param title {string}
|
||||||
|
* @param subtitle {string}
|
||||||
|
* @param subTitle {string} - equal to subtitle
|
||||||
* @param cover {string}
|
* @param cover {string}
|
||||||
* @param description {string?}
|
* @param description {string?}
|
||||||
* @param tags {Map<string, string[]> | {} | null | undefined}
|
* @param tags {Map<string, string[]> | {} | null | undefined}
|
||||||
@@ -897,8 +899,9 @@ function Comic({id, title, subtitle, subTitle, cover, tags, description, maxPage
|
|||||||
* @param comments {Comment[]?}- `since 1.0.7` App will display comments in the details page.
|
* @param comments {Comment[]?}- `since 1.0.7` App will display comments in the details page.
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
function ComicDetails({title, cover, description, tags, chapters, isFavorite, subId, thumbnails, recommend, commentCount, likesCount, isLiked, uploader, updateTime, uploadTime, url, stars, maxPage, comments}) {
|
function ComicDetails({title, subtitle, subTitle, cover, description, tags, chapters, isFavorite, subId, thumbnails, recommend, commentCount, likesCount, isLiked, uploader, updateTime, uploadTime, url, stars, maxPage, comments}) {
|
||||||
this.title = title;
|
this.title = title;
|
||||||
|
this.subtitle = subtitle ?? subTitle;
|
||||||
this.cover = cover;
|
this.cover = cover;
|
||||||
this.description = description;
|
this.description = description;
|
||||||
this.tags = tags;
|
this.tags = tags;
|
||||||
|
@@ -105,6 +105,7 @@
|
|||||||
"Continuous (Right to Left)": "连续(从右到左)",
|
"Continuous (Right to Left)": "连续(从右到左)",
|
||||||
"Continuous (Top to Bottom)": "连续(从上到下)",
|
"Continuous (Top to Bottom)": "连续(从上到下)",
|
||||||
"Auto page turning interval": "自动翻页间隔",
|
"Auto page turning interval": "自动翻页间隔",
|
||||||
|
"The number of pic in screen (Only Gallery Mode)": "同屏幕图片数量(仅画廊模式)",
|
||||||
"Theme Mode": "主题模式",
|
"Theme Mode": "主题模式",
|
||||||
"System": "系统",
|
"System": "系统",
|
||||||
"Light": "浅色",
|
"Light": "浅色",
|
||||||
@@ -152,7 +153,7 @@
|
|||||||
"If the directory contains a file named 'cover.*', it will be used as the cover image. Otherwise the first image will be used." : "如果目录包含一个名为'cover.*'的文件,它将被用作封面图片。否则将使用第一张图片。",
|
"If the directory contains a file named 'cover.*', it will be used as the cover image. Otherwise the first image will be used." : "如果目录包含一个名为'cover.*'的文件,它将被用作封面图片。否则将使用第一张图片。",
|
||||||
"The directory name will be used as the comic title. And the name of chapter directories will be used as the chapter titles.\n" : "目录名称将被用作漫画标题。章节目录的名称将被用作章节标题。\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" : "目录名称将被用作漫画标题。章节目录的名称将被用作章节标题。\n",
|
||||||
"Export as cbz": "导出为cbz",
|
"Export as cbz": "导出为cbz",
|
||||||
"Select a cbz file." : "选择一个cbz文件",
|
"Select a cbz/zip file." : "选择一个cbz/zip文件",
|
||||||
"A cbz file" : "一个cbz文件",
|
"A cbz file" : "一个cbz文件",
|
||||||
"Fullscreen": "全屏",
|
"Fullscreen": "全屏",
|
||||||
"Exit": "退出",
|
"Exit": "退出",
|
||||||
@@ -244,7 +245,8 @@
|
|||||||
"Deleted @a favorite items.": "已删除 @a 条无效收藏",
|
"Deleted @a favorite items.": "已删除 @a 条无效收藏",
|
||||||
"New version available": "有新版本可用",
|
"New version available": "有新版本可用",
|
||||||
"A new version is available. Do you want to update now?" : "有新版本可用。您要现在更新吗?",
|
"A new version is available. Do you want to update now?" : "有新版本可用。您要现在更新吗?",
|
||||||
"No new version available": "没有新版本可用"
|
"No new version available": "没有新版本可用",
|
||||||
|
"Export as pdf": "导出为pdf"
|
||||||
},
|
},
|
||||||
"zh_TW": {
|
"zh_TW": {
|
||||||
"Home": "首頁",
|
"Home": "首頁",
|
||||||
@@ -352,6 +354,7 @@
|
|||||||
"Continuous (Right to Left)": "連續(從右到左)",
|
"Continuous (Right to Left)": "連續(從右到左)",
|
||||||
"Continuous (Top to Bottom)": "連續(從上到下)",
|
"Continuous (Top to Bottom)": "連續(從上到下)",
|
||||||
"Auto page turning interval": "自動翻頁間隔",
|
"Auto page turning interval": "自動翻頁間隔",
|
||||||
|
"The number of pic in screen (Only Gallery Mode)": "同螢幕圖片數量(僅畫廊模式)",
|
||||||
"Theme Mode": "主題模式",
|
"Theme Mode": "主題模式",
|
||||||
"System": "系統",
|
"System": "系統",
|
||||||
"Light": "浅色",
|
"Light": "浅色",
|
||||||
@@ -399,7 +402,7 @@
|
|||||||
"If the directory contains a file named 'cover.*', it will be used as the cover image. Otherwise the first image will be used." : "如果目錄包含一個名為'cover.*'的文件,它將被用作封面圖片。否則將使用第一張圖片。",
|
"If the directory contains a file named 'cover.*', it will be used as the cover image. Otherwise the first image will be used." : "如果目錄包含一個名為'cover.*'的文件,它將被用作封面圖片。否則將使用第一張圖片。",
|
||||||
"The directory name will be used as the comic title. And the name of chapter directories will be used as the chapter titles.\n" : "目錄名稱將被用作漫畫標題。章節目錄的名稱將被用作章節標題。\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" : "目錄名稱將被用作漫畫標題。章節目錄的名稱將被用作章節標題。\n",
|
||||||
"Export as cbz": "匯出為cbz",
|
"Export as cbz": "匯出為cbz",
|
||||||
"Select a cbz file." : "選擇一個cbz文件",
|
"Select a cbz/zip file." : "選擇一個cbz/zip文件",
|
||||||
"A cbz file" : "一個cbz文件",
|
"A cbz file" : "一個cbz文件",
|
||||||
"Fullscreen": "全螢幕",
|
"Fullscreen": "全螢幕",
|
||||||
"Exit": "退出",
|
"Exit": "退出",
|
||||||
@@ -491,6 +494,7 @@
|
|||||||
"Deleted @a favorite items.": "已刪除 @a 條無效收藏",
|
"Deleted @a favorite items.": "已刪除 @a 條無效收藏",
|
||||||
"New version available": "有新版本可用",
|
"New version available": "有新版本可用",
|
||||||
"A new version is available. Do you want to update now?" : "有新版本可用。您要現在更新嗎?",
|
"A new version is available. Do you want to update now?" : "有新版本可用。您要現在更新嗎?",
|
||||||
"No new version available": "沒有新版本可用"
|
"No new version available": "沒有新版本可用",
|
||||||
|
"Export as pdf": "匯出為pdf"
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -332,7 +332,7 @@ class _FilledTabBarState extends State<FilledTabBar> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AnimatedBuilder(
|
return AnimatedBuilder(
|
||||||
animation: _controller,
|
animation: _controller.animation ?? _controller,
|
||||||
builder: buildTabBar,
|
builder: buildTabBar,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -427,7 +427,7 @@ class _FilledTabBarState extends State<FilledTabBar> {
|
|||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
child: DefaultTextStyle(
|
child: DefaultTextStyle(
|
||||||
style: DefaultTextStyle.of(context).style.copyWith(
|
style: DefaultTextStyle.of(context).style.copyWith(
|
||||||
color: i == _controller.index
|
color: i == _controller.animation?.value.round()
|
||||||
? context.colorScheme.primary
|
? context.colorScheme.primary
|
||||||
: context.colorScheme.onSurface,
|
: context.colorScheme.onSurface,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
|
@@ -163,7 +163,9 @@ class ComicTile extends StatelessWidget {
|
|||||||
Widget buildImage(BuildContext context) {
|
Widget buildImage(BuildContext context) {
|
||||||
ImageProvider image;
|
ImageProvider image;
|
||||||
if (comic is LocalComic) {
|
if (comic is LocalComic) {
|
||||||
image = FileImage((comic as LocalComic).coverFile);
|
image = LocalComicImageProvider(comic as LocalComic);
|
||||||
|
} else if (comic is History) {
|
||||||
|
image = HistoryImageProvider(comic as History);
|
||||||
} else if (comic.sourceKey == 'local') {
|
} else if (comic.sourceKey == 'local') {
|
||||||
var localComic = LocalManager().find(comic.id, ComicType.local);
|
var localComic = LocalManager().find(comic.id, ComicType.local);
|
||||||
if (localComic == null) {
|
if (localComic == null) {
|
||||||
@@ -829,6 +831,7 @@ class ComicList extends StatefulWidget {
|
|||||||
this.trailingSliver,
|
this.trailingSliver,
|
||||||
this.errorLeading,
|
this.errorLeading,
|
||||||
this.menuBuilder,
|
this.menuBuilder,
|
||||||
|
this.controller,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Future<Res<List<Comic>>> Function(int page)? loadPage;
|
final Future<Res<List<Comic>>> Function(int page)? loadPage;
|
||||||
@@ -843,6 +846,8 @@ class ComicList extends StatefulWidget {
|
|||||||
|
|
||||||
final List<MenuEntry> Function(Comic)? menuBuilder;
|
final List<MenuEntry> Function(Comic)? menuBuilder;
|
||||||
|
|
||||||
|
final ScrollController? controller;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ComicList> createState() => ComicListState();
|
State<ComicList> createState() => ComicListState();
|
||||||
}
|
}
|
||||||
@@ -1064,6 +1069,7 @@ class ComicListState extends State<ComicList> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return SmoothCustomScrollView(
|
return SmoothCustomScrollView(
|
||||||
|
controller: widget.controller,
|
||||||
slivers: [
|
slivers: [
|
||||||
if (widget.leadingSliver != null) widget.leadingSliver!,
|
if (widget.leadingSliver != null) widget.leadingSliver!,
|
||||||
if (_maxPage != 1) _buildSliverPageSelector(),
|
if (_maxPage != 1) _buildSliverPageSelector(),
|
||||||
|
@@ -19,13 +19,14 @@ import 'package:venera/foundation/consts.dart';
|
|||||||
import 'package:venera/foundation/favorites.dart';
|
import 'package:venera/foundation/favorites.dart';
|
||||||
import 'package:venera/foundation/history.dart';
|
import 'package:venera/foundation/history.dart';
|
||||||
import 'package:venera/foundation/image_provider/cached_image.dart';
|
import 'package:venera/foundation/image_provider/cached_image.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/local.dart';
|
import 'package:venera/foundation/local.dart';
|
||||||
import 'package:venera/foundation/res.dart';
|
import 'package:venera/foundation/res.dart';
|
||||||
import 'package:venera/network/cloudflare.dart';
|
import 'package:venera/network/cloudflare.dart';
|
||||||
import 'package:venera/pages/comic_page.dart';
|
import 'package:venera/pages/comic_page.dart';
|
||||||
import 'package:venera/pages/favorites/favorites_page.dart';
|
import 'package:venera/pages/favorites/favorites_page.dart';
|
||||||
import 'package:venera/utils/ext.dart';
|
import 'package:venera/utils/ext.dart';
|
||||||
import 'package:venera/utils/io.dart';
|
|
||||||
import 'package:venera/utils/tags_translation.dart';
|
import 'package:venera/utils/tags_translation.dart';
|
||||||
import 'package:venera/utils/translations.dart';
|
import 'package:venera/utils/translations.dart';
|
||||||
|
|
||||||
|
@@ -47,10 +47,16 @@ class NaviPane extends StatefulWidget {
|
|||||||
final GlobalKey<NavigatorState> navigatorKey;
|
final GlobalKey<NavigatorState> navigatorKey;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<NaviPane> createState() => _NaviPaneState();
|
State<NaviPane> createState() => NaviPaneState();
|
||||||
|
|
||||||
|
static NaviPaneState of(BuildContext context) {
|
||||||
|
return context.findAncestorStateOfType<NaviPaneState>()!;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _NaviPaneState extends State<NaviPane>
|
typedef NaviItemTapListener = void Function(int);
|
||||||
|
|
||||||
|
class NaviPaneState extends State<NaviPane>
|
||||||
with SingleTickerProviderStateMixin {
|
with SingleTickerProviderStateMixin {
|
||||||
late int _currentPage = widget.initialPage;
|
late int _currentPage = widget.initialPage;
|
||||||
|
|
||||||
@@ -66,6 +72,16 @@ class _NaviPaneState extends State<NaviPane>
|
|||||||
|
|
||||||
late AnimationController controller;
|
late AnimationController controller;
|
||||||
|
|
||||||
|
final _naviItemTapListeners = <NaviItemTapListener>[];
|
||||||
|
|
||||||
|
void addNaviItemTapListener(NaviItemTapListener listener) {
|
||||||
|
_naviItemTapListeners.add(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeNaviItemTapListener(NaviItemTapListener listener) {
|
||||||
|
_naviItemTapListeners.remove(listener);
|
||||||
|
}
|
||||||
|
|
||||||
static const _kBottomBarHeight = 58.0;
|
static const _kBottomBarHeight = 58.0;
|
||||||
|
|
||||||
static const _kFoldedSideBarWidth = 80.0;
|
static const _kFoldedSideBarWidth = 80.0;
|
||||||
@@ -85,9 +101,15 @@ class _NaviPaneState extends State<NaviPane>
|
|||||||
}
|
}
|
||||||
|
|
||||||
void updatePage(int index) {
|
void updatePage(int index) {
|
||||||
|
for (var listener in _naviItemTapListeners) {
|
||||||
|
listener(index);
|
||||||
|
}
|
||||||
if (widget.observer.routes.length > 1) {
|
if (widget.observer.routes.length > 1) {
|
||||||
widget.navigatorKey.currentState!.popUntil((route) => route.isFirst);
|
widget.navigatorKey.currentState!.popUntil((route) => route.isFirst);
|
||||||
}
|
}
|
||||||
|
if (currentPage == index) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
currentPage = index;
|
currentPage = index;
|
||||||
});
|
});
|
||||||
@@ -670,14 +692,14 @@ class _NaviPopScope extends StatelessWidget {
|
|||||||
class _NaviMainView extends StatefulWidget {
|
class _NaviMainView extends StatefulWidget {
|
||||||
const _NaviMainView({required this.state});
|
const _NaviMainView({required this.state});
|
||||||
|
|
||||||
final _NaviPaneState state;
|
final NaviPaneState state;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_NaviMainView> createState() => _NaviMainViewState();
|
State<_NaviMainView> createState() => _NaviMainViewState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _NaviMainViewState extends State<_NaviMainView> {
|
class _NaviMainViewState extends State<_NaviMainView> {
|
||||||
_NaviPaneState get state => widget.state;
|
NaviPaneState get state => widget.state;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
@@ -10,7 +10,7 @@ export "widget_utils.dart";
|
|||||||
export "context.dart";
|
export "context.dart";
|
||||||
|
|
||||||
class _App {
|
class _App {
|
||||||
final version = "1.0.7";
|
final version = "1.0.8";
|
||||||
|
|
||||||
bool get isAndroid => Platform.isAndroid;
|
bool get isAndroid => Platform.isAndroid;
|
||||||
|
|
||||||
|
@@ -106,6 +106,7 @@ class _Settings with ChangeNotifier {
|
|||||||
'defaultSearchTarget': null,
|
'defaultSearchTarget': null,
|
||||||
'autoPageTurningInterval': 5, // in seconds
|
'autoPageTurningInterval': 5, // in seconds
|
||||||
'readerMode': 'galleryLeftToRight', // values of [ReaderMode]
|
'readerMode': 'galleryLeftToRight', // values of [ReaderMode]
|
||||||
|
'readerScreenPicNumber': 1, // 1 - 5
|
||||||
'enableTapToTurnPages': true,
|
'enableTapToTurnPages': true,
|
||||||
'enablePageAnimation': true,
|
'enablePageAnimation': true,
|
||||||
'language': 'system', // system, zh-CN, zh-TW, en-US
|
'language': 'system', // system, zh-CN, zh-TW, en-US
|
||||||
|
@@ -172,7 +172,7 @@ class ComicDetails with HistoryMixin {
|
|||||||
|
|
||||||
ComicDetails.fromJson(Map<String, dynamic> json)
|
ComicDetails.fromJson(Map<String, dynamic> json)
|
||||||
: title = json["title"],
|
: title = json["title"],
|
||||||
subTitle = json["subTitle"],
|
subTitle = json["subtitle"],
|
||||||
cover = json["cover"],
|
cover = json["cover"],
|
||||||
description = json["description"],
|
description = json["description"],
|
||||||
tags = _generateMap(json["tags"]),
|
tags = _generateMap(json["tags"]),
|
||||||
@@ -198,7 +198,9 @@ class ComicDetails with HistoryMixin {
|
|||||||
maxPage = json["maxPage"],
|
maxPage = json["maxPage"],
|
||||||
comments = (json["comments"] as List?)
|
comments = (json["comments"] as List?)
|
||||||
?.map((e) => Comment.fromJson(e))
|
?.map((e) => Comment.fromJson(e))
|
||||||
.toList();
|
.toList(){
|
||||||
|
print(json);
|
||||||
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
return {
|
return {
|
||||||
|
@@ -2,7 +2,9 @@ import 'dart:async';
|
|||||||
|
|
||||||
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_type.dart';
|
import 'package:venera/foundation/comic_type.dart';
|
||||||
|
import 'package:venera/utils/translations.dart';
|
||||||
|
|
||||||
import 'app.dart';
|
import 'app.dart';
|
||||||
|
|
||||||
@@ -22,15 +24,18 @@ abstract mixin class HistoryMixin {
|
|||||||
HistoryType get historyType;
|
HistoryType get historyType;
|
||||||
}
|
}
|
||||||
|
|
||||||
class History {
|
class History implements Comic {
|
||||||
HistoryType type;
|
HistoryType type;
|
||||||
|
|
||||||
DateTime time;
|
DateTime time;
|
||||||
|
|
||||||
|
@override
|
||||||
String title;
|
String title;
|
||||||
|
|
||||||
|
@override
|
||||||
String subtitle;
|
String subtitle;
|
||||||
|
|
||||||
|
@override
|
||||||
String cover;
|
String cover;
|
||||||
|
|
||||||
int ep;
|
int ep;
|
||||||
@@ -44,6 +49,7 @@ class History {
|
|||||||
/// The number of episodes is 1-based.
|
/// The number of episodes is 1-based.
|
||||||
Set<int> readEpisode;
|
Set<int> readEpisode;
|
||||||
|
|
||||||
|
@override
|
||||||
int? maxPage;
|
int? maxPage;
|
||||||
|
|
||||||
History.fromModel(
|
History.fromModel(
|
||||||
@@ -137,6 +143,47 @@ class History {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(id, type);
|
int get hashCode => Object.hash(id, type);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get description {
|
||||||
|
var res = "";
|
||||||
|
if (ep >= 1) {
|
||||||
|
res += "Chapter @ep".tlParams({
|
||||||
|
"ep": ep,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (page >= 1) {
|
||||||
|
if (ep >= 1) {
|
||||||
|
res += " - ";
|
||||||
|
}
|
||||||
|
res += "Page @page".tlParams({
|
||||||
|
"page": page,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get favoriteId => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get language => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get sourceKey => type == ComicType.local
|
||||||
|
? 'local'
|
||||||
|
: type.comicSource?.key ?? "Unknown:${type.value}";
|
||||||
|
|
||||||
|
@override
|
||||||
|
double? get stars => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<String>? get tags => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class HistoryManager with ChangeNotifier {
|
class HistoryManager with ChangeNotifier {
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
import 'dart:async' show Future, StreamController, scheduleMicrotask;
|
import 'dart:async' show Future, StreamController, scheduleMicrotask;
|
||||||
import 'dart:collection';
|
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:ui' as ui show Codec;
|
import 'dart:ui' as ui show Codec;
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
@@ -108,7 +107,7 @@ abstract class BaseImageProvider<T extends BaseImageProvider<T>>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static final _cache = LinkedHashMap<String, Uint8List>();
|
static final _cache = <String, Uint8List>{};
|
||||||
|
|
||||||
static var _cacheSize = 0;
|
static var _cacheSize = 0;
|
||||||
|
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
import 'dart:async' show Future, StreamController;
|
import 'dart:async' show Future, StreamController;
|
||||||
import 'dart:io';
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:venera/network/images.dart';
|
import 'package:venera/network/images.dart';
|
||||||
@@ -22,10 +21,19 @@ class CachedImageProvider
|
|||||||
|
|
||||||
final String? cid;
|
final String? cid;
|
||||||
|
|
||||||
|
static int loadingCount = 0;
|
||||||
|
|
||||||
|
static const _kMaxLoadingCount = 8;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Uint8List> load(StreamController<ImageChunkEvent> chunkEvents) async {
|
Future<Uint8List> load(StreamController<ImageChunkEvent> chunkEvents) async {
|
||||||
|
while(loadingCount > _kMaxLoadingCount) {
|
||||||
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
}
|
||||||
|
loadingCount++;
|
||||||
|
try {
|
||||||
if(url.startsWith("file://")) {
|
if(url.startsWith("file://")) {
|
||||||
var file = openFilePlatform(url.substring(7));
|
var file = File(url.substring(7));
|
||||||
return file.readAsBytes();
|
return file.readAsBytes();
|
||||||
}
|
}
|
||||||
await for (var progress in ImageDownloader.loadThumbnail(url, sourceKey, cid)) {
|
await for (var progress in ImageDownloader.loadThumbnail(url, sourceKey, cid)) {
|
||||||
@@ -39,6 +47,10 @@ class CachedImageProvider
|
|||||||
}
|
}
|
||||||
throw "Error: Empty response body.";
|
throw "Error: Empty response body.";
|
||||||
}
|
}
|
||||||
|
finally {
|
||||||
|
loadingCount--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<CachedImageProvider> obtainKey(ImageConfiguration configuration) {
|
Future<CachedImageProvider> obtainKey(ImageConfiguration configuration) {
|
||||||
|
57
lib/foundation/image_provider/history_image_provider.dart
Normal file
57
lib/foundation/image_provider/history_image_provider.dart
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import 'dart:async' show Future, StreamController;
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:venera/foundation/local.dart';
|
||||||
|
import 'package:venera/network/images.dart';
|
||||||
|
import '../history.dart';
|
||||||
|
import 'base_image_provider.dart';
|
||||||
|
import 'history_image_provider.dart' as image_provider;
|
||||||
|
|
||||||
|
class HistoryImageProvider
|
||||||
|
extends BaseImageProvider<image_provider.HistoryImageProvider> {
|
||||||
|
/// Image provider for normal image.
|
||||||
|
///
|
||||||
|
/// [url] is the url of the image. Local file path is also supported.
|
||||||
|
const HistoryImageProvider(this.history);
|
||||||
|
|
||||||
|
final History history;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Uint8List> load(StreamController<ImageChunkEvent> chunkEvents) async {
|
||||||
|
var url = history.cover;
|
||||||
|
if (!url.contains('/')) {
|
||||||
|
var localComic = LocalManager().find(history.id, history.type);
|
||||||
|
if (localComic != null) {
|
||||||
|
return localComic.coverFile.readAsBytes();
|
||||||
|
}
|
||||||
|
var comicSource =
|
||||||
|
history.type.comicSource ?? (throw "Comic source not found.");
|
||||||
|
var comic = await comicSource.loadComicInfo!(history.id);
|
||||||
|
url = comic.data.cover;
|
||||||
|
history.cover = url;
|
||||||
|
HistoryManager().addHistory(history);
|
||||||
|
}
|
||||||
|
await for (var progress in ImageDownloader.loadThumbnail(
|
||||||
|
url,
|
||||||
|
history.type.sourceKey,
|
||||||
|
history.id,
|
||||||
|
)) {
|
||||||
|
chunkEvents.add(ImageChunkEvent(
|
||||||
|
cumulativeBytesLoaded: progress.currentBytes,
|
||||||
|
expectedTotalBytes: progress.totalBytes,
|
||||||
|
));
|
||||||
|
if (progress.imageBytes != null) {
|
||||||
|
return progress.imageBytes!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw "Error: Empty response body.";
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<HistoryImageProvider> obtainKey(ImageConfiguration configuration) {
|
||||||
|
return SynchronousFuture(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get key => "history${history.id}${history.type.value}";
|
||||||
|
}
|
66
lib/foundation/image_provider/local_comic_image.dart
Normal file
66
lib/foundation/image_provider/local_comic_image.dart
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import 'dart:async' show Future, StreamController;
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:venera/foundation/local.dart';
|
||||||
|
import 'package:venera/utils/io.dart';
|
||||||
|
import 'base_image_provider.dart';
|
||||||
|
import 'local_comic_image.dart' as image_provider;
|
||||||
|
|
||||||
|
class LocalComicImageProvider
|
||||||
|
extends BaseImageProvider<image_provider.LocalComicImageProvider> {
|
||||||
|
/// Image provider for normal image.
|
||||||
|
///
|
||||||
|
/// [url] is the url of the image. Local file path is also supported.
|
||||||
|
const LocalComicImageProvider(this.comic);
|
||||||
|
|
||||||
|
final LocalComic comic;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Uint8List> load(StreamController<ImageChunkEvent> chunkEvents) async {
|
||||||
|
File? file = comic.coverFile;
|
||||||
|
if(! await file.exists()) {
|
||||||
|
file = null;
|
||||||
|
var dir = Directory(comic.directory);
|
||||||
|
if (! await dir.exists()) {
|
||||||
|
throw "Error: Comic not found.";
|
||||||
|
}
|
||||||
|
Directory? firstDir;
|
||||||
|
await for (var entity in dir.list()) {
|
||||||
|
if(entity is File) {
|
||||||
|
if(["jpg", "jpeg", "png", "webp", "gif", "jpe", "jpeg"].contains(entity.extension)) {
|
||||||
|
file = entity;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if(entity is Directory) {
|
||||||
|
firstDir ??= entity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(file == null && firstDir != null) {
|
||||||
|
await for (var entity in firstDir.list()) {
|
||||||
|
if(entity is File) {
|
||||||
|
if(["jpg", "jpeg", "png", "webp", "gif", "jpe", "jpeg"].contains(entity.extension)) {
|
||||||
|
file = entity;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(file == null) {
|
||||||
|
throw "Error: Cover not found.";
|
||||||
|
}
|
||||||
|
var data = await file.readAsBytes();
|
||||||
|
if(data.isEmpty) {
|
||||||
|
throw "Exception: Empty file(${file.path}).";
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<LocalComicImageProvider> obtainKey(ImageConfiguration configuration) {
|
||||||
|
return SynchronousFuture(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get key => "local${comic.id}${comic.comicType.value}";
|
||||||
|
}
|
@@ -71,12 +71,12 @@ class LocalComic with HistoryMixin implements Comic {
|
|||||||
downloadedChapters = List.from(jsonDecode(row[8] as String)),
|
downloadedChapters = List.from(jsonDecode(row[8] as String)),
|
||||||
createdAt = DateTime.fromMillisecondsSinceEpoch(row[9] as int);
|
createdAt = DateTime.fromMillisecondsSinceEpoch(row[9] as int);
|
||||||
|
|
||||||
File get coverFile => openFilePlatform(FilePath.join(
|
File get coverFile => File(FilePath.join(
|
||||||
baseDir,
|
baseDir,
|
||||||
cover,
|
cover,
|
||||||
));
|
));
|
||||||
|
|
||||||
String get baseDir => directory.contains("/") ? directory : FilePath.join(LocalManager().path, directory);
|
String get baseDir => (directory.contains('/') || directory.contains('\\')) ? directory : FilePath.join(LocalManager().path, directory);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get description => "";
|
String get description => "";
|
||||||
@@ -151,6 +151,8 @@ class LocalManager with ChangeNotifier {
|
|||||||
/// path to the directory where all the comics are stored
|
/// path to the directory where all the comics are stored
|
||||||
late String path;
|
late String path;
|
||||||
|
|
||||||
|
Directory get directory => Directory(path);
|
||||||
|
|
||||||
// return error message if failed
|
// return error message if failed
|
||||||
Future<String?> setNewPath(String newPath) async {
|
Future<String?> setNewPath(String newPath) async {
|
||||||
var newDir = Directory(newPath);
|
var newDir = Directory(newPath);
|
||||||
@@ -162,7 +164,7 @@ class LocalManager with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await copyDirectoryIsolate(
|
await copyDirectoryIsolate(
|
||||||
Directory(path),
|
directory,
|
||||||
newDir,
|
newDir,
|
||||||
);
|
);
|
||||||
await File(FilePath.join(App.dataPath, 'local_path')).writeAsString(newPath);
|
await File(FilePath.join(App.dataPath, 'local_path')).writeAsString(newPath);
|
||||||
@@ -170,7 +172,7 @@ class LocalManager with ChangeNotifier {
|
|||||||
Log.error("IO", e, s);
|
Log.error("IO", e, s);
|
||||||
return e.toString();
|
return e.toString();
|
||||||
}
|
}
|
||||||
await Directory(path).deleteIgnoreError(recursive:true);
|
await directory.deleteContents(recursive: true);
|
||||||
path = newPath;
|
path = newPath;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -217,15 +219,15 @@ class LocalManager with ChangeNotifier {
|
|||||||
''');
|
''');
|
||||||
if (File(FilePath.join(App.dataPath, 'local_path')).existsSync()) {
|
if (File(FilePath.join(App.dataPath, 'local_path')).existsSync()) {
|
||||||
path = File(FilePath.join(App.dataPath, 'local_path')).readAsStringSync();
|
path = File(FilePath.join(App.dataPath, 'local_path')).readAsStringSync();
|
||||||
if (!Directory(path).existsSync()) {
|
if (!directory.existsSync()) {
|
||||||
path = await findDefaultPath();
|
path = await findDefaultPath();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
path = await findDefaultPath();
|
path = await findDefaultPath();
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (!Directory(path).existsSync()) {
|
if (!directory.existsSync()) {
|
||||||
await Directory(path).create();
|
await directory.create();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch(e, s) {
|
catch(e, s) {
|
||||||
@@ -354,12 +356,12 @@ class LocalManager with ChangeNotifier {
|
|||||||
throw "Invalid ep";
|
throw "Invalid ep";
|
||||||
}
|
}
|
||||||
var comic = find(id, type) ?? (throw "Comic Not Found");
|
var comic = find(id, type) ?? (throw "Comic Not Found");
|
||||||
var directory = openDirectoryPlatform(comic.baseDir);
|
var directory = Directory(comic.baseDir);
|
||||||
if (comic.chapters != null) {
|
if (comic.chapters != null) {
|
||||||
var cid = ep is int
|
var cid = ep is int
|
||||||
? comic.chapters!.keys.elementAt(ep - 1)
|
? comic.chapters!.keys.elementAt(ep - 1)
|
||||||
: (ep as String);
|
: (ep as String);
|
||||||
directory = openDirectoryPlatform(FilePath.join(directory.path, cid));
|
directory = Directory(FilePath.join(directory.path, cid));
|
||||||
}
|
}
|
||||||
var files = <File>[];
|
var files = <File>[];
|
||||||
await for (var entity in directory.list()) {
|
await for (var entity in directory.list()) {
|
||||||
@@ -406,10 +408,10 @@ class LocalManager with ChangeNotifier {
|
|||||||
String id, ComicType type, String name) async {
|
String id, ComicType type, String name) async {
|
||||||
var comic = find(id, type);
|
var comic = find(id, type);
|
||||||
if (comic != null) {
|
if (comic != null) {
|
||||||
return openDirectoryPlatform(FilePath.join(path, comic.directory));
|
return Directory(FilePath.join(path, comic.directory));
|
||||||
}
|
}
|
||||||
var dir = findValidDirectoryName(path, name);
|
var dir = findValidDirectoryName(path, name);
|
||||||
return openDirectoryPlatform(FilePath.join(path, dir)).create().then((value) => value);
|
return Directory(FilePath.join(path, dir)).create().then((value) => value);
|
||||||
}
|
}
|
||||||
|
|
||||||
void completeTask(DownloadTask task) {
|
void completeTask(DownloadTask task) {
|
||||||
@@ -468,7 +470,7 @@ class LocalManager with ChangeNotifier {
|
|||||||
|
|
||||||
void deleteComic(LocalComic c, [bool removeFileOnDisk = true]) {
|
void deleteComic(LocalComic c, [bool removeFileOnDisk = true]) {
|
||||||
if(removeFileOnDisk) {
|
if(removeFileOnDisk) {
|
||||||
var dir = openDirectoryPlatform(FilePath.join(path, c.directory));
|
var dir = Directory(FilePath.join(path, c.directory));
|
||||||
dir.deleteIgnoreError(recursive: true);
|
dir.deleteIgnoreError(recursive: true);
|
||||||
}
|
}
|
||||||
//Deleting a local comic means that it's nolonger available, thus both favorite and history should be deleted.
|
//Deleting a local comic means that it's nolonger available, thus both favorite and history should be deleted.
|
||||||
|
@@ -1,14 +1,18 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class SimpleController extends StateController {
|
class SimpleController extends StateController {
|
||||||
final void Function()? refresh_;
|
final void Function()? refreshFunction;
|
||||||
|
|
||||||
SimpleController({this.refresh_});
|
final Map<String, dynamic> Function()? control;
|
||||||
|
|
||||||
|
SimpleController({this.refreshFunction, this.control});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void refresh() {
|
void refresh() {
|
||||||
(refresh_ ?? super.refresh)();
|
(refreshFunction ?? super.refresh)();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> get controlMap => control?.call() ?? {};
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class StateController {
|
abstract class StateController {
|
||||||
@@ -71,8 +75,8 @@ abstract class StateController {
|
|||||||
|
|
||||||
static SimpleController putSimpleController(
|
static SimpleController putSimpleController(
|
||||||
void Function() onUpdate, Object? tag,
|
void Function() onUpdate, Object? tag,
|
||||||
{void Function()? refresh}) {
|
{void Function()? refresh, Map<String, dynamic> Function()? control}) {
|
||||||
var controller = SimpleController(refresh_: refresh);
|
var controller = SimpleController(refreshFunction: refresh, control: control);
|
||||||
controller.stateUpdaters.add(Pair(null, onUpdate));
|
controller.stateUpdaters.add(Pair(null, onUpdate));
|
||||||
_controllers.add(StateControllerWrapped(controller, false, tag));
|
_controllers.add(StateControllerWrapped(controller, false, tag));
|
||||||
return controller;
|
return controller;
|
||||||
@@ -202,6 +206,7 @@ abstract class StateWithController<T extends StatefulWidget> extends State<T> {
|
|||||||
},
|
},
|
||||||
tag,
|
tag,
|
||||||
refresh: refresh,
|
refresh: refresh,
|
||||||
|
control: () => control,
|
||||||
);
|
);
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
@@ -218,6 +223,8 @@ abstract class StateWithController<T extends StatefulWidget> extends State<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Object? get tag;
|
Object? get tag;
|
||||||
|
|
||||||
|
Map<String, dynamic> get control => {};
|
||||||
}
|
}
|
||||||
|
|
||||||
class Pair<M, V>{
|
class Pair<M, V>{
|
||||||
|
@@ -20,6 +20,7 @@ void main(List<String> args) {
|
|||||||
if (runWebViewTitleBarWidget(args)) {
|
if (runWebViewTitleBarWidget(args)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
overrideIO(() {
|
||||||
runZonedGuarded(() async {
|
runZonedGuarded(() async {
|
||||||
await Rhttp.init();
|
await Rhttp.init();
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
@@ -55,6 +56,7 @@ void main(List<String> args) {
|
|||||||
}, (error, stack) {
|
}, (error, stack) {
|
||||||
Log.error("Unhandled Exception", "$error\n$stack");
|
Log.error("Unhandled Exception", "$error\n$stack");
|
||||||
});
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class MyApp extends StatefulWidget {
|
class MyApp extends StatefulWidget {
|
||||||
|
@@ -235,21 +235,22 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (path == null) {
|
if (path == null) {
|
||||||
|
try {
|
||||||
var dir = await LocalManager().findValidDirectory(
|
var dir = await LocalManager().findValidDirectory(
|
||||||
comicId,
|
comicId,
|
||||||
comicType,
|
comicType,
|
||||||
comic!.title,
|
comic!.title,
|
||||||
);
|
);
|
||||||
if (!(await dir.exists())) {
|
if (!(await dir.exists())) {
|
||||||
try {
|
|
||||||
await dir.create();
|
await dir.create();
|
||||||
} catch (e) {
|
}
|
||||||
|
path = dir.path;
|
||||||
|
} catch (e, s) {
|
||||||
|
Log.error("Download", e.toString(), s);
|
||||||
_setError("Error: $e");
|
_setError("Error: $e");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
path = dir.path;
|
|
||||||
}
|
|
||||||
|
|
||||||
await LocalManager().saveCurrentDownloadingTasks();
|
await LocalManager().saveCurrentDownloadingTasks();
|
||||||
|
|
||||||
@@ -266,11 +267,13 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
|
|||||||
throw "Failed to download cover";
|
throw "Failed to download cover";
|
||||||
}
|
}
|
||||||
var fileType = detectFileType(data);
|
var fileType = detectFileType(data);
|
||||||
var file = File(FilePath.join(path!, "cover${fileType.ext}"));
|
var file =
|
||||||
|
File(FilePath.join(path!, "cover${fileType.ext}"));
|
||||||
file.writeAsBytesSync(data);
|
file.writeAsBytesSync(data);
|
||||||
return "file://${file.path}";
|
return "file://${file.path}";
|
||||||
});
|
});
|
||||||
if (res.error) {
|
if (res.error) {
|
||||||
|
Log.error("Download", res.errorMessage!);
|
||||||
_setError("Error: ${res.errorMessage}");
|
_setError("Error: ${res.errorMessage}");
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
@@ -294,6 +297,7 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (res.error) {
|
if (res.error) {
|
||||||
|
Log.error("Download", res.errorMessage!);
|
||||||
_setError("Error: ${res.errorMessage}");
|
_setError("Error: ${res.errorMessage}");
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
@@ -323,6 +327,7 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (res.error) {
|
if (res.error) {
|
||||||
|
Log.error("Download", res.errorMessage!);
|
||||||
_setError("Error: ${res.errorMessage}");
|
_setError("Error: ${res.errorMessage}");
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
@@ -347,6 +352,7 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (task.error != null) {
|
if (task.error != null) {
|
||||||
|
Log.error("Download", task.error.toString());
|
||||||
_setError("Error: ${task.error}");
|
_setError("Error: ${task.error}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -375,7 +381,6 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
|
|||||||
_message = message;
|
_message = message;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
stopRecorder();
|
stopRecorder();
|
||||||
Log.error("Download", message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -448,7 +453,8 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
|
|||||||
}).toList(),
|
}).toList(),
|
||||||
directory: Directory(path!).name,
|
directory: Directory(path!).name,
|
||||||
chapters: comic!.chapters,
|
chapters: comic!.chapters,
|
||||||
cover: File(_cover!.split("file://").last).uri.pathSegments.last,
|
cover:
|
||||||
|
File(_cover!.split("file://").last).name,
|
||||||
comicType: ComicType(source.key.hashCode),
|
comicType: ComicType(source.key.hashCode),
|
||||||
downloadedChapters: chapters ?? [],
|
downloadedChapters: chapters ?? [],
|
||||||
createdAt: DateTime.now(),
|
createdAt: DateTime.now(),
|
||||||
@@ -726,8 +732,7 @@ class ArchiveDownloadTask extends DownloadTask {
|
|||||||
isDownloaded = status.isFinished;
|
isDownloaded = status.isFinished;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
} catch (e) {
|
||||||
catch(e) {
|
|
||||||
_setError("Error: $e");
|
_setError("Error: $e");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@@ -223,7 +223,7 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
children: [
|
children: [
|
||||||
SelectableText(comic.title, style: ts.s18),
|
SelectableText(comic.title, style: ts.s18),
|
||||||
if (comic.subTitle != null)
|
if (comic.subTitle != null)
|
||||||
SelectableText(comic.subTitle!, style: ts.s14),
|
SelectableText(comic.subTitle!, style: ts.s14).paddingVertical(4),
|
||||||
Text(
|
Text(
|
||||||
(ComicSource.find(comic.sourceKey)?.name) ?? '',
|
(ComicSource.find(comic.sourceKey)?.name) ?? '',
|
||||||
style: ts.s12,
|
style: ts.s12,
|
||||||
@@ -288,8 +288,7 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
onLongPressed: quickFavorite,
|
onLongPressed: quickFavorite,
|
||||||
iconColor: context.useTextColor(Colors.purple),
|
iconColor: context.useTextColor(Colors.purple),
|
||||||
),
|
),
|
||||||
if (comicSource.commentsLoader != null &&
|
if (comicSource.commentsLoader != null)
|
||||||
(comic.comments == null || comic.comments!.isEmpty))
|
|
||||||
_ActionButton(
|
_ActionButton(
|
||||||
icon: const Icon(Icons.comment),
|
icon: const Icon(Icons.comment),
|
||||||
text: (comic.commentsCount ?? 'Comments'.tl).toString(),
|
text: (comic.commentsCount ?? 'Comments'.tl).toString(),
|
||||||
|
@@ -46,6 +46,18 @@ class _ExplorePageState extends State<ExplorePage>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onNaviItemTapped(int index) {
|
||||||
|
if (index == 2) {
|
||||||
|
int page = controller.index;
|
||||||
|
String currentPageId = pages[page];
|
||||||
|
StateController.find<SimpleController>(tag: currentPageId)
|
||||||
|
.control!()['toTop']
|
||||||
|
?.call();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NaviPaneState? naviPane;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
pages = List<String>.from(appdata.settings["explore_pages"]);
|
pages = List<String>.from(appdata.settings["explore_pages"]);
|
||||||
@@ -59,13 +71,21 @@ class _ExplorePageState extends State<ExplorePage>
|
|||||||
vsync: this,
|
vsync: this,
|
||||||
);
|
);
|
||||||
appdata.settings.addListener(onSettingsChanged);
|
appdata.settings.addListener(onSettingsChanged);
|
||||||
|
NaviPane.of(context).addNaviItemTapListener(onNaviItemTapped);
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
naviPane = NaviPane.of(context);
|
||||||
|
super.didChangeDependencies();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
controller.dispose();
|
controller.dispose();
|
||||||
appdata.settings.removeListener(onSettingsChanged);
|
appdata.settings.removeListener(onSettingsChanged);
|
||||||
|
naviPane?.removeNaviItemTapListener(onNaviItemTapped);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,6 +252,8 @@ class _SingleExplorePageState extends StateWithController<_SingleExplorePage>
|
|||||||
|
|
||||||
bool _wantKeepAlive = true;
|
bool _wantKeepAlive = true;
|
||||||
|
|
||||||
|
var scrollController = ScrollController();
|
||||||
|
|
||||||
void onSettingsChanged() {
|
void onSettingsChanged() {
|
||||||
var explorePages = appdata.settings["explore_pages"];
|
var explorePages = appdata.settings["explore_pages"];
|
||||||
if (!explorePages.contains(widget.title)) {
|
if (!explorePages.contains(widget.title)) {
|
||||||
@@ -274,6 +296,7 @@ class _SingleExplorePageState extends StateWithController<_SingleExplorePage>
|
|||||||
data,
|
data,
|
||||||
comicSourceKey,
|
comicSourceKey,
|
||||||
key: ValueKey(key),
|
key: ValueKey(key),
|
||||||
|
controller: scrollController,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return const Center(
|
return const Center(
|
||||||
@@ -287,6 +310,7 @@ class _SingleExplorePageState extends StateWithController<_SingleExplorePage>
|
|||||||
loadPage: data.loadPage,
|
loadPage: data.loadPage,
|
||||||
loadNext: data.loadNext,
|
loadNext: data.loadNext,
|
||||||
key: ValueKey(key),
|
key: ValueKey(key),
|
||||||
|
controller: scrollController,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,6 +347,7 @@ class _SingleExplorePageState extends StateWithController<_SingleExplorePage>
|
|||||||
|
|
||||||
Widget buildPage() {
|
Widget buildPage() {
|
||||||
return SmoothCustomScrollView(
|
return SmoothCustomScrollView(
|
||||||
|
controller: scrollController,
|
||||||
slivers: _buildPage().toList(),
|
slivers: _buildPage().toList(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -352,15 +377,30 @@ class _SingleExplorePageState extends StateWithController<_SingleExplorePage>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool get wantKeepAlive => _wantKeepAlive;
|
bool get wantKeepAlive => _wantKeepAlive;
|
||||||
|
|
||||||
|
void toTop() {
|
||||||
|
if (scrollController.hasClients) {
|
||||||
|
scrollController.animateTo(
|
||||||
|
scrollController.position.minScrollExtent,
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> get control => {"toTop": toTop};
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MixedExplorePage extends StatefulWidget {
|
class _MixedExplorePage extends StatefulWidget {
|
||||||
const _MixedExplorePage(this.data, this.sourceKey, {super.key});
|
const _MixedExplorePage(this.data, this.sourceKey, {super.key, this.controller});
|
||||||
|
|
||||||
final ExplorePageData data;
|
final ExplorePageData data;
|
||||||
|
|
||||||
final String sourceKey;
|
final String sourceKey;
|
||||||
|
|
||||||
|
final ScrollController? controller;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_MixedExplorePage> createState() => _MixedExplorePageState();
|
State<_MixedExplorePage> createState() => _MixedExplorePageState();
|
||||||
}
|
}
|
||||||
@@ -394,6 +434,7 @@ class _MixedExplorePageState
|
|||||||
@override
|
@override
|
||||||
Widget buildContent(BuildContext context, List<Object> data) {
|
Widget buildContent(BuildContext context, List<Object> data) {
|
||||||
return SmoothCustomScrollView(
|
return SmoothCustomScrollView(
|
||||||
|
controller: widget.controller,
|
||||||
slivers: [
|
slivers: [
|
||||||
...buildSlivers(context, data),
|
...buildSlivers(context, data),
|
||||||
if (haveNextPage) const ListLoadingIndicator().toSliver()
|
if (haveNextPage) const ListLoadingIndicator().toSliver()
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
import 'package:venera/foundation/appdata.dart';
|
||||||
|
|
||||||
part of 'favorites_page.dart';
|
part of 'favorites_page.dart';
|
||||||
|
|
||||||
/// Open a dialog to create a new favorite folder.
|
/// Open a dialog to create a new favorite folder.
|
||||||
@@ -83,7 +85,7 @@ void addFavorite(Comic comic) {
|
|||||||
showDialog(
|
showDialog(
|
||||||
context: App.rootContext,
|
context: App.rootContext,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
String? selectedFolder;
|
String? selectedFolder = appdata.settings['quickFavorite'];
|
||||||
|
|
||||||
return StatefulBuilder(builder: (context, setState) {
|
return StatefulBuilder(builder: (context, setState) {
|
||||||
return ContentDialog(
|
return ContentDialog(
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_reorderable_grid_view/widgets/reorderable_builder.dart';
|
import 'package:flutter_reorderable_grid_view/widgets/reorderable_builder.dart';
|
||||||
import 'package:venera/components/components.dart';
|
import 'package:venera/components/components.dart';
|
||||||
@@ -11,9 +10,7 @@ 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/consts.dart';
|
||||||
import 'package:venera/foundation/favorites.dart';
|
import 'package:venera/foundation/favorites.dart';
|
||||||
import 'package:venera/foundation/local.dart';
|
|
||||||
import 'package:venera/foundation/res.dart';
|
import 'package:venera/foundation/res.dart';
|
||||||
import 'package:venera/network/download.dart';
|
|
||||||
import 'package:venera/pages/comic_page.dart';
|
import 'package:venera/pages/comic_page.dart';
|
||||||
import 'package:venera/utils/io.dart';
|
import 'package:venera/utils/io.dart';
|
||||||
import 'package:venera/utils/translations.dart';
|
import 'package:venera/utils/translations.dart';
|
||||||
|
@@ -78,33 +78,7 @@ class _HistoryPageState extends State<HistoryPage> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
SliverGridComics(
|
SliverGridComics(
|
||||||
comics: comics.map(
|
comics: comics,
|
||||||
(e) {
|
|
||||||
var cover = e.cover;
|
|
||||||
if (!cover.isURL) {
|
|
||||||
var localComic = LocalManager().find(
|
|
||||||
e.id,
|
|
||||||
e.type,
|
|
||||||
);
|
|
||||||
if(localComic != null) {
|
|
||||||
cover = "file://${localComic.coverFile.path}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Comic(
|
|
||||||
e.title,
|
|
||||||
cover,
|
|
||||||
e.id,
|
|
||||||
e.subtitle,
|
|
||||||
null,
|
|
||||||
getDescription(e),
|
|
||||||
e.type == ComicType.local
|
|
||||||
? 'local'
|
|
||||||
: e.type.comicSource?.key ?? "Unknown:${e.type.value}",
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
).toList(),
|
|
||||||
badgeBuilder: (c) {
|
badgeBuilder: (c) {
|
||||||
return ComicSource.find(c.sourceKey)?.name;
|
return ComicSource.find(c.sourceKey)?.name;
|
||||||
},
|
},
|
||||||
|
@@ -7,6 +7,8 @@ import 'package:venera/foundation/consts.dart';
|
|||||||
import 'package:venera/foundation/favorites.dart';
|
import 'package:venera/foundation/favorites.dart';
|
||||||
import 'package:venera/foundation/history.dart';
|
import 'package:venera/foundation/history.dart';
|
||||||
import 'package:venera/foundation/image_provider/cached_image.dart';
|
import 'package:venera/foundation/image_provider/cached_image.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/local.dart';
|
import 'package:venera/foundation/local.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';
|
||||||
@@ -264,21 +266,6 @@ class _HistoryState extends State<_History> {
|
|||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
itemCount: history.length,
|
itemCount: history.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
var cover = history[index].cover;
|
|
||||||
ImageProvider imageProvider = CachedImageProvider(
|
|
||||||
cover,
|
|
||||||
sourceKey: history[index].type.comicSource?.key,
|
|
||||||
cid: history[index].id,
|
|
||||||
);
|
|
||||||
if (!cover.isURL) {
|
|
||||||
var localComic = LocalManager().find(
|
|
||||||
history[index].id,
|
|
||||||
history[index].type,
|
|
||||||
);
|
|
||||||
if (localComic != null) {
|
|
||||||
imageProvider = FileImage(localComic.coverFile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context.to(
|
context.to(
|
||||||
@@ -301,7 +288,7 @@ class _HistoryState extends State<_History> {
|
|||||||
),
|
),
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
child: AnimatedImage(
|
child: AnimatedImage(
|
||||||
image: imageProvider,
|
image: HistoryImageProvider(history[index]),
|
||||||
width: 96,
|
width: 96,
|
||||||
height: 128,
|
height: 128,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
@@ -418,8 +405,8 @@ class _LocalState extends State<_Local> {
|
|||||||
),
|
),
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
child: AnimatedImage(
|
child: AnimatedImage(
|
||||||
image: FileImage(
|
image: LocalComicImageProvider(
|
||||||
local[index].coverFile,
|
local[index],
|
||||||
),
|
),
|
||||||
width: 96,
|
width: 96,
|
||||||
height: 128,
|
height: 128,
|
||||||
@@ -511,7 +498,7 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> {
|
|||||||
String info = [
|
String info = [
|
||||||
"Select a directory which contains the comic files.".tl,
|
"Select a directory which contains the comic files.".tl,
|
||||||
"Select a directory which contains the comic directories.".tl,
|
"Select a directory which contains the comic directories.".tl,
|
||||||
"Select a cbz file.".tl,
|
"Select a cbz/zip file.".tl,
|
||||||
"Select an EhViewer database and a download folder.".tl
|
"Select an EhViewer database and a download folder.".tl
|
||||||
][type];
|
][type];
|
||||||
List<String> importMethods = [
|
List<String> importMethods = [
|
||||||
|
@@ -4,9 +4,11 @@ import 'package:venera/foundation/app.dart';
|
|||||||
import 'package:venera/foundation/appdata.dart';
|
import 'package:venera/foundation/appdata.dart';
|
||||||
import 'package:venera/foundation/comic_source/comic_source.dart';
|
import 'package:venera/foundation/comic_source/comic_source.dart';
|
||||||
import 'package:venera/foundation/local.dart';
|
import 'package:venera/foundation/local.dart';
|
||||||
|
import 'package:venera/foundation/log.dart';
|
||||||
import 'package:venera/pages/downloading_page.dart';
|
import 'package:venera/pages/downloading_page.dart';
|
||||||
import 'package:venera/utils/cbz.dart';
|
import 'package:venera/utils/cbz.dart';
|
||||||
import 'package:venera/utils/io.dart';
|
import 'package:venera/utils/io.dart';
|
||||||
|
import 'package:venera/utils/pdf.dart';
|
||||||
import 'package:venera/utils/translations.dart';
|
import 'package:venera/utils/translations.dart';
|
||||||
|
|
||||||
class LocalComicsPage extends StatefulWidget {
|
class LocalComicsPage extends StatefulWidget {
|
||||||
@@ -299,8 +301,7 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
|
|||||||
return ContentDialog(
|
return ContentDialog(
|
||||||
title: "Delete".tl,
|
title: "Delete".tl,
|
||||||
content: CheckboxListTile(
|
content: CheckboxListTile(
|
||||||
title:
|
title: Text("Also remove files on disk".tl),
|
||||||
Text("Also remove files on disk".tl),
|
|
||||||
value: removeComicFile,
|
value: removeComicFile,
|
||||||
onChanged: (v) {
|
onChanged: (v) {
|
||||||
state(() {
|
state(() {
|
||||||
@@ -361,6 +362,34 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
|
|||||||
}
|
}
|
||||||
controller.close();
|
controller.close();
|
||||||
}),
|
}),
|
||||||
|
if (!multiSelectMode)
|
||||||
|
MenuEntry(
|
||||||
|
icon: Icons.picture_as_pdf_outlined,
|
||||||
|
text: "Export as pdf".tl,
|
||||||
|
onClick: () async {
|
||||||
|
var cache = FilePath.join(App.cachePath, 'temp.pdf');
|
||||||
|
var controller = showLoadingDialog(
|
||||||
|
context,
|
||||||
|
allowCancel: false,
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await createPdfFromComicIsolate(
|
||||||
|
comic: c as LocalComic,
|
||||||
|
savePath: cache,
|
||||||
|
);
|
||||||
|
await saveFile(
|
||||||
|
file: File(cache),
|
||||||
|
filename: "${c.title}.pdf",
|
||||||
|
);
|
||||||
|
} catch (e, s) {
|
||||||
|
Log.error("PDF Export", e, s);
|
||||||
|
context.showMessage(message: e.toString());
|
||||||
|
} finally {
|
||||||
|
controller.close();
|
||||||
|
File(cache).deleteIgnoreError();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@@ -103,6 +103,9 @@ class _ReaderGestureDetectorState extends State<_ReaderGestureDetector> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void onMouseWheel(bool forward) {
|
void onMouseWheel(bool forward) {
|
||||||
|
if (HardwareKeyboard.instance.isControlPressed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (context.reader.mode.key.startsWith('gallery')) {
|
if (context.reader.mode.key.startsWith('gallery')) {
|
||||||
if (forward) {
|
if (forward) {
|
||||||
if (!context.reader.toNextPage()) {
|
if (!context.reader.toNextPage()) {
|
||||||
|
@@ -83,7 +83,8 @@ class _ReaderImagesState extends State<_ReaderImages> {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
if (reader.mode.isGallery) {
|
if (reader.mode.isGallery) {
|
||||||
return _GalleryMode(key: Key(reader.mode.key));
|
return _GalleryMode(
|
||||||
|
key: Key('${reader.mode.key}_${reader.imagesPerPage}'));
|
||||||
} else {
|
} else {
|
||||||
return _ContinuousMode(key: Key(reader.mode.key));
|
return _ContinuousMode(key: Key(reader.mode.key));
|
||||||
}
|
}
|
||||||
@@ -110,6 +111,10 @@ class _GalleryModeState extends State<_GalleryMode>
|
|||||||
|
|
||||||
late _ReaderState reader;
|
late _ReaderState reader;
|
||||||
|
|
||||||
|
int get totalPages => ((reader.images!.length + reader.imagesPerPage - 1) /
|
||||||
|
reader.imagesPerPage)
|
||||||
|
.ceil();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
reader = context.reader;
|
reader = context.reader;
|
||||||
@@ -124,8 +129,14 @@ class _GalleryModeState extends State<_GalleryMode>
|
|||||||
|
|
||||||
void cache(int current) {
|
void cache(int current) {
|
||||||
for (int i = current + 1; i <= current + preCacheCount; i++) {
|
for (int i = current + 1; i <= current + preCacheCount; i++) {
|
||||||
if (i <= reader.maxPage && !cached[i]) {
|
if (i <= totalPages && !cached[i]) {
|
||||||
_precacheImage(i, context);
|
int startIndex = (i - 1) * reader.imagesPerPage;
|
||||||
|
int endIndex =
|
||||||
|
math.min(startIndex + reader.imagesPerPage, reader.images!.length);
|
||||||
|
for (int i = startIndex; i < endIndex; i++) {
|
||||||
|
precacheImage(
|
||||||
|
_createImageProviderFromKey(reader.images![i], context), context);
|
||||||
|
}
|
||||||
cached[i] = true;
|
cached[i] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -141,32 +152,43 @@ class _GalleryModeState extends State<_GalleryMode>
|
|||||||
scrollDirection: reader.mode == ReaderMode.galleryTopToBottom
|
scrollDirection: reader.mode == ReaderMode.galleryTopToBottom
|
||||||
? Axis.vertical
|
? Axis.vertical
|
||||||
: Axis.horizontal,
|
: Axis.horizontal,
|
||||||
itemCount: reader.images!.length + 2,
|
itemCount: totalPages + 2,
|
||||||
builder: (BuildContext context, int index) {
|
builder: (BuildContext context, int index) {
|
||||||
ImageProvider? imageProvider;
|
if (index == 0 || index == totalPages + 1) {
|
||||||
if (index != 0 && index != reader.images!.length + 1) {
|
|
||||||
imageProvider = _createImageProvider(index, context);
|
|
||||||
} else {
|
|
||||||
return PhotoViewGalleryPageOptions.customChild(
|
return PhotoViewGalleryPageOptions.customChild(
|
||||||
scaleStateController: PhotoViewScaleStateController(),
|
scaleStateController: PhotoViewScaleStateController(),
|
||||||
child: const SizedBox(),
|
child: const SizedBox(),
|
||||||
);
|
);
|
||||||
}
|
} else {
|
||||||
|
int pageIndex = index - 1;
|
||||||
|
int startIndex = pageIndex * reader.imagesPerPage;
|
||||||
|
int endIndex = math.min(startIndex + reader.imagesPerPage, reader.images!.length);
|
||||||
|
List<String> pageImages = reader.images!.sublist(startIndex, endIndex);
|
||||||
|
|
||||||
cached[index] = true;
|
cached[index] = true;
|
||||||
cache(index);
|
cache(index);
|
||||||
|
|
||||||
photoViewControllers[index] ??= PhotoViewController();
|
photoViewControllers[index] = PhotoViewController();
|
||||||
|
|
||||||
|
if(reader.imagesPerPage == 1) {
|
||||||
return PhotoViewGalleryPageOptions(
|
return PhotoViewGalleryPageOptions(
|
||||||
filterQuality: FilterQuality.medium,
|
filterQuality: FilterQuality.medium,
|
||||||
controller: photoViewControllers[index],
|
controller: photoViewControllers[index],
|
||||||
imageProvider: imageProvider,
|
imageProvider: _createImageProviderFromKey(pageImages[0], context),
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
errorBuilder: (_, error, s, retry) {
|
errorBuilder: (_, error, s, retry) {
|
||||||
return NetworkError(message: error.toString(), retry: retry);
|
return NetworkError(message: error.toString(), retry: retry);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return PhotoViewGalleryPageOptions.customChild(
|
||||||
|
controller: photoViewControllers[index],
|
||||||
|
minScale: PhotoViewComputedScale.contained * 1.0,
|
||||||
|
maxScale: PhotoViewComputedScale.covered * 10.0,
|
||||||
|
child: buildPageImages(pageImages),
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
pageController: controller,
|
pageController: controller,
|
||||||
loadingBuilder: (context, event) => Center(
|
loadingBuilder: (context, event) => Center(
|
||||||
@@ -186,9 +208,9 @@ class _GalleryModeState extends State<_GalleryMode>
|
|||||||
if (!reader.toPrevChapter()) {
|
if (!reader.toPrevChapter()) {
|
||||||
reader.toPage(1);
|
reader.toPage(1);
|
||||||
}
|
}
|
||||||
} else if (i == reader.maxPage + 1) {
|
} else if (i == totalPages + 1) {
|
||||||
if (!reader.toNextChapter()) {
|
if (!reader.toNextChapter()) {
|
||||||
reader.toPage(reader.maxPage);
|
reader.toPage(totalPages);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
reader.setPage(i);
|
reader.setPage(i);
|
||||||
@@ -198,9 +220,30 @@ class _GalleryModeState extends State<_GalleryMode>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget buildPageImages(List<String> images) {
|
||||||
|
Axis axis = (reader.mode == ReaderMode.galleryTopToBottom)
|
||||||
|
? Axis.vertical
|
||||||
|
: Axis.horizontal;
|
||||||
|
|
||||||
|
List<Widget> imageWidgets = images.map((imageKey) {
|
||||||
|
ImageProvider imageProvider =
|
||||||
|
_createImageProviderFromKey(imageKey, context);
|
||||||
|
return Expanded(
|
||||||
|
child: Image(
|
||||||
|
image: imageProvider,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
return axis == Axis.vertical
|
||||||
|
? Column(children: imageWidgets)
|
||||||
|
: Row(children: imageWidgets);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> animateToPage(int page) {
|
Future<void> animateToPage(int page) {
|
||||||
if ((page - controller.page!).abs() > 1) {
|
if ((page - controller.page!.round()).abs() > 1) {
|
||||||
controller.jumpToPage(page > controller.page! ? page - 1 : page + 1);
|
controller.jumpToPage(page > controller.page! ? page - 1 : page + 1);
|
||||||
}
|
}
|
||||||
return controller.animateToPage(
|
return controller.animateToPage(
|
||||||
@@ -600,11 +643,26 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ImageProvider _createImageProviderFromKey(
|
||||||
|
String imageKey, BuildContext context) {
|
||||||
|
if (imageKey.startsWith('file://')) {
|
||||||
|
return FileImage(File(imageKey.replaceFirst("file://", '')));
|
||||||
|
} else {
|
||||||
|
var reader = context.reader;
|
||||||
|
return ReaderImageProvider(
|
||||||
|
imageKey,
|
||||||
|
reader.type.comicSource!.key,
|
||||||
|
reader.cid,
|
||||||
|
reader.eid,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ImageProvider _createImageProvider(int page, BuildContext context) {
|
ImageProvider _createImageProvider(int page, BuildContext context) {
|
||||||
var reader = context.reader;
|
var reader = context.reader;
|
||||||
var imageKey = reader.images![page - 1];
|
var imageKey = reader.images![page - 1];
|
||||||
if (imageKey.startsWith('file://')) {
|
if (imageKey.startsWith('file://')) {
|
||||||
return FileImage(openFilePlatform(imageKey.replaceFirst("file://", '')));
|
return FileImage(File(imageKey.replaceFirst("file://", '')));
|
||||||
} else {
|
} else {
|
||||||
return ReaderImageProvider(
|
return ReaderImageProvider(
|
||||||
imageKey,
|
imageKey,
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
library venera_reader;
|
library venera_reader;
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
@@ -82,7 +83,8 @@ class _ReaderState extends State<Reader> with _ReaderLocation, _ReaderWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get maxPage => images?.length ?? 1;
|
int get maxPage =>
|
||||||
|
((images?.length ?? 1) + imagesPerPage - 1) ~/ imagesPerPage;
|
||||||
|
|
||||||
ComicType get type => widget.type;
|
ComicType get type => widget.type;
|
||||||
|
|
||||||
@@ -94,6 +96,30 @@ class _ReaderState extends State<Reader> with _ReaderLocation, _ReaderWindow {
|
|||||||
|
|
||||||
late ReaderMode mode;
|
late ReaderMode mode;
|
||||||
|
|
||||||
|
int get imagesPerPage => appdata.settings['readerScreenPicNumber'] ?? 1;
|
||||||
|
|
||||||
|
int _lastImagesPerPage = appdata.settings['readerScreenPicNumber'] ?? 1;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
_checkImagesPerPageChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _checkImagesPerPageChange() {
|
||||||
|
int currentImagesPerPage = imagesPerPage;
|
||||||
|
if (_lastImagesPerPage != currentImagesPerPage) {
|
||||||
|
_adjustPageForImagesPerPageChange(_lastImagesPerPage, currentImagesPerPage);
|
||||||
|
_lastImagesPerPage = currentImagesPerPage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _adjustPageForImagesPerPageChange(int oldImagesPerPage, int newImagesPerPage) {
|
||||||
|
int previousImageIndex = (page - 1) * oldImagesPerPage;
|
||||||
|
int newPage = (previousImageIndex ~/ newImagesPerPage) + 1;
|
||||||
|
page = newPage;
|
||||||
|
}
|
||||||
|
|
||||||
History? history;
|
History? history;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -133,6 +159,7 @@ class _ReaderState extends State<Reader> with _ReaderLocation, _ReaderWindow {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
_checkImagesPerPageChange();
|
||||||
return KeyboardListener(
|
return KeyboardListener(
|
||||||
focusNode: focusNode,
|
focusNode: focusNode,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
|
@@ -469,7 +469,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
ImageProvider image;
|
ImageProvider image;
|
||||||
var imageKey = images[index];
|
var imageKey = images[index];
|
||||||
if (imageKey.startsWith('file://')) {
|
if (imageKey.startsWith('file://')) {
|
||||||
image = FileImage(openFilePlatform(imageKey.replaceFirst("file://", '')));
|
image = FileImage(File(imageKey.replaceFirst("file://", '')));
|
||||||
} else {
|
} else {
|
||||||
image = ReaderImageProvider(
|
image = ReaderImageProvider(
|
||||||
imageKey,
|
imageKey,
|
||||||
@@ -515,7 +515,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (imageKey.startsWith("file://")) {
|
if (imageKey.startsWith("file://")) {
|
||||||
return await openFilePlatform(imageKey.substring(7)).readAsBytes();
|
return await File(imageKey.substring(7)).readAsBytes();
|
||||||
} else {
|
} else {
|
||||||
return (await CacheManager().findCache(
|
return (await CacheManager().findCache(
|
||||||
"$imageKey@${context.reader.type.sourceKey}@${context.reader.cid}@${context.reader.eid}"))!
|
"$imageKey@${context.reader.type.sourceKey}@${context.reader.cid}@${context.reader.eid}"))!
|
||||||
|
@@ -68,6 +68,13 @@ class _AboutSettingsState extends State<AboutSettings> {
|
|||||||
launchUrlString("https://github.com/venera-app/venera");
|
launchUrlString("https://github.com/venera-app/venera");
|
||||||
},
|
},
|
||||||
).toSliver(),
|
).toSliver(),
|
||||||
|
ListTile(
|
||||||
|
title: const Text("Telegram"),
|
||||||
|
trailing: const Icon(Icons.open_in_new),
|
||||||
|
onTap: () {
|
||||||
|
launchUrlString("https://t.me/venera_release");
|
||||||
|
},
|
||||||
|
).toSliver(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -34,25 +34,8 @@ class _AppSettingsState extends State<AppSettings> {
|
|||||||
callback: () async {
|
callback: () async {
|
||||||
String? result;
|
String? result;
|
||||||
if (App.isAndroid) {
|
if (App.isAndroid) {
|
||||||
var channel = const MethodChannel("venera/storage");
|
var picker = DirectoryPicker();
|
||||||
var permission = await channel.invokeMethod('');
|
result = (await picker.pickDirectory())?.path;
|
||||||
if (permission != true) {
|
|
||||||
context.showMessage(message: "Permission denied".tl);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var path = await selectDirectory();
|
|
||||||
if (path != null) {
|
|
||||||
// check if the path is writable
|
|
||||||
var testFile = File(FilePath.join(path, "test"));
|
|
||||||
try {
|
|
||||||
await testFile.writeAsBytes([1]);
|
|
||||||
await testFile.delete();
|
|
||||||
} catch (e) {
|
|
||||||
context.showMessage(message: "Permission denied".tl);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
result = path;
|
|
||||||
}
|
|
||||||
} else if (App.isIOS) {
|
} else if (App.isIOS) {
|
||||||
result = await selectDirectoryIOS();
|
result = await selectDirectoryIOS();
|
||||||
} else {
|
} else {
|
||||||
@@ -127,16 +110,23 @@ class _AppSettingsState extends State<AppSettings> {
|
|||||||
title: "Import App Data".tl,
|
title: "Import App Data".tl,
|
||||||
callback: () async {
|
callback: () async {
|
||||||
var controller = showLoadingDialog(context);
|
var controller = showLoadingDialog(context);
|
||||||
var file = await selectFile(ext: ['venera']);
|
var file = await selectFile(ext: ['venera', 'picadata']);
|
||||||
if (file != null) {
|
if (file != null) {
|
||||||
var cacheFile = File(FilePath.join(App.cachePath, "temp.venera"));
|
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')) {
|
||||||
|
await importPicaData(cacheFile);
|
||||||
|
} else {
|
||||||
await importAppData(cacheFile);
|
await importAppData(cacheFile);
|
||||||
|
}
|
||||||
} 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 {
|
||||||
|
cacheFile.deleteIgnoreError();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
controller.close();
|
controller.close();
|
||||||
},
|
},
|
||||||
|
@@ -41,6 +41,11 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
|||||||
"continuousTopToBottom": "Continuous (Top to Bottom)".tl,
|
"continuousTopToBottom": "Continuous (Top to Bottom)".tl,
|
||||||
},
|
},
|
||||||
onChanged: () {
|
onChanged: () {
|
||||||
|
var readerMode = appdata.settings['readerMode'];
|
||||||
|
if (readerMode?.toLowerCase().startsWith('continuous') ?? false) {
|
||||||
|
appdata.settings['readerScreenPicNumber'] = 1;
|
||||||
|
widget.onChanged?.call('readerScreenPicNumber');
|
||||||
|
}
|
||||||
widget.onChanged?.call("readerMode");
|
widget.onChanged?.call("readerMode");
|
||||||
},
|
},
|
||||||
).toSliver(),
|
).toSliver(),
|
||||||
@@ -54,6 +59,25 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
|||||||
widget.onChanged?.call("autoPageTurningInterval");
|
widget.onChanged?.call("autoPageTurningInterval");
|
||||||
},
|
},
|
||||||
).toSliver(),
|
).toSliver(),
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: AbsorbPointer(
|
||||||
|
absorbing: (appdata.settings['readerMode']?.toLowerCase().startsWith('continuous') ?? false),
|
||||||
|
child: AnimatedOpacity(
|
||||||
|
opacity: (appdata.settings['readerMode']?.toLowerCase().startsWith('continuous') ?? false) ? 0.5 : 1.0,
|
||||||
|
duration: Duration(milliseconds: 300),
|
||||||
|
child: _SliderSetting(
|
||||||
|
title: "The number of pic in screen (Only Gallery Mode)".tl,
|
||||||
|
settingsIndex: "readerScreenPicNumber",
|
||||||
|
interval: 1,
|
||||||
|
min: 1,
|
||||||
|
max: 5,
|
||||||
|
onChanged: () {
|
||||||
|
widget.onChanged?.call("readerScreenPicNumber");
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
_SwitchSetting(
|
_SwitchSetting(
|
||||||
title: 'Long press to zoom'.tl,
|
title: 'Long press to zoom'.tl,
|
||||||
settingKey: 'enableLongPressToZoom',
|
settingKey: 'enableLongPressToZoom',
|
||||||
|
@@ -178,8 +178,9 @@ class _SettingsPageState extends State<SettingsPage> implements PopEntry {
|
|||||||
Positioned.fill(child: buildLeft()),
|
Positioned.fill(child: buildLeft()),
|
||||||
Positioned(
|
Positioned(
|
||||||
left: offset,
|
left: offset,
|
||||||
width: MediaQuery.of(context).size.width,
|
right: 0,
|
||||||
height: MediaQuery.of(context).size.height,
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
child: Listener(
|
child: Listener(
|
||||||
onPointerDown: handlePointerDown,
|
onPointerDown: handlePointerDown,
|
||||||
child: AnimatedSwitcher(
|
child: AnimatedSwitcher(
|
||||||
|
@@ -104,14 +104,14 @@ abstract class CBZ {
|
|||||||
FilePath.join(LocalManager().path, sanitizeFileName(metaData.title)),
|
FilePath.join(LocalManager().path, sanitizeFileName(metaData.title)),
|
||||||
);
|
);
|
||||||
dest.createSync();
|
dest.createSync();
|
||||||
coverFile.copy(
|
coverFile.copyMem(
|
||||||
FilePath.join(dest.path, 'cover.${coverFile.path.split('.').last}'));
|
FilePath.join(dest.path, 'cover.${coverFile.extension}'));
|
||||||
if (metaData.chapters == null) {
|
if (metaData.chapters == null) {
|
||||||
for (var i = 0; i < files.length; i++) {
|
for (var i = 0; i < files.length; i++) {
|
||||||
var src = files[i];
|
var src = files[i];
|
||||||
var dst = File(
|
var dst = File(
|
||||||
FilePath.join(dest.path, '${i + 1}.${src.path.split('.').last}'));
|
FilePath.join(dest.path, '${i + 1}.${src.path.split('.').last}'));
|
||||||
await src.copy(dst.path);
|
await src.copyMem(dst.path);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
dest.createSync();
|
dest.createSync();
|
||||||
@@ -129,7 +129,7 @@ abstract class CBZ {
|
|||||||
var src = chapter.value[i];
|
var src = chapter.value[i];
|
||||||
var dst = File(FilePath.join(
|
var dst = File(FilePath.join(
|
||||||
chapterDir.path, '${i + 1}.${src.path.split('.').last}'));
|
chapterDir.path, '${i + 1}.${src.path.split('.').last}'));
|
||||||
await src.copy(dst.path);
|
await src.copyMem(dst.path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -142,10 +142,9 @@ abstract class CBZ {
|
|||||||
directory: dest.name,
|
directory: dest.name,
|
||||||
chapters: cpMap,
|
chapters: cpMap,
|
||||||
downloadedChapters: cpMap?.keys.toList() ?? [],
|
downloadedChapters: cpMap?.keys.toList() ?? [],
|
||||||
cover: 'cover.${coverFile.path.split('.').last}',
|
cover: 'cover.${coverFile.extension}',
|
||||||
createdAt: DateTime.now(),
|
createdAt: DateTime.now(),
|
||||||
);
|
);
|
||||||
LocalManager().add(comic);
|
|
||||||
await cache.delete(recursive: true);
|
await cache.delete(recursive: true);
|
||||||
return comic;
|
return comic;
|
||||||
}
|
}
|
||||||
@@ -164,7 +163,7 @@ abstract class CBZ {
|
|||||||
var dstName =
|
var dstName =
|
||||||
'${i.toString().padLeft(width, '0')}.${image.split('.').last}';
|
'${i.toString().padLeft(width, '0')}.${image.split('.').last}';
|
||||||
var dst = File(FilePath.join(cache.path, dstName));
|
var dst = File(FilePath.join(cache.path, dstName));
|
||||||
await src.copy(dst.path);
|
await src.copyMem(dst.path);
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -187,18 +186,18 @@ abstract class CBZ {
|
|||||||
}
|
}
|
||||||
int i = 1;
|
int i = 1;
|
||||||
for (var image in allImages) {
|
for (var image in allImages) {
|
||||||
var src = openFilePlatform(image);
|
var src = File(image);
|
||||||
var width = allImages.length.toString().length;
|
var width = allImages.length.toString().length;
|
||||||
var dstName =
|
var dstName =
|
||||||
'${i.toString().padLeft(width, '0')}.${image.split('.').last}';
|
'${i.toString().padLeft(width, '0')}.${image.split('.').last}';
|
||||||
var dst = File(FilePath.join(cache.path, dstName));
|
var dst = File(FilePath.join(cache.path, dstName));
|
||||||
await src.copy(dst.path);
|
await src.copyMem(dst.path);
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var cover = comic.coverFile;
|
var cover = comic.coverFile;
|
||||||
await cover
|
await cover
|
||||||
.copy(FilePath.join(cache.path, 'cover.${cover.path.split('.').last}'));
|
.copyMem(FilePath.join(cache.path, 'cover.${cover.path.split('.').last}'));
|
||||||
await File(FilePath.join(cache.path, 'metadata.json')).writeAsString(
|
await File(FilePath.join(cache.path, 'metadata.json')).writeAsString(
|
||||||
jsonEncode(
|
jsonEncode(
|
||||||
ComicMetaData(
|
ComicMetaData(
|
||||||
|
@@ -1,11 +1,14 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:isolate';
|
import 'dart:isolate';
|
||||||
|
|
||||||
|
import 'package:sqlite3/sqlite3.dart';
|
||||||
import 'package:venera/foundation/app.dart';
|
import 'package:venera/foundation/app.dart';
|
||||||
import 'package:venera/foundation/appdata.dart';
|
import 'package:venera/foundation/appdata.dart';
|
||||||
import 'package:venera/foundation/comic_source/comic_source.dart';
|
import 'package:venera/foundation/comic_source/comic_source.dart';
|
||||||
|
import 'package:venera/foundation/comic_type.dart';
|
||||||
import 'package:venera/foundation/favorites.dart';
|
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/network/cookie_jar.dart';
|
import 'package:venera/network/cookie_jar.dart';
|
||||||
import 'package:zip_flutter/zip_flutter.dart';
|
import 'package:zip_flutter/zip_flutter.dart';
|
||||||
|
|
||||||
@@ -43,6 +46,11 @@ Future<File> exportAppData() async {
|
|||||||
Future<void> importAppData(File file, [bool checkVersion = false]) async {
|
Future<void> importAppData(File file, [bool checkVersion = false]) async {
|
||||||
var cacheDirPath = FilePath.join(App.cachePath, 'temp_data');
|
var cacheDirPath = FilePath.join(App.cachePath, 'temp_data');
|
||||||
var cacheDir = Directory(cacheDirPath);
|
var cacheDir = Directory(cacheDirPath);
|
||||||
|
if (cacheDir.existsSync()) {
|
||||||
|
cacheDir.deleteSync(recursive: true);
|
||||||
|
}
|
||||||
|
cacheDir.createSync();
|
||||||
|
try {
|
||||||
await Isolate.run(() {
|
await Isolate.run(() {
|
||||||
ZipFile.openAndExtract(file.path, cacheDirPath);
|
ZipFile.openAndExtract(file.path, cacheDirPath);
|
||||||
});
|
});
|
||||||
@@ -65,7 +73,8 @@ Future<void> importAppData(File file, [bool checkVersion = false]) async {
|
|||||||
}
|
}
|
||||||
if (await localFavoriteFile.exists()) {
|
if (await localFavoriteFile.exists()) {
|
||||||
LocalFavoritesManager().close();
|
LocalFavoritesManager().close();
|
||||||
File(FilePath.join(App.dataPath, "local_favorite.db")).deleteIfExistsSync();
|
File(FilePath.join(App.dataPath, "local_favorite.db"))
|
||||||
|
.deleteIfExistsSync();
|
||||||
localFavoriteFile
|
localFavoriteFile
|
||||||
.renameSync(FilePath.join(App.dataPath, "local_favorite.db"));
|
.renameSync(FilePath.join(App.dataPath, "local_favorite.db"));
|
||||||
LocalFavoritesManager().init();
|
LocalFavoritesManager().init();
|
||||||
@@ -93,11 +102,109 @@ Future<void> importAppData(File file, [bool checkVersion = false]) async {
|
|||||||
if (Directory(comicSourceDir).existsSync()) {
|
if (Directory(comicSourceDir).existsSync()) {
|
||||||
for (var file in Directory(comicSourceDir).listSync()) {
|
for (var file in Directory(comicSourceDir).listSync()) {
|
||||||
if (file is File) {
|
if (file is File) {
|
||||||
var targetFile = FilePath.join(App.dataPath, "comic_source", file.name);
|
var targetFile =
|
||||||
|
FilePath.join(App.dataPath, "comic_source", file.name);
|
||||||
File(targetFile).deleteIfExistsSync();
|
File(targetFile).deleteIfExistsSync();
|
||||||
await file.copy(targetFile);
|
await file.copy(targetFile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await ComicSource.reload();
|
await ComicSource.reload();
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
cacheDir.deleteIgnoreError(recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> importPicaData(File file) async {
|
||||||
|
var cacheDirPath = FilePath.join(App.cachePath, 'temp_data');
|
||||||
|
var cacheDir = Directory(cacheDirPath);
|
||||||
|
if (cacheDir.existsSync()) {
|
||||||
|
cacheDir.deleteSync(recursive: true);
|
||||||
|
}
|
||||||
|
cacheDir.createSync();
|
||||||
|
try {
|
||||||
|
await Isolate.run(() {
|
||||||
|
ZipFile.openAndExtract(file.path, cacheDirPath);
|
||||||
|
});
|
||||||
|
var localFavoriteFile = cacheDir.joinFile("local_favorite.db");
|
||||||
|
if (localFavoriteFile.existsSync()) {
|
||||||
|
var db = sqlite3.open(localFavoriteFile.path);
|
||||||
|
try {
|
||||||
|
var folderNames = db
|
||||||
|
.select("SELECT name FROM sqlite_master WHERE type='table';")
|
||||||
|
.map((e) => e["name"] as String)
|
||||||
|
.toList();
|
||||||
|
folderNames.removeWhere((e) => e == "folder_order" || e == "folder_sync");
|
||||||
|
for (var folderName in folderNames) {
|
||||||
|
if (!LocalFavoritesManager().existsFolder(folderName)) {
|
||||||
|
LocalFavoritesManager().createFolder(folderName);
|
||||||
|
}
|
||||||
|
for (var comic in db.select("SELECT * FROM \"$folderName\";")) {
|
||||||
|
LocalFavoritesManager().addComic(
|
||||||
|
folderName,
|
||||||
|
FavoriteItem(
|
||||||
|
id: comic['target'],
|
||||||
|
name: comic['name'],
|
||||||
|
coverPath: comic['cover_path'],
|
||||||
|
author: comic['author'],
|
||||||
|
type: ComicType(switch(comic['type']) {
|
||||||
|
0 => 'picacg'.hashCode,
|
||||||
|
1 => 'ehentai'.hashCode,
|
||||||
|
2 => 'jm'.hashCode,
|
||||||
|
3 => 'hitomi'.hashCode,
|
||||||
|
4 => 'wnacg'.hashCode,
|
||||||
|
6 => 'nhentai'.hashCode,
|
||||||
|
_ => comic['type']
|
||||||
|
}),
|
||||||
|
tags: comic['tags'].split(','),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(e) {
|
||||||
|
Log.error("Import Data", "Failed to import local favorite: $e");
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
db.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var historyFile = cacheDir.joinFile("history.db");
|
||||||
|
if (historyFile.existsSync()) {
|
||||||
|
var db = sqlite3.open(historyFile.path);
|
||||||
|
try {
|
||||||
|
for (var comic in db.select("SELECT * FROM history;")) {
|
||||||
|
HistoryManager().addHistory(
|
||||||
|
History.fromMap({
|
||||||
|
"type": switch(comic['type']) {
|
||||||
|
0 => 'picacg'.hashCode,
|
||||||
|
1 => 'ehentai'.hashCode,
|
||||||
|
2 => 'jm'.hashCode,
|
||||||
|
3 => 'hitomi'.hashCode,
|
||||||
|
4 => 'wnacg'.hashCode,
|
||||||
|
6 => 'nhentai'.hashCode,
|
||||||
|
_ => comic['type']
|
||||||
|
},
|
||||||
|
"id": comic['target'],
|
||||||
|
"maxPage": comic["max_page"],
|
||||||
|
"ep": comic["ep"],
|
||||||
|
"page": comic["page"],
|
||||||
|
"time": comic["time"],
|
||||||
|
"title": comic["title"],
|
||||||
|
"subtitle": comic["subtitle"],
|
||||||
|
"cover": comic["cover"],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(e) {
|
||||||
|
Log.error("Import Data", "Failed to import history: $e");
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
db.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
cacheDir.deleteIgnoreError(recursive: true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -13,7 +13,7 @@ class FileType {
|
|||||||
var mime = lookupMimeType('no-file.$ext') ?? 'application/octet-stream';
|
var mime = lookupMimeType('no-file.$ext') ?? 'application/octet-stream';
|
||||||
// Android doesn't support some mime types
|
// Android doesn't support some mime types
|
||||||
mime = switch(mime) {
|
mime = switch(mime) {
|
||||||
'text/javascript' => 'application/javascript',
|
'text/javascript' => 'application/octet-stream',
|
||||||
'application/x-cbr' => 'application/octet-stream',
|
'application/x-cbr' => 'application/octet-stream',
|
||||||
_ => mime,
|
_ => mime,
|
||||||
};
|
};
|
||||||
|
@@ -20,7 +20,7 @@ class ImportComic {
|
|||||||
const ImportComic({this.selectedFolder, this.copyToLocal = true});
|
const ImportComic({this.selectedFolder, this.copyToLocal = true});
|
||||||
|
|
||||||
Future<bool> cbz() async {
|
Future<bool> cbz() async {
|
||||||
var file = await selectFile(ext: ['cbz']);
|
var file = await selectFile(ext: ['cbz', 'zip']);
|
||||||
Map<String?, List<LocalComic>> imported = {};
|
Map<String?, List<LocalComic>> imported = {};
|
||||||
if (file == null) {
|
if (file == null) {
|
||||||
return false;
|
return false;
|
||||||
@@ -34,7 +34,7 @@ class ImportComic {
|
|||||||
App.rootContext.showMessage(message: e.toString());
|
App.rootContext.showMessage(message: e.toString());
|
||||||
}
|
}
|
||||||
controller.close();
|
controller.close();
|
||||||
return registerComics(imported, true);
|
return registerComics(imported, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> ehViewer() async {
|
Future<bool> ehViewer() async {
|
||||||
@@ -60,7 +60,7 @@ class ImportComic {
|
|||||||
if (cancelled) {
|
if (cancelled) {
|
||||||
return imported;
|
return imported;
|
||||||
}
|
}
|
||||||
var comicDir = openDirectoryPlatform(
|
var comicDir = Directory(
|
||||||
FilePath.join(comicSrc.path, comic['DIRNAME'] as String));
|
FilePath.join(comicSrc.path, comic['DIRNAME'] as String));
|
||||||
String titleJP =
|
String titleJP =
|
||||||
comic['TITLE_JPN'] == null ? "" : comic['TITLE_JPN'] as String;
|
comic['TITLE_JPN'] == null ? "" : comic['TITLE_JPN'] as String;
|
||||||
@@ -105,8 +105,7 @@ class ImportComic {
|
|||||||
if (cancelled) {
|
if (cancelled) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
var folderName =
|
var folderName = tag == '' ? '(EhViewer)Default'.tl : '(EhViewer)$tag';
|
||||||
tag == '' ? '(EhViewer)Default'.tl : '(EhViewer)$tag';
|
|
||||||
var comicList = db.select("""
|
var comicList = db.select("""
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM DOWNLOAD_DIRNAME DN
|
FROM DOWNLOAD_DIRNAME DN
|
||||||
@@ -176,8 +175,7 @@ class ImportComic {
|
|||||||
String? title,
|
String? title,
|
||||||
String? subtitle,
|
String? subtitle,
|
||||||
List<String>? tags,
|
List<String>? tags,
|
||||||
DateTime? createTime})
|
DateTime? createTime}) async {
|
||||||
async {
|
|
||||||
if (!(await directory.exists())) return null;
|
if (!(await directory.exists())) return null;
|
||||||
var name = title ?? directory.name;
|
var name = title ?? directory.name;
|
||||||
if (LocalManager().findByName(name) != null) {
|
if (LocalManager().findByName(name) != null) {
|
||||||
@@ -212,12 +210,13 @@ class ImportComic {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fileList.sort();
|
fileList.sort();
|
||||||
coverPath = fileList.firstWhereOrNull((l) => l.startsWith('cover')) ?? fileList.first;
|
coverPath = fileList.firstWhereOrNull((l) => l.startsWith('cover')) ??
|
||||||
|
fileList.first;
|
||||||
|
|
||||||
chapters.sort();
|
chapters.sort();
|
||||||
if (hasChapters && coverPath == '') {
|
if (hasChapters && coverPath == '') {
|
||||||
// use the first image in the first chapter as the cover
|
// use the first image in the first chapter as the cover
|
||||||
var firstChapter = openDirectoryPlatform('${directory.path}/${chapters.first}');
|
var firstChapter = Directory('${directory.path}/${chapters.first}');
|
||||||
await for (var entry in firstChapter.list()) {
|
await for (var entry in firstChapter.list()) {
|
||||||
if (entry is File) {
|
if (entry is File) {
|
||||||
coverPath = entry.name;
|
coverPath = entry.name;
|
||||||
@@ -243,19 +242,21 @@ class ImportComic {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<Map<String, String>> _copyDirectories(Map<String, dynamic> data) async {
|
static Future<Map<String, String>> _copyDirectories(
|
||||||
|
Map<String, dynamic> data) async {
|
||||||
|
return overrideIO(() async {
|
||||||
var toBeCopied = data['toBeCopied'] as List<String>;
|
var toBeCopied = data['toBeCopied'] as List<String>;
|
||||||
var destination = data['destination'] as String;
|
var destination = data['destination'] as String;
|
||||||
Map<String, String> result = {};
|
Map<String, String> result = {};
|
||||||
for (var dir in toBeCopied) {
|
for (var dir in toBeCopied) {
|
||||||
var source = openDirectoryPlatform(dir);
|
var source = Directory(dir);
|
||||||
var dest = openDirectoryPlatform("$destination/${source.name}");
|
var dest = Directory("$destination/${source.name}");
|
||||||
if (dest.existsSync()) {
|
if (dest.existsSync()) {
|
||||||
// The destination directory already exists, and it is not managed by the app.
|
// The destination directory already exists, and it is not managed by the app.
|
||||||
// Rename the old directory to avoid conflicts.
|
// Rename the old directory to avoid conflicts.
|
||||||
Log.info("Import Comic",
|
Log.info("Import Comic",
|
||||||
"Directory already exists: ${source.name}\nRenaming the old directory.");
|
"Directory already exists: ${source.name}\nRenaming the old directory.");
|
||||||
await dest.rename(
|
dest.renameSync(
|
||||||
findValidDirectoryName(dest.parent.path, "${dest.path}_old"));
|
findValidDirectoryName(dest.parent.path, "${dest.path}_old"));
|
||||||
}
|
}
|
||||||
dest.createSync();
|
dest.createSync();
|
||||||
@@ -263,6 +264,7 @@ class ImportComic {
|
|||||||
result[source.path] = dest.path;
|
result[source.path] = dest.path;
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String?, List<LocalComic>>> _copyComicsToLocalDir(
|
Future<Map<String?, List<LocalComic>>> _copyComicsToLocalDir(
|
||||||
@@ -284,13 +286,13 @@ class ImportComic {
|
|||||||
// copy the comics to the local directory
|
// copy the comics to the local directory
|
||||||
var pathMap = await compute<Map<String, dynamic>, Map<String, String>>(
|
var pathMap = await compute<Map<String, dynamic>, Map<String, String>>(
|
||||||
_copyDirectories, {
|
_copyDirectories, {
|
||||||
'toBeCopied': comics[favoriteFolder]!.map((e) => e.directory).toList(),
|
'toBeCopied':
|
||||||
|
comics[favoriteFolder]!.map((e) => e.directory).toList(),
|
||||||
'destination': destPath,
|
'destination': destPath,
|
||||||
});
|
});
|
||||||
//Construct a new object since LocalComic.directory is a final String
|
//Construct a new object since LocalComic.directory is a final String
|
||||||
for (var c in comics[favoriteFolder]!) {
|
for (var c in comics[favoriteFolder]!) {
|
||||||
result[favoriteFolder]!.add(
|
result[favoriteFolder]!.add(LocalComic(
|
||||||
LocalComic(
|
|
||||||
id: c.id,
|
id: c.id,
|
||||||
title: c.title,
|
title: c.title,
|
||||||
subtitle: c.subtitle,
|
subtitle: c.subtitle,
|
||||||
@@ -300,20 +302,20 @@ class ImportComic {
|
|||||||
cover: c.cover,
|
cover: c.cover,
|
||||||
comicType: c.comicType,
|
comicType: c.comicType,
|
||||||
downloadedChapters: c.downloadedChapters,
|
downloadedChapters: c.downloadedChapters,
|
||||||
createdAt: c.createdAt
|
createdAt: c.createdAt,
|
||||||
)
|
));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e, s) {
|
||||||
App.rootContext.showMessage(message: "Failed to copy comics".tl);
|
App.rootContext.showMessage(message: "Failed to copy comics".tl);
|
||||||
Log.error("Import Comic", e.toString());
|
Log.error("Import Comic", e.toString(), s);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> registerComics(Map<String?, List<LocalComic>> importedComics, bool copy) async {
|
Future<bool> registerComics(
|
||||||
|
Map<String?, List<LocalComic>> importedComics, bool copy) async {
|
||||||
try {
|
try {
|
||||||
if (copy) {
|
if (copy) {
|
||||||
importedComics = await _copyComicsToLocalDir(importedComics);
|
importedComics = await _copyComicsToLocalDir(importedComics);
|
||||||
@@ -334,9 +336,7 @@ class ImportComic {
|
|||||||
author: comic.subtitle,
|
author: comic.subtitle,
|
||||||
type: comic.comicType,
|
type: comic.comicType,
|
||||||
tags: comic.tags,
|
tags: comic.tags,
|
||||||
favoriteTime: comic.createdAt
|
favoriteTime: comic.createdAt));
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -344,9 +344,9 @@ class ImportComic {
|
|||||||
message: "Imported @a comics".tlParams({
|
message: "Imported @a comics".tlParams({
|
||||||
'a': importedCount,
|
'a': importedCount,
|
||||||
}));
|
}));
|
||||||
} catch(e) {
|
} catch (e, s) {
|
||||||
App.rootContext.showMessage(message: "Failed to register comics".tl);
|
App.rootContext.showMessage(message: "Failed to register comics".tl);
|
||||||
Log.error("Import Comic", e.toString());
|
Log.error("Import Comic", e.toString(), s);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
@@ -73,6 +73,15 @@ extension FileSystemEntityExt on FileSystemEntity {
|
|||||||
|
|
||||||
extension FileExtension on File {
|
extension FileExtension on File {
|
||||||
String get extension => path.split('.').last;
|
String get extension => path.split('.').last;
|
||||||
|
|
||||||
|
/// Copy the file to the specified path using memory.
|
||||||
|
///
|
||||||
|
/// This method prevents errors caused by files from different file systems.
|
||||||
|
Future<void> copyMem(String newPath) async {
|
||||||
|
var newFile = File(newPath);
|
||||||
|
// Stream is not usable since [AndroidFile] does not support [openRead].
|
||||||
|
await newFile.writeAsBytes(await readAsBytes());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension DirectoryExtension on Directory {
|
extension DirectoryExtension on Directory {
|
||||||
@@ -81,7 +90,7 @@ extension DirectoryExtension on Directory {
|
|||||||
int total = 0;
|
int total = 0;
|
||||||
for (var f in listSync(recursive: true)) {
|
for (var f in listSync(recursive: true)) {
|
||||||
if (FileSystemEntity.typeSync(f.path) == FileSystemEntityType.file) {
|
if (FileSystemEntity.typeSync(f.path) == FileSystemEntityType.file) {
|
||||||
total += await openFilePlatform(f.path).length();
|
total += await File(f.path).length();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return total;
|
return total;
|
||||||
@@ -93,7 +102,21 @@ extension DirectoryExtension on Directory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
File joinFile(String name) {
|
File joinFile(String name) {
|
||||||
return openFilePlatform(FilePath.join(path, name));
|
return File(FilePath.join(path, name));
|
||||||
|
}
|
||||||
|
|
||||||
|
void deleteContentsSync({recursive = true}) {
|
||||||
|
if (!existsSync()) return;
|
||||||
|
for (var f in listSync()) {
|
||||||
|
f.deleteIfExistsSync(recursive: recursive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteContents({recursive = true}) async {
|
||||||
|
if (!existsSync()) return;
|
||||||
|
for (var f in listSync()) {
|
||||||
|
await f.deleteIfExists(recursive: recursive);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,14 +147,15 @@ String sanitizeFileName(String fileName) {
|
|||||||
Future<void> copyDirectory(Directory source, Directory destination) async {
|
Future<void> copyDirectory(Directory source, Directory destination) async {
|
||||||
List<FileSystemEntity> contents = source.listSync();
|
List<FileSystemEntity> contents = source.listSync();
|
||||||
for (FileSystemEntity content in contents) {
|
for (FileSystemEntity content in contents) {
|
||||||
String newPath = destination.path +
|
String newPath = FilePath.join(destination.path, content.name);
|
||||||
Platform.pathSeparator +
|
|
||||||
content.path.split(Platform.pathSeparator).last;
|
|
||||||
|
|
||||||
if (content is File) {
|
if (content is File) {
|
||||||
content.copySync(newPath);
|
var resultFile = File(newPath);
|
||||||
|
resultFile.createSync();
|
||||||
|
var data = content.readAsBytesSync();
|
||||||
|
resultFile.writeAsBytesSync(data);
|
||||||
} else if (content is Directory) {
|
} else if (content is Directory) {
|
||||||
Directory newDirectory = openDirectoryPlatform(newPath);
|
Directory newDirectory = Directory(newPath);
|
||||||
newDirectory.createSync();
|
newDirectory.createSync();
|
||||||
copyDirectory(content.absolute, newDirectory.absolute);
|
copyDirectory(content.absolute, newDirectory.absolute);
|
||||||
}
|
}
|
||||||
@@ -140,18 +164,16 @@ Future<void> copyDirectory(Directory source, Directory destination) async {
|
|||||||
|
|
||||||
Future<void> copyDirectoryIsolate(
|
Future<void> copyDirectoryIsolate(
|
||||||
Directory source, Directory destination) async {
|
Directory source, Directory destination) async {
|
||||||
await Isolate.run(() {
|
await Isolate.run(() => overrideIO(() => copyDirectory(source, destination)));
|
||||||
copyDirectory(source, destination);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String findValidDirectoryName(String path, String directory) {
|
String findValidDirectoryName(String path, String directory) {
|
||||||
var name = sanitizeFileName(directory);
|
var name = sanitizeFileName(directory);
|
||||||
var dir = openDirectoryPlatform("$path/$name");
|
var dir = Directory("$path/$name");
|
||||||
var i = 1;
|
var i = 1;
|
||||||
while (dir.existsSync() && dir.listSync().isNotEmpty) {
|
while (dir.existsSync() && dir.listSync().isNotEmpty) {
|
||||||
name = sanitizeFileName("$directory($i)");
|
name = sanitizeFileName("$directory($i)");
|
||||||
dir = openDirectoryPlatform("$path/$name");
|
dir = Directory("$path/$name");
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
return name;
|
return name;
|
||||||
@@ -184,11 +206,12 @@ class DirectoryPicker {
|
|||||||
directory = (await AndroidDirectory.pickDirectory())?.path;
|
directory = (await AndroidDirectory.pickDirectory())?.path;
|
||||||
} else {
|
} else {
|
||||||
// ios, macos
|
// ios, macos
|
||||||
directory = await _methodChannel.invokeMethod<String?>("getDirectoryPath");
|
directory =
|
||||||
|
await _methodChannel.invokeMethod<String?>("getDirectoryPath");
|
||||||
}
|
}
|
||||||
if (directory == null) return null;
|
if (directory == null) return null;
|
||||||
_finalizer.attach(this, directory);
|
_finalizer.attach(this, directory);
|
||||||
return openDirectoryPlatform(directory);
|
return Directory(directory);
|
||||||
} finally {
|
} finally {
|
||||||
Future.delayed(const Duration(milliseconds: 100), () {
|
Future.delayed(const Duration(milliseconds: 100), () {
|
||||||
IO._isSelectingFiles = false;
|
IO._isSelectingFiles = false;
|
||||||
@@ -249,7 +272,9 @@ Future<FileSelectResult?> selectFile({required List<String> ext}) async {
|
|||||||
file = FileSelectResult(xFile.path);
|
file = FileSelectResult(xFile.path);
|
||||||
}
|
}
|
||||||
if (!ext.contains(file.path.split(".").last)) {
|
if (!ext.contains(file.path.split(".").last)) {
|
||||||
App.rootContext.showMessage(message: "Invalid file type");
|
App.rootContext.showMessage(
|
||||||
|
message: "Invalid file type: ${file.path.split(".").last}",
|
||||||
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return file;
|
return file;
|
||||||
@@ -311,33 +336,45 @@ Future<void> saveFile(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Directory openDirectoryPlatform(String path) {
|
class _IOOverrides extends IOOverrides {
|
||||||
|
@override
|
||||||
|
Directory createDirectory(String path) {
|
||||||
if (App.isAndroid) {
|
if (App.isAndroid) {
|
||||||
var dir = AndroidDirectory.fromPathSync(path);
|
var dir = AndroidDirectory.fromPathSync(path);
|
||||||
if (dir == null) {
|
if (dir == null) {
|
||||||
return Directory(path);
|
return super.createDirectory(path);
|
||||||
}
|
}
|
||||||
return dir;
|
return dir;
|
||||||
} else {
|
} else {
|
||||||
return Directory(path);
|
return super.createDirectory(path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
File openFilePlatform(String path) {
|
@override
|
||||||
|
File createFile(String path) {
|
||||||
if (path.startsWith("file://")) {
|
if (path.startsWith("file://")) {
|
||||||
path = path.substring(7);
|
path = path.substring(7);
|
||||||
}
|
}
|
||||||
if (App.isAndroid) {
|
if (App.isAndroid) {
|
||||||
var f = AndroidFile.fromPathSync(path);
|
var f = AndroidFile.fromPathSync(path);
|
||||||
if (f == null) {
|
if (f == null) {
|
||||||
return File(path);
|
return super.createFile(path);
|
||||||
}
|
}
|
||||||
return f;
|
return f;
|
||||||
} else {
|
} else {
|
||||||
return File(path);
|
return super.createFile(path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
T overrideIO<T>(T Function() f) {
|
||||||
|
return IOOverrides.runWithIOOverrides<T>(
|
||||||
|
f,
|
||||||
|
_IOOverrides(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
class Share {
|
class Share {
|
||||||
static void shareFile({
|
static void shareFile({
|
||||||
required Uint8List data,
|
required Uint8List data,
|
||||||
|
92
lib/utils/pdf.dart
Normal file
92
lib/utils/pdf.dart
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import 'dart:isolate';
|
||||||
|
|
||||||
|
import 'package:pdf/widgets.dart';
|
||||||
|
import 'package:venera/foundation/local.dart';
|
||||||
|
import 'package:venera/utils/io.dart';
|
||||||
|
|
||||||
|
Future<void> _createPdfFromComic({
|
||||||
|
required LocalComic comic,
|
||||||
|
required String savePath,
|
||||||
|
required String localPath,
|
||||||
|
}) async {
|
||||||
|
final pdf = Document(
|
||||||
|
title: comic.title,
|
||||||
|
author: comic.subTitle ?? "",
|
||||||
|
producer: "Venera",
|
||||||
|
);
|
||||||
|
|
||||||
|
pdf.document.outline;
|
||||||
|
|
||||||
|
var baseDir = comic.directory.contains('/') || comic.directory.contains('\\')
|
||||||
|
? comic.directory
|
||||||
|
: FilePath.join(localPath, comic.directory);
|
||||||
|
|
||||||
|
// add cover
|
||||||
|
var imageData = File(FilePath.join(baseDir, comic.cover)).readAsBytesSync();
|
||||||
|
pdf.addPage(Page(
|
||||||
|
build: (Context context) {
|
||||||
|
return Image(MemoryImage(imageData), fit: BoxFit.contain);
|
||||||
|
},
|
||||||
|
));
|
||||||
|
|
||||||
|
bool multiChapters = comic.chapters != null;
|
||||||
|
|
||||||
|
void reorderFiles(List<FileSystemEntity> files) {
|
||||||
|
files.removeWhere(
|
||||||
|
(element) => element is! File || element.path.startsWith('cover'));
|
||||||
|
files.sort((a, b) {
|
||||||
|
var aName = (a as File).name;
|
||||||
|
var bName = (b as File).name;
|
||||||
|
var aNumber = int.tryParse(aName);
|
||||||
|
var bNumber = int.tryParse(bName);
|
||||||
|
if (aNumber != null && bNumber != null) {
|
||||||
|
return aNumber.compareTo(bNumber);
|
||||||
|
}
|
||||||
|
return aName.compareTo(bName);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!multiChapters) {
|
||||||
|
var files = Directory(baseDir).listSync();
|
||||||
|
reorderFiles(files);
|
||||||
|
|
||||||
|
for (var file in files) {
|
||||||
|
var imageData = (file as File).readAsBytesSync();
|
||||||
|
pdf.addPage(Page(
|
||||||
|
build: (Context context) {
|
||||||
|
return Image(MemoryImage(imageData), fit: BoxFit.contain);
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (var chapter in comic.chapters!.keys) {
|
||||||
|
var files = Directory(FilePath.join(baseDir, chapter)).listSync();
|
||||||
|
reorderFiles(files);
|
||||||
|
for (var file in files) {
|
||||||
|
var imageData = (file as File).readAsBytesSync();
|
||||||
|
pdf.addPage(Page(
|
||||||
|
build: (Context context) {
|
||||||
|
return Image(MemoryImage(imageData), fit: BoxFit.contain);
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final file = File(savePath);
|
||||||
|
file.writeAsBytesSync(await pdf.save());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> createPdfFromComicIsolate({
|
||||||
|
required LocalComic comic,
|
||||||
|
required String savePath,
|
||||||
|
}) async {
|
||||||
|
var localPath = LocalManager().path;
|
||||||
|
return Isolate.run(() => overrideIO(() async {
|
||||||
|
return await _createPdfFromComic(
|
||||||
|
comic: comic,
|
||||||
|
savePath: savePath,
|
||||||
|
localPath: localPath,
|
||||||
|
);
|
||||||
|
}));
|
||||||
|
}
|
60
pubspec.lock
60
pubspec.lock
@@ -33,6 +33,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.4"
|
version: "1.0.4"
|
||||||
|
archive:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: archive
|
||||||
|
sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.6.1"
|
||||||
args:
|
args:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -49,6 +57,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.11.0"
|
version: "2.11.0"
|
||||||
|
barcode:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: barcode
|
||||||
|
sha256: ab180ce22c6555d77d45f0178a523669db67f95856e3378259ef2ffeb43e6003
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.8"
|
||||||
battery_plus:
|
battery_plus:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -65,6 +81,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.1"
|
version: "2.0.1"
|
||||||
|
bidi:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: bidi
|
||||||
|
sha256: "9a712c7ddf708f7c41b1923aa83648a3ed44cfd75b04f72d598c45e5be287f9d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.12"
|
||||||
boolean_selector:
|
boolean_selector:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -393,8 +417,8 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: "829a566b738a26ea98e523807f49838e21308543"
|
ref: "3315082b9f7055655610e4f6f136b69e48228c05"
|
||||||
resolved-ref: "829a566b738a26ea98e523807f49838e21308543"
|
resolved-ref: "3315082b9f7055655610e4f6f136b69e48228c05"
|
||||||
url: "https://github.com/pkuislm/flutter_saf.git"
|
url: "https://github.com/pkuislm/flutter_saf.git"
|
||||||
source: git
|
source: git
|
||||||
version: "0.0.1"
|
version: "0.0.1"
|
||||||
@@ -465,6 +489,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.2"
|
version: "4.0.2"
|
||||||
|
image:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image
|
||||||
|
sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.3.0"
|
||||||
intl:
|
intl:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -626,6 +658,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.0"
|
version: "1.9.0"
|
||||||
|
path_parsing:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_parsing
|
||||||
|
sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.0"
|
||||||
path_provider:
|
path_provider:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -674,6 +714,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.0"
|
version: "2.3.0"
|
||||||
|
pdf:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: pdf
|
||||||
|
sha256: "05df53f8791587402493ac97b9869d3824eccbc77d97855f4545cf72df3cae07"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.11.1"
|
||||||
petitparser:
|
petitparser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -715,6 +763,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.9.1"
|
version: "3.9.1"
|
||||||
|
qr:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: qr
|
||||||
|
sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.2"
|
||||||
rhttp:
|
rhttp:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@@ -2,7 +2,7 @@ name: venera
|
|||||||
description: "A comic app."
|
description: "A comic app."
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
version: 1.0.7+107
|
version: 1.0.8+108
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.5.0 <4.0.0'
|
sdk: '>=3.5.0 <4.0.0'
|
||||||
@@ -68,7 +68,8 @@ dependencies:
|
|||||||
flutter_saf:
|
flutter_saf:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/pkuislm/flutter_saf.git
|
url: https://github.com/pkuislm/flutter_saf.git
|
||||||
ref: dd5242918da0ea9a0a50b0f87ade7a2def65453d
|
ref: 3315082b9f7055655610e4f6f136b69e48228c05
|
||||||
|
pdf: ^3.11.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
Reference in New Issue
Block a user