From cc5b16ee33cc0f7d6cd2eb6aa3dba8c84e487251 Mon Sep 17 00:00:00 2001 From: niuhuan Date: Thu, 30 Jun 2022 03:02:01 +0800 Subject: [PATCH] ReadPKZ --- android/app/src/main/AndroidManifest.xml | 13 + ci/cmd/send_to_community/main.go | 1 - lib/basic/Entities.dart | 325 ++++++++++++++++ lib/basic/Method.dart | 98 +++++ lib/screens/AccountScreen.dart | 28 ++ lib/screens/AppScreen.dart | 23 ++ lib/screens/CategoriesScreen.dart | 2 +- lib/screens/ComicsScreen.dart | 2 +- lib/screens/DownloadExportToFileScreen.dart | 55 ++- lib/screens/DownloadReaderScreen.dart | 2 +- lib/screens/InitScreen.dart | 38 +- lib/screens/PkzArchiveScreen.dart | 123 +++++++ lib/screens/PkzComicInfoScreen.dart | 249 +++++++++++++ lib/screens/PkzReaderScreen.dart | 253 +++++++++++++ lib/screens/SettingsScreen.dart | 2 +- lib/screens/components/ComicInfoCard.dart | 5 + lib/screens/components/ComicList.dart | 2 +- lib/screens/components/ImageReader.dart | 348 +++++++++++------- lib/screens/components/PkzComicInfoCard.dart | 138 +++++++ lib/screens/components/PkzImages.dart | 176 +++++++++ macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 52 ++- pubspec.yaml | 3 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 25 files changed, 1804 insertions(+), 140 deletions(-) create mode 100644 lib/screens/PkzArchiveScreen.dart create mode 100644 lib/screens/PkzComicInfoScreen.dart create mode 100644 lib/screens/PkzReaderScreen.dart create mode 100644 lib/screens/components/PkzComicInfoCard.dart create mode 100644 lib/screens/components/PkzImages.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 2e0145d..14822ff 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -48,6 +48,19 @@ + + + + + + + diff --git a/ci/cmd/send_to_community/main.go b/ci/cmd/send_to_community/main.go index d9578d1..ebe0877 100644 --- a/ci/cmd/send_to_community/main.go +++ b/ci/cmd/send_to_community/main.go @@ -14,7 +14,6 @@ import ( ) func main() { - return // get version var version commons.Version codeFile, err := ioutil.ReadFile("version.code.txt") diff --git a/lib/basic/Entities.dart b/lib/basic/Entities.dart index ee66e1a..9045938 100644 --- a/lib/basic/Entities.dart +++ b/lib/basic/Entities.dart @@ -687,3 +687,328 @@ class Collection { .toList(); } } + +class PkzArchive { + PkzArchive({ + required this.coverPath, + required this.authorAvatarPath, + required this.comics, + required this.comicCount, + required this.volumesCount, + required this.chapterCount, + required this.pictureCount, + }); + + late final String coverPath; + late final String authorAvatarPath; + late final List comics; + late final int comicCount; + late final int volumesCount; + late final int chapterCount; + late final int pictureCount; + + PkzArchive.fromJson(Map json) { + coverPath = json['cover_path']; + authorAvatarPath = json['author_avatar_path']; + comics = + List.from(json['comics']).map((e) => PkzComic.fromJson(e)).toList(); + comicCount = json['comic_count']; + volumesCount = json['volumes_count']; + chapterCount = json['chapter_count']; + pictureCount = json['picture_count']; + } + + Map toJson() { + final _data = {}; + _data['cover_path'] = coverPath; + _data['author_avatar_path'] = authorAvatarPath; + _data['comics'] = comics.map((e) => e.toJson()).toList(); + _data['comic_count'] = comicCount; + _data['volumes_count'] = volumesCount; + _data['chapter_count'] = chapterCount; + _data['picture_count'] = pictureCount; + return _data; + } +} + +class PkzComic { + PkzComic({ + required this.id, + required this.title, + required this.categories, + required this.tags, + required this.updatedAt, + required this.createdAt, + required this.description, + required this.chineseTeam, + required this.finished, + required this.coverPath, + required this.authorAvatarPath, + required this.volumes, + required this.volumesCount, + required this.chapterCount, + required this.pictureCount, + required this.idx, + }); + + late final String id; + late final String title; + late final List categories; + late final List tags; + late final int updatedAt; + late final int createdAt; + late final String description; + late final String chineseTeam; + late final bool finished; + late final String coverPath; + late final String authorAvatarPath; + late final List volumes; + late final int volumesCount; + late final int chapterCount; + late final int pictureCount; + late final int idx; + late final String author; + late final String authorId; + + PkzComic.fromJson(Map json) { + id = json['id']; + title = json['title']; + categories = List.castFrom(json['categories']); + tags = List.castFrom(json['tags']); + updatedAt = json['updated_at']; + createdAt = json['created_at']; + description = json['description']; + chineseTeam = json['chinese_team']; + finished = json['finished']; + coverPath = json['cover_path']; + authorAvatarPath = json['author_avatar_path']; + volumes = + List.from(json['volumes']).map((e) => PkzVolume.fromJson(e)).toList(); + volumesCount = json['volumes_count']; + chapterCount = json['chapter_count']; + pictureCount = json['picture_count']; + idx = json['idx']; + author = json['author']; + authorId = json['author_id']; + } + + Map toJson() { + final _data = {}; + _data['id'] = id; + _data['title'] = title; + _data['categories'] = categories; + _data['tags'] = tags; + _data['updated_at'] = updatedAt; + _data['created_at'] = createdAt; + _data['description'] = description; + _data['chinese_team'] = chineseTeam; + _data['finished'] = finished; + _data['cover_path'] = coverPath; + _data['author_avatar_path'] = authorAvatarPath; + _data['volumes'] = volumes.map((e) => e.toJson()).toList(); + _data['volumes_count'] = volumesCount; + _data['chapter_count'] = chapterCount; + _data['picture_count'] = pictureCount; + _data['idx'] = idx; + _data['author'] = author; + _data['author_id'] = authorId; + return _data; + } +} + +class PkzVolume { + PkzVolume({ + required this.id, + required this.title, + required this.updatedAt, + required this.createdAt, + required this.coverPath, + required this.chapters, + required this.chapterCount, + required this.pictureCount, + required this.idx, + }); + + late final String id; + late final String title; + late final int updatedAt; + late final int createdAt; + late final String coverPath; + late final List chapters; + late final int chapterCount; + late final int pictureCount; + late final int idx; + + PkzVolume.fromJson(Map json) { + id = json['id']; + title = json['title']; + updatedAt = json['updated_at']; + createdAt = json['created_at']; + coverPath = json['cover_path']; + chapters = + List.from(json['chapters']).map((e) => PkzChapter.fromJson(e)).toList(); + chapterCount = json['chapter_count']; + pictureCount = json['picture_count']; + idx = json['idx']; + } + + Map toJson() { + final _data = {}; + _data['id'] = id; + _data['title'] = title; + _data['updated_at'] = updatedAt; + _data['created_at'] = createdAt; + _data['cover_path'] = coverPath; + _data['chapters'] = chapters.map((e) => e.toJson()).toList(); + _data['chapter_count'] = chapterCount; + _data['picture_count'] = pictureCount; + _data['idx'] = idx; + return _data; + } +} + +class PkzChapter { + PkzChapter({ + required this.id, + required this.title, + required this.updatedAt, + required this.createdAt, + required this.coverPath, + required this.pictures, + required this.pictureCount, + required this.idx, + }); + + late final String id; + late final String title; + late final int updatedAt; + late final int createdAt; + late final String coverPath; + late final List pictures; + late final int pictureCount; + late final int idx; + + PkzChapter.fromJson(Map json) { + id = json['id']; + title = json['title']; + updatedAt = json['updated_at']; + createdAt = json['created_at']; + coverPath = json['cover_path']; + pictures = + List.from(json['pictures']).map((e) => PkzPicture.fromJson(e)).toList(); + pictureCount = json['picture_count']; + idx = json['idx']; + } + + Map toJson() { + final _data = {}; + _data['id'] = id; + _data['title'] = title; + _data['updated_at'] = updatedAt; + _data['created_at'] = createdAt; + _data['cover_path'] = coverPath; + _data['pictures'] = pictures.map((e) => e.toJson()).toList(); + _data['picture_count'] = pictureCount; + _data['idx'] = idx; + return _data; + } +} + +class PkzPicture { + PkzPicture({ + required this.id, + required this.title, + required this.width, + required this.height, + required this.format, + required this.picturePath, + required this.idx, + }); + + late final String id; + late final String title; + late final int width; + late final int height; + late final String format; + late final String picturePath; + late final int idx; + + PkzPicture.fromJson(Map json) { + id = json['id']; + title = json['title']; + width = json['width']; + height = json['height']; + format = json['format']; + picturePath = json['picture_path']; + idx = json['idx']; + } + + Map toJson() { + final _data = {}; + _data['id'] = id; + _data['title'] = title; + _data['width'] = width; + _data['height'] = height; + _data['format'] = format; + _data['picture_path'] = picturePath; + _data['idx'] = idx; + return _data; + } +} + +class Knight extends BasicUser { + late final String role; + late final String character; + late final int comicsUploaded; + + Knight.fromJson(Map json) : super.fromJson(json) { + role = json['role']; + character = json['character']; + comicsUploaded = json['comicsUploaded']; + } +} + +class PkzComicViewLog { + PkzComicViewLog({ + required this.fileName, + required this.lastViewComicId, + required this.filePath, + required this.lastViewComicTitle, + required this.lastViewEpId, + required this.lastViewEpName, + required this.lastViewPictureRank, + required this.lastViewTime, + }); + late final String fileName; + late final String lastViewComicId; + late final String filePath; + late final String lastViewComicTitle; + late final String lastViewEpId; + late final String lastViewEpName; + late final int lastViewPictureRank; + late final String lastViewTime; + + PkzComicViewLog.fromJson(Map json){ + fileName = json['fileName']; + lastViewComicId = json['lastViewComicId']; + filePath = json['filePath']; + lastViewComicTitle = json['lastViewComicTitle']; + lastViewEpId = json['lastViewEpId']; + lastViewEpName = json['lastViewEpName']; + lastViewPictureRank = json['lastViewPictureRank']; + lastViewTime = json['lastViewTime']; + } + + Map toJson() { + final _data = {}; + _data['fileName'] = fileName; + _data['lastViewComicId'] = lastViewComicId; + _data['filePath'] = filePath; + _data['lastViewComicTitle'] = lastViewComicTitle; + _data['lastViewEpId'] = lastViewEpId; + _data['lastViewEpName'] = lastViewEpName; + _data['lastViewPictureRank'] = lastViewPictureRank; + _data['lastViewTime'] = lastViewTime; + return _data; + } +} diff --git a/lib/basic/Method.dart b/lib/basic/Method.dart index 0ccf184..bd72427 100644 --- a/lib/basic/Method.dart +++ b/lib/basic/Method.dart @@ -560,6 +560,19 @@ class Method { }); } + /// 导出下载的图片到PKZ + Future exportComicDownloadToPkz( + List comicIds, + String dir, + String name, + ) { + return _flatInvoke("exportComicDownloadToPkz", { + "comicIds": comicIds, + "dir": dir, + "name": name, + }); + } + /// 使用网络将下载传输到其他设备 Future exportComicUsingSocket(String comicId) async { return int.parse(await _flatInvoke("exportComicUsingSocket", comicId)); @@ -721,4 +734,89 @@ class Method { Future verifyAuthentication() async { return await _channel.invokeMethod("verifyAuthentication"); } + + Future pkzInfo(String pkzPath) async { + return PkzArchive.fromJson( + jsonDecode(await _flatInvoke("pkzInfo", pkzPath))); + } + + Future loadPkzFile(String pkzPath, String path) async { + return base64Decode(await _flatInvoke("loadPkzFile", { + "pkzPath": pkzPath, + "path": path, + })); + } + + Future> pkzComicViewLogs( + String fileName, + String comicId, + ) async { + return List.of(jsonDecode(await _flatInvoke("pkzComicViewLogs", fileName))) + .map((e) => PkzComicViewLog.fromJson(e)) + .toList(); + } + + Future pkzComicViewLogByPkzNameAndId( + String fileName, + String comicId, + ) async { + String data = await _flatInvoke("pkzComicViewLogByPkzNameAndId", { + "fileName": fileName, + "comicId": comicId, + }); + if (data == "" || data == "nil" || data == "null") { + return null; + } + return PkzComicViewLog.fromJson(jsonDecode(data)); + } + + Future viewPkz( + String fileName, + String filePath, + ) async { + return _flatInvoke("viewPkz", { + "fileName": fileName, + "filePath": filePath, + }); + } + + Future viewPkzComic( + String fileName, + String filePath, + String comicId, + String comicTitle, + ) async { + return _flatInvoke("viewPkzComic", { + "fileName": fileName, + "filePath": filePath, + "comicId": comicId, + "comicTitle": comicTitle, + }); + } + + Future viewPkzEpAndPicture( + String fileName, + String filePath, + String comicId, + String comicTitle, + String epId, + String epTitle, + int pictureRank, + ) async { + return _flatInvoke("viewPkzEpAndPicture", { + "fileName": fileName, + "filePath": filePath, + "comicId": comicId, + "comicTitle": comicTitle, + "epId": epId, + "epTitle": epTitle, + "pictureRank": pictureRank, + }); + } + + Future> leaderboardOfKnight() async { + return List.of(jsonDecode(await _flatInvoke("leaderboardOfKnight", ""))) + .map((e) => Knight.fromJson(e)) + .toList(); + } } diff --git a/lib/screens/AccountScreen.dart b/lib/screens/AccountScreen.dart index 96e2233..f38380c 100644 --- a/lib/screens/AccountScreen.dart +++ b/lib/screens/AccountScreen.dart @@ -1,3 +1,7 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:app_links/app_links.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:pikapika/basic/Common.dart'; @@ -7,9 +11,12 @@ import 'package:pikapika/basic/enum/ErrorTypes.dart'; import 'package:pikapika/screens/RegisterScreen.dart'; import 'package:pikapika/screens/SettingsScreen.dart'; import 'package:pikapika/screens/components/NetworkSetting.dart'; +import 'package:uri_to_file/uri_to_file.dart'; +import '../basic/Navigator.dart'; import 'AppScreen.dart'; import 'DownloadListScreen.dart'; +import 'PkzArchiveScreen.dart'; import 'ThemeScreen.dart'; import 'components/ContentLoading.dart'; @@ -25,13 +32,34 @@ class _AccountScreenState extends State { late bool _logging = false; late String _username = ""; late String _password = ""; + late StreamSubscription _linkSubscription; @override void initState() { + final appLinks = AppLinks(); + // todo 不必要cancel 随机监听就好了, APP关闭时销毁, 考虑移动到APP里 + _linkSubscription = appLinks.uriLinkStream.listen((uri) async { + RegExp regExp = RegExp(r"^.*\.pkz$"); + final matches = regExp.allMatches(uri.toString()); + if (matches.isNotEmpty) { + File file = await toFile(uri.toString()); + Navigator.of(context).push(MaterialPageRoute( + builder: (BuildContext context) => + PkzArchiveScreen(pkzPath: file.path), + )); + } + }); + _loadProperties(); super.initState(); } + @override + void dispose() { + _linkSubscription.cancel(); + super.dispose(); + } + Future _loadProperties() async { var username = await method.getUsername(); var password = await method.getPassword(); diff --git a/lib/screens/AppScreen.dart b/lib/screens/AppScreen.dart index 34dbe71..3332b5a 100644 --- a/lib/screens/AppScreen.dart +++ b/lib/screens/AppScreen.dart @@ -1,8 +1,14 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:app_links/app_links.dart'; import 'package:flutter/material.dart'; import 'package:pikapika/basic/config/Version.dart'; import 'package:pikapika/screens/components/Badge.dart'; +import 'package:uri_to_file/uri_to_file.dart'; import 'CategoriesScreen.dart'; +import 'PkzArchiveScreen.dart'; import 'SpaceScreen.dart'; // MAIN UI 底部导航栏 @@ -14,15 +20,32 @@ class AppScreen extends StatefulWidget { } class _AppScreenState extends State { + late StreamSubscription _linkSubscription; + @override void initState() { versionEvent.subscribe(_onVersion); + final appLinks = AppLinks(); + // todo 不必要cancel 随机监听就好了, APP关闭时销毁, 考虑移动到APP里 + _linkSubscription = appLinks.uriLinkStream.listen((uri) async { + RegExp regExp = RegExp(r"^.*\.pkz$"); + final matches = regExp.allMatches(uri.toString()); + if (matches.isNotEmpty) { + File file = await toFile(uri.toString()); + Navigator.of(context).push(MaterialPageRoute( + builder: (BuildContext context) => + PkzArchiveScreen(pkzPath: file.path), + )); + } + }); + super.initState(); } @override void dispose() { versionEvent.unsubscribe(_onVersion); + _linkSubscription.cancel(); super.dispose(); } diff --git a/lib/screens/CategoriesScreen.dart b/lib/screens/CategoriesScreen.dart index 8038da1..0e7f64c 100644 --- a/lib/screens/CategoriesScreen.dart +++ b/lib/screens/CategoriesScreen.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_search_bar/flutter_search_bar.dart'; import 'package:pikapika/basic/Entities.dart'; import 'package:pikapika/basic/config/ShadowCategoriesEvent.dart'; -import 'package:pikapika/basic/config/shadowCategoriesMode.dart'; +import 'package:pikapika/basic/config/ShadowCategoriesMode.dart'; import 'package:pikapika/basic/store/Categories.dart'; import 'package:pikapika/basic/config/ShadowCategories.dart'; import 'package:pikapika/screens/ComicCollectionsScreen.dart'; diff --git a/lib/screens/ComicsScreen.dart b/lib/screens/ComicsScreen.dart index 123b211..a4f77fc 100644 --- a/lib/screens/ComicsScreen.dart +++ b/lib/screens/ComicsScreen.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_search_bar/flutter_search_bar.dart'; import 'package:pikapika/basic/Common.dart'; import 'package:pikapika/basic/config/ShadowCategories.dart'; -import 'package:pikapika/basic/config/shadowCategoriesMode.dart'; +import 'package:pikapika/basic/config/ShadowCategoriesMode.dart'; import 'package:pikapika/basic/store/Categories.dart'; import 'package:pikapika/basic/config/ListLayout.dart'; import 'package:pikapika/basic/Method.dart'; diff --git a/lib/screens/DownloadExportToFileScreen.dart b/lib/screens/DownloadExportToFileScreen.dart index 9341bd2..05f7dd4 100644 --- a/lib/screens/DownloadExportToFileScreen.dart +++ b/lib/screens/DownloadExportToFileScreen.dart @@ -62,7 +62,7 @@ class _DownloadExportToFileScreenState } @override - Widget build(BuildContext context){ + Widget build(BuildContext context) { return rightClickPop( child: buildScreen(context), context: context, @@ -185,6 +185,59 @@ class _DownloadExportToFileScreenState child: _buildButtonInner('导出到HTML+JPG\n(可直接在相册中打开观看)'), )); widgets.add(Container(height: 10)); + ///////////////////// + widgets.add(MaterialButton( + onPressed: () async { + late String? path; + try { + path = await chooseFolder(context); + } catch (e) { + defaultToast(context, "$e"); + return; + } + var name = ""; + if (currentExportRename()) { + var rename = await inputString( + context, + "请输入保存后的名称", + defaultValue: _task.title, + ); + if (rename != null && rename.isNotEmpty) { + name = rename; + } else { + return; + } + } + print("path $path"); + if (path != null) { + try { + setState(() { + exporting = true; + }); + await method.exportComicDownloadToPkz( + [widget.comicId], + path, + name, + ); + setState(() { + exportResult = "导出成功"; + }); + } catch (e) { + setState(() { + exportResult = "导出失败 $e"; + }); + } finally { + setState(() { + exporting = false; + }); + } + } + }, + child: + _buildButtonInner('导出到xxx.pkz\n(可直接打开观看的格式,不支持导入,可以躲避BD网盘或者TX的检测)'), + )); + widgets.add(Container(height: 10)); + ///////////////////// widgets.add(MaterialButton( onPressed: () async { late String? path; diff --git a/lib/screens/DownloadReaderScreen.dart b/lib/screens/DownloadReaderScreen.dart index 9bc56ce..28d2bd0 100644 --- a/lib/screens/DownloadReaderScreen.dart +++ b/lib/screens/DownloadReaderScreen.dart @@ -76,7 +76,7 @@ class _DownloadReaderScreenState extends State { } FutureOr _onDownload() async { - defaultToast(context, "您已经在下载阅读"); + defaultToast(context, "您阅读的是下载漫画"); } FutureOr _onChangeEp(int epOrder) { diff --git a/lib/screens/InitScreen.dart b/lib/screens/InitScreen.dart index 9152031..69a467f 100644 --- a/lib/screens/InitScreen.dart +++ b/lib/screens/InitScreen.dart @@ -1,3 +1,6 @@ +import 'dart:async'; +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:pikapika/basic/config/Address.dart'; import 'package:pikapika/basic/config/AndroidDisplayMode.dart'; @@ -29,8 +32,10 @@ import 'package:pikapika/basic/config/TimeOffsetHour.dart'; import 'package:pikapika/basic/config/UsingRightClickPop.dart'; import 'package:pikapika/basic/config/Version.dart'; import 'package:pikapika/basic/config/VolumeController.dart'; -import 'package:pikapika/basic/config/shadowCategoriesMode.dart'; - +import 'package:pikapika/basic/config/ShadowCategoriesMode.dart'; +import 'package:pikapika/screens/PkzArchiveScreen.dart'; +import 'package:app_links/app_links.dart'; +import 'package:uri_to_file/uri_to_file.dart'; import '../basic/config/ExportRename.dart'; import 'AccountScreen.dart'; import 'AppScreen.dart'; @@ -88,6 +93,32 @@ class _InitScreenState extends State { await initUsingRightClickPop(); await initAuthentication(); autoCheckNewVersion(); + + final appLinks = AppLinks(); + String? initUrl; + if (Platform.isAndroid || Platform.isIOS) { + try { + initUrl = (await appLinks.getInitialAppLink())?.toString(); + // Use the uri and warn the user, if it is not correct, + // but keep in mind it could be `null`. + } on FormatException { + // Handle exception by warning the user their action did not succeed + // return? + } + } + if (initUrl != null) { + RegExp regExp = RegExp(r"^.*\.pkz$"); + final matches = regExp.allMatches(initUrl!); + if (matches.isNotEmpty) { + File file = await toFile(initUrl!); + Navigator.of(context).pushReplacement(MaterialPageRoute( + builder: (BuildContext context) => + PkzArchiveScreen(pkzPath: file.path), + )); + return; + } + } + setState(() { _authenticating = currentAuthentication(); }); @@ -112,7 +143,8 @@ class _InitScreenState extends State { onPressed: () { _goAuthentication(); }, - child: const Text('您在之前使用APP时开启了身份验证, 请点这段文字进行身份核查, 核查通过后将会进入APP'), + child: + const Text('您在之前使用APP时开启了身份验证, 请点这段文字进行身份核查, 核查通过后将会进入APP'), ), ), ), diff --git a/lib/screens/PkzArchiveScreen.dart b/lib/screens/PkzArchiveScreen.dart new file mode 100644 index 0000000..b0835b4 --- /dev/null +++ b/lib/screens/PkzArchiveScreen.dart @@ -0,0 +1,123 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:path/path.dart' as p; +import 'package:permission_handler/permission_handler.dart'; +import 'package:pikapika/basic/Entities.dart'; +import 'package:pikapika/basic/Method.dart'; +import 'package:pikapika/screens/components/ContentBuilder.dart'; +import 'package:pikapika/screens/components/PkzComicInfoCard.dart'; + +import '../basic/Navigator.dart'; +import 'PkzComicInfoScreen.dart'; + +class PkzArchiveScreen extends StatefulWidget { + final String pkzPath; + + const PkzArchiveScreen({Key? key, required this.pkzPath}) : super(key: key); + + @override + State createState() => _PkzArchiveScreenState(); +} + +class _PkzArchiveScreenState extends State with RouteAware { + Map _logMap = {}; + late String _fileName; + late Future _future; + late PkzArchive _info; + + @override + void initState() { + _fileName = p.basename(widget.pkzPath); + _future = _load(); + super.initState(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + routeObserver.subscribe(this, ModalRoute.of(context)!); + } + + @override + void dispose() { + routeObserver.unsubscribe(this); + super.dispose(); + } + + @override + void didPopNext() { + () async { + var a = await method.pkzComicViewLogs(_fileName, widget.pkzPath); + for (var value in a) { + _logMap[value.lastViewComicId] = value; + } + setState(() {}); + }(); + } + + Future _load() async { + await method.viewPkz(_fileName, widget.pkzPath); + var p = await Permission.storage.request(); + if (!p.isGranted) { + throw 'error permission'; + } + _info = await method.pkzInfo(widget.pkzPath); + if (_info.comics.length == 1) { + Navigator.of(context).pushReplacement(MaterialPageRoute( + builder: (BuildContext context) => PkzComicInfoScreen( + pkzPath: widget.pkzPath, + pkzComic: _info.comics.first, + ), + )); + } + var a = await method.pkzComicViewLogs(_fileName, widget.pkzPath); + for (var value in a) { + _logMap[value.lastViewComicId] = value; + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(_fileName), + ), + body: ContentBuilder( + future: _future, + onRefresh: () async { + setState(() { + _future = _load(); + }); + }, + successBuilder: ( + BuildContext context, + AsyncSnapshot snapshot, + ) { + return ListView(children: [ + ..._info.comics + .map((e) => GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + Navigator.of(context).push(MaterialPageRoute( + builder: (BuildContext context) { + return PkzComicInfoScreen( + pkzComic: e, + pkzPath: widget.pkzPath, + ); + }, + )); + }, + child: PkzComicInfoCard( + info: e, + pkzPath: widget.pkzPath, + displayViewLog: _logMap[e.id], + ), + )) + .toList(), + ]); + }, + ), + ); + } +} diff --git a/lib/screens/PkzComicInfoScreen.dart b/lib/screens/PkzComicInfoScreen.dart new file mode 100644 index 0000000..9d8fed9 --- /dev/null +++ b/lib/screens/PkzComicInfoScreen.dart @@ -0,0 +1,249 @@ +import 'package:flutter/material.dart'; +import 'package:path/path.dart' as p; +import 'package:pikapika/basic/Entities.dart'; +import 'package:pikapika/basic/Method.dart'; +import 'package:pikapika/screens/PkzReaderScreen.dart'; + +import '../basic/Navigator.dart'; +import 'components/PkzComicInfoCard.dart'; + +class PkzComicInfoScreen extends StatefulWidget { + final String pkzPath; + final PkzComic pkzComic; + + const PkzComicInfoScreen( + {Key? key, required this.pkzPath, required this.pkzComic}) + : super(key: key); + + @override + State createState() => _PkzComicInfoScreenState(); +} + +class _PkzComicInfoScreenState extends State + with RouteAware { + PkzComicViewLog? _log; + + @override + void initState() { + _load(); + super.initState(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + routeObserver.subscribe(this, ModalRoute.of(context)!); + } + + @override + void dispose() { + routeObserver.unsubscribe(this); + super.dispose(); + } + + @override + void didPopNext() { + () async { + _log = await method.pkzComicViewLogByPkzNameAndId( + p.basename(widget.pkzPath), + widget.pkzComic.id, + ); + setState(() {}); + }(); + } + + _load() async { + await method.viewPkzComic( + p.basename(widget.pkzPath), + widget.pkzPath, + widget.pkzComic.id, + widget.pkzComic.title, + ); + _log = await method.pkzComicViewLogByPkzNameAndId( + p.basename(widget.pkzPath), + widget.pkzComic.id, + ); + setState(() {}); + } + + @override + Widget build(BuildContext context) { + List chapterButtons = []; + for (var volume in widget.pkzComic.volumes) { + for (var chapter in volume.chapters) { + chapterButtons.add(MaterialButton( + onPressed: () { + Navigator.of(context).push(MaterialPageRoute( + builder: (BuildContext context) { + return PkzReaderScreen( + comicInfo: widget.pkzComic, + currentEpId: chapter.id, + pkzPath: widget.pkzPath, + ); + }, + )); + }, + color: Colors.white, + child: Text( + chapter.title, + style: const TextStyle(color: Colors.black), + ), + )); + } + } + + final theme = Theme.of(context); + return Scaffold( + appBar: AppBar( + title: Text( + widget.pkzComic.title, + ), + ), + body: ListView(children: [ + PkzComicInfoCard(info: widget.pkzComic, pkzPath: widget.pkzPath), + Container( + padding: const EdgeInsets.only(top: 5, bottom: 5), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: theme.dividerColor, + ), + ), + ), + child: Wrap( + children: widget.pkzComic.tags.map((e) { + return Container( + padding: const EdgeInsets.only( + left: 10, + right: 10, + top: 3, + bottom: 3, + ), + margin: const EdgeInsets.only( + left: 5, + right: 5, + top: 3, + bottom: 3, + ), + decoration: BoxDecoration( + color: Colors.pink.shade100, + border: Border.all( + style: BorderStyle.solid, + color: Colors.pink.shade400, + ), + borderRadius: const BorderRadius.all(Radius.circular(30)), + ), + child: Text( + e, + style: TextStyle( + color: Colors.pink.shade500, + height: 1.4, + ), + strutStyle: const StrutStyle( + height: 1.4, + ), + ), + ); + }).toList(), + ), + ), + Container( + padding: const EdgeInsets.only( + top: 5, + bottom: 5, + left: 10, + right: 10, + ), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: theme.dividerColor, + ), + ), + ), + child: SelectableText( + widget.pkzComic.description, + style: const TextStyle( + fontSize: 13, + color: Colors.grey, + ), + ), + ), + LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + PkzChapter? first; + Map chapters = {}; + for (var vol in widget.pkzComic.volumes) { + for (var c in vol.chapters) { + first ??= c; + chapters[c.id] = (c); + } + } + print("chapters : ${chapters}"); + if (chapters.isEmpty) { + return Container(); + } + final width = constraints.maxWidth; + return Container( + padding: const EdgeInsets.only(left: 10, right: 10), + margin: const EdgeInsets.only(bottom: 10), + width: width, + child: MaterialButton( + onPressed: () { + if (chapters.containsKey(_log?.lastViewEpId)) { + Navigator.of(context).push(MaterialPageRoute( + builder: (BuildContext context) { + return PkzReaderScreen( + comicInfo: widget.pkzComic, + currentEpId: _log!.lastViewEpId, + pkzPath: widget.pkzPath, + initPicturePosition: _log!.lastViewPictureRank, + ); + }, + )); + return; + } + Navigator.of(context).push(MaterialPageRoute( + builder: (BuildContext context) { + return PkzReaderScreen( + comicInfo: widget.pkzComic, + currentEpId: first!.id, + pkzPath: widget.pkzPath, + ); + }, + )); + }, + child: Row( + children: [ + Expanded( + child: Container( + color: Theme.of(context) + .textTheme + .bodyText1! + .color! + .withOpacity(.05), + padding: const EdgeInsets.all(10), + child: Text( + chapters.containsKey(_log?.lastViewEpId) + ? "继续阅读 ${chapters[_log?.lastViewEpId]!.title}" + : "开始阅读", + textAlign: TextAlign.center, + ), + ), + ) + ], + ), + ), + ); + }, + ), + Wrap( + spacing: 10, + runSpacing: 10, + alignment: WrapAlignment.spaceAround, + children: chapterButtons, + ), + ]), + ); + } +} diff --git a/lib/screens/PkzReaderScreen.dart b/lib/screens/PkzReaderScreen.dart new file mode 100644 index 0000000..8414755 --- /dev/null +++ b/lib/screens/PkzReaderScreen.dart @@ -0,0 +1,253 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:path/path.dart' as p; +import 'package:pikapika/basic/Common.dart'; +import 'package:pikapika/basic/Entities.dart'; +import 'package:pikapika/basic/config/AutoFullScreen.dart'; +import 'package:pikapika/basic/config/FullScreenUI.dart'; +import 'package:pikapika/basic/config/ReaderDirection.dart'; +import 'package:pikapika/basic/config/ReaderType.dart'; +import 'package:pikapika/basic/Method.dart'; +import 'components/ContentError.dart'; +import 'components/ContentLoading.dart'; +import 'components/ImageReader.dart'; +import 'components/RightClickPop.dart'; + +// 阅读下载的内容 +class PkzReaderScreen extends StatefulWidget { + final String pkzPath; + final PkzComic comicInfo; + late final List epList; + final String currentEpId; + final int? initPicturePosition; + final ReaderType pagerType = currentReaderType(); + final ReaderDirection pagerDirection = gReaderDirection; + late final bool autoFullScreen; + + PkzReaderScreen({ + Key? key, + required this.comicInfo, + required this.currentEpId, + this.initPicturePosition, + bool? autoFullScreen, + required this.pkzPath, + }) : super(key: key) { + epList = []; + for (var volume in comicInfo.volumes) { + for (var chapter in volume.chapters) { + epList.add(chapter); + } + } + this.autoFullScreen = autoFullScreen ?? currentAutoFullScreen(); + } + + @override + State createState() => _PkzReaderScreenState(); +} + +class _PkzReaderScreenState extends State { + late PkzChapter _ep; + late int _epOrder; + late bool _fullScreen = false; + late List pictures = []; + late Future _future = _load(); + int? _lastChangeRank; + bool _replacement = false; + + @override + void initState() { + // EP + pictures.clear(); + for (var ep in widget.epList) { + if (ep.id == widget.currentEpId) { + _ep = ep; + _epOrder = widget.epList.indexOf(ep); + pictures.addAll(ep.pictures); + break; + } + } + if (widget.autoFullScreen) { + setState(() { + SystemChrome.setEnabledSystemUIMode( + SystemUiMode.manual, + overlays: [], + ); + _fullScreen = true; + }); + } + // INIT + _future = _load(); + super.initState(); + } + + @override + void dispose() { + if (!_replacement) { + switchFullScreenUI(); + } + super.dispose(); + } + + Future _load() async { + if (widget.initPicturePosition == null) { + await method.viewPkzEpAndPicture( + p.basename(widget.pkzPath), + widget.pkzPath, + widget.comicInfo.id, + widget.comicInfo.title, + _ep.id, + _ep.title, + 0, + ); + } + } + + Future _onPositionChange(int position) async { + _lastChangeRank = position; + await method.viewPkzEpAndPicture( + p.basename(widget.pkzPath), + widget.pkzPath, + widget.comicInfo.id, + widget.comicInfo.title, + _ep.id, + _ep.title, + position, + ); + return; + } + + FutureOr _onDownload() async { + defaultToast(context, "您阅读的是下载漫画"); + } + + FutureOr _onChangeEp(int epOrder) { + final ep = widget.epList[epOrder]; + _replacement = true; + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (context) => PkzReaderScreen( + comicInfo: widget.comicInfo, + pkzPath: widget.pkzPath, + currentEpId: ep.id, + autoFullScreen: _fullScreen, + ), + ), + ); + } + + FutureOr _onReloadEp() { + _replacement = true; + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (context) => PkzReaderScreen( + comicInfo: widget.comicInfo, + currentEpId: widget.currentEpId, + initPicturePosition: _lastChangeRank ?? widget.initPicturePosition, + // maybe null + autoFullScreen: _fullScreen, + pkzPath: widget.pkzPath, + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return rightClickPop( + child: buildScreen(context), + context: context, + canPop: true, + ); + } + + Widget buildScreen(BuildContext context) { + return readerKeyboardHolder(_build(context)); + } + + Widget _build(BuildContext context) { + return FutureBuilder( + future: _future, + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.hasError) { + return Scaffold( + appBar: _fullScreen + ? null + : AppBar( + title: Text("${_ep.title} - ${widget.comicInfo.title}"), + ), + body: ContentError( + error: snapshot.error, + stackTrace: snapshot.stackTrace, + onRefresh: () async { + setState(() { + _future = _load(); + }); + }, + ), + ); + } + if (snapshot.connectionState != ConnectionState.done) { + return Scaffold( + appBar: _fullScreen + ? null + : AppBar( + title: Text("${_ep.title} - ${widget.comicInfo.title}"), + ), + body: const ContentLoading(label: '加载中'), + ); + } + var epNameMap = {}; + for (var i = 0; i < widget.epList.length; i++) { + epNameMap[i] = widget.epList[i].title; + } + return Scaffold( + body: ImageReader( + ImageReaderStruct( + images: pictures + .map((e) => ReaderImageInfo( + "", + "", + "", + e.width, + e.height, + e.format, + 0, + pkzFile: PkzFile(widget.pkzPath, e.picturePath), + )) + .toList(), + fullScreen: _fullScreen, + onFullScreenChange: _onFullScreenChange, + onPositionChange: _onPositionChange, + initPosition: widget.initPicturePosition, + epOrder: _epOrder, + epNameMap: epNameMap, + comicTitle: widget.comicInfo.title, + onReloadEp: _onReloadEp, + onChangeEp: _onChangeEp, + onDownload: _onDownload, + ), + ), + ); + }, + ); + } + + Future _onFullScreenChange(bool fullScreen) async { + setState(() { + if (fullScreen) { + if (Platform.isAndroid || Platform.isIOS) { + SystemChrome.setEnabledSystemUIMode( + SystemUiMode.manual, + overlays: [], + ); + } + } else { + switchFullScreenUI(); + } + _fullScreen = fullScreen; + }); + } +} diff --git a/lib/screens/SettingsScreen.dart b/lib/screens/SettingsScreen.dart index 9b53d8e..bd28fe8 100644 --- a/lib/screens/SettingsScreen.dart +++ b/lib/screens/SettingsScreen.dart @@ -25,7 +25,7 @@ import 'package:pikapika/basic/config/Themes.dart'; import 'package:pikapika/basic/config/TimeOffsetHour.dart'; import 'package:pikapika/basic/config/Version.dart'; import 'package:pikapika/basic/config/VolumeController.dart'; -import 'package:pikapika/basic/config/shadowCategoriesMode.dart'; +import 'package:pikapika/basic/config/ShadowCategoriesMode.dart'; import 'package:pikapika/screens/components/NetworkSetting.dart'; import 'package:pikapika/screens/components/RightClickPop.dart'; diff --git a/lib/screens/components/ComicInfoCard.dart b/lib/screens/components/ComicInfoCard.dart index c7310d9..3b8fcf0 100644 --- a/lib/screens/components/ComicInfoCard.dart +++ b/lib/screens/components/ComicInfoCard.dart @@ -367,3 +367,8 @@ final authorStyle = TextStyle( fontSize: 13, color: Colors.pink.shade300, ); + +final authorStyleX = TextStyle( + fontSize: 13, + color: Colors.pink.shade300.withOpacity(.7), +); diff --git a/lib/screens/components/ComicList.dart b/lib/screens/components/ComicList.dart index df61515..1bbf9a7 100644 --- a/lib/screens/components/ComicList.dart +++ b/lib/screens/components/ComicList.dart @@ -7,7 +7,7 @@ import 'package:pikapika/basic/Entities.dart'; import 'package:pikapika/basic/Method.dart'; import 'package:pikapika/basic/config/ShadowCategories.dart'; import 'package:pikapika/basic/config/ListLayout.dart'; -import 'package:pikapika/basic/config/shadowCategoriesMode.dart'; +import 'package:pikapika/basic/config/ShadowCategoriesMode.dart'; import 'ComicInfoCard.dart'; import 'Images.dart'; diff --git a/lib/screens/components/ImageReader.dart b/lib/screens/components/ImageReader.dart index 23042bc..f6a856a 100644 --- a/lib/screens/components/ImageReader.dart +++ b/lib/screens/components/ImageReader.dart @@ -21,6 +21,7 @@ import 'package:pikapika/basic/config/ReaderDirection.dart'; import 'package:pikapika/basic/config/ReaderSliderPosition.dart'; import 'package:pikapika/basic/config/ReaderType.dart'; import 'package:pikapika/basic/config/VolumeController.dart'; +import 'package:pikapika/screens/components/PkzImages.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import '../FilePhotoViewScreen.dart'; import 'gesture_zoom_box.dart'; @@ -30,7 +31,7 @@ import 'Images.dart'; /////////////// Event<_ReaderControllerEventArgs> _readerControllerEvent = -Event<_ReaderControllerEventArgs>(); + Event<_ReaderControllerEventArgs>(); class _ReaderControllerEventArgs extends EventArgs { final String key; @@ -93,6 +94,13 @@ void delVolumeListen() { // 对Reader的传参以及封装 +class PkzFile { + final String pkzPath; + final String path; + + PkzFile(this.pkzPath, this.path); +} + class ReaderImageInfo { final String fileServer; final String path; @@ -101,9 +109,18 @@ class ReaderImageInfo { final int? height; final String? format; final int? fileSize; + final PkzFile? pkzFile; - ReaderImageInfo(this.fileServer, this.path, this.downloadLocalPath, - this.width, this.height, this.format, this.fileSize); + ReaderImageInfo( + this.fileServer, + this.path, + this.downloadLocalPath, + this.width, + this.height, + this.format, + this.fileSize, { + this.pkzFile, + }); } class ImageReaderStruct { @@ -156,7 +173,7 @@ class _ImageReaderState extends State { late final FullScreenAction _fullScreenAction = currentFullScreenAction(); late final ReaderSliderPosition _readerSliderPosition = - currentReaderSliderPosition(); + currentReaderSliderPosition(); @override Widget build(BuildContext context) { @@ -186,12 +203,12 @@ class _ImageReaderContent extends StatefulWidget { final ImageReaderStruct struct; const _ImageReaderContent( - this.struct, - this.pagerDirection, - this.pagerType, - this.fullScreenAction, - this.readerSliderPosition, - ); + this.struct, + this.pagerDirection, + this.pagerType, + this.fullScreenAction, + this.readerSliderPosition, + ); @override State createState() { @@ -290,7 +307,7 @@ abstract class _ImageReaderContentState extends State<_ImageReaderContent> { @override Widget build(BuildContext context) { switch (currentFullScreenAction()) { - // 按钮 + // 按钮 case FullScreenAction.CONTROLLER: return Stack( children: [ @@ -339,37 +356,37 @@ abstract class _ImageReaderContentState extends State<_ImageReaderContent> { widget.struct.fullScreen ? Container() : Container( - height: 45, - color: const Color(0x88000000), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Container(width: 15), - IconButton( - icon: const Icon(Icons.fullscreen), - color: Colors.white, - onPressed: () { - widget.struct - .onFullScreenChange(!widget.struct.fullScreen); - }, + height: 45, + color: const Color(0x88000000), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container(width: 15), + IconButton( + icon: const Icon(Icons.fullscreen), + color: Colors.white, + onPressed: () { + widget.struct + .onFullScreenChange(!widget.struct.fullScreen); + }, + ), + Container(width: 10), + Expanded( + child: + widget.pagerType != ReaderType.WEB_TOON_FREE_ZOOM + ? _buildSliderBottom() + : Container(), + ), + Container(width: 10), + IconButton( + icon: const Icon(Icons.skip_next_outlined), + color: Colors.white, + onPressed: _onNextAction, + ), + Container(width: 15), + ], + ), ), - Container(width: 10), - Expanded( - child: - widget.pagerType != ReaderType.WEB_TOON_FREE_ZOOM - ? _buildSliderBottom() - : Container(), - ), - Container(width: 10), - IconButton( - icon: const Icon(Icons.skip_next_outlined), - color: Colors.white, - onPressed: _onNextAction, - ), - Container(width: 15), - ], - ), - ), ], ); case ReaderSliderPosition.RIGHT: @@ -406,19 +423,19 @@ abstract class _ImageReaderContentState extends State<_ImageReaderContent> { Widget _buildAppBar() => widget.struct.fullScreen ? Container() : AppBar( - title: Text( - "${widget.struct.epNameMap[widget.struct.epOrder] ?? ""} - ${widget.struct.comicTitle}"), - actions: [ - IconButton( - onPressed: _onChooseEp, - icon: const Icon(Icons.menu_open), - ), - IconButton( - onPressed: _onMoreSetting, - icon: const Icon(Icons.more_horiz), - ), - ], - ); + title: Text( + "${widget.struct.epNameMap[widget.struct.epOrder] ?? ""} - ${widget.struct.comicTitle}"), + actions: [ + IconButton( + onPressed: _onChooseEp, + icon: const Icon(Icons.menu_open), + ), + IconButton( + onPressed: _onMoreSetting, + icon: const Icon(Icons.more_horiz), + ), + ], + ); Widget _buildSliderBottom() { return Column( @@ -436,52 +453,52 @@ abstract class _ImageReaderContentState extends State<_ImageReaderContent> { Widget _buildSliderLeft() => widget.struct.fullScreen ? Container() : Align( - alignment: Alignment.centerLeft, - child: Material( - color: Colors.transparent, - child: Container( - width: 35, - height: 300, - decoration: const BoxDecoration( - color: Color(0x66000000), - borderRadius: BorderRadius.only( - topRight: Radius.circular(10), - bottomRight: Radius.circular(10), + alignment: Alignment.centerLeft, + child: Material( + color: Colors.transparent, + child: Container( + width: 35, + height: 300, + decoration: const BoxDecoration( + color: Color(0x66000000), + borderRadius: BorderRadius.only( + topRight: Radius.circular(10), + bottomRight: Radius.circular(10), + ), + ), + padding: + const EdgeInsets.only(top: 10, bottom: 10, left: 6, right: 5), + child: Center( + child: _buildSliderWidget(Axis.vertical), + ), + ), ), - ), - padding: - const EdgeInsets.only(top: 10, bottom: 10, left: 6, right: 5), - child: Center( - child: _buildSliderWidget(Axis.vertical), - ), - ), - ), - ); + ); Widget _buildSliderRight() => widget.struct.fullScreen ? Container() : Align( - alignment: Alignment.centerRight, - child: Material( - color: Colors.transparent, - child: Container( - width: 35, - height: 300, - decoration: const BoxDecoration( - color: Color(0x66000000), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(10), - bottomLeft: Radius.circular(10), + alignment: Alignment.centerRight, + child: Material( + color: Colors.transparent, + child: Container( + width: 35, + height: 300, + decoration: const BoxDecoration( + color: Color(0x66000000), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(10), + bottomLeft: Radius.circular(10), + ), + ), + padding: + const EdgeInsets.only(top: 10, bottom: 10, left: 5, right: 6), + child: Center( + child: _buildSliderWidget(Axis.vertical), + ), + ), ), - ), - padding: - const EdgeInsets.only(top: 10, bottom: 10, left: 5, right: 6), - child: Center( - child: _buildSliderWidget(Axis.vertical), - ), - ), - ), - ); + ); Widget _buildSliderWidget(Axis axis) { return FlutterSlider( @@ -544,7 +561,7 @@ abstract class _ImageReaderContentState extends State<_ImageReaderContent> { color: Colors.transparent, child: Container( padding: - const EdgeInsets.only(left: 10, right: 10, top: 4, bottom: 4), + const EdgeInsets.only(left: 10, right: 10, top: 4, bottom: 4), margin: const EdgeInsets.only(bottom: 10), decoration: const BoxDecoration( borderRadius: BorderRadius.only( @@ -930,7 +947,13 @@ class _WebToonReaderState extends _ImageReaderContentState { @override void initState() { for (var e in widget.struct.images) { - if (e.downloadLocalPath != null) { + if (e.pkzFile != null && + e.width != null && + e.height != null && + e.width! > 0 && + e.height! > 0) { + _trueSizes.add(Size(e.width!.toDouble(), e.height!.toDouble())); + } else if (e.downloadLocalPath != null) { _trueSizes.add(Size(e.width!.toDouble(), e.height!.toDouble())); } else { _trueSizes.add(null); @@ -1030,7 +1053,16 @@ class _WebToonReaderState extends _ImageReaderContentState { } var e = widget.struct.images[index]; - if (e.downloadLocalPath != null) { + if (e.pkzFile != null) { + _images.add(_WebToonPkzImage( + width: e.width!, + height: e.height!, + format: e.format!, + size: renderSize, + onTrueSize: onTrueSize, + pkzFile: e.pkzFile!, + )); + } else if (e.downloadLocalPath != null) { _images.add(_WebToonDownloadImage( fileServer: e.fileServer, path: e.path, @@ -1054,9 +1086,9 @@ class _WebToonReaderState extends _ImageReaderContentState { return ScrollablePositionedList.builder( initialScrollIndex: super._startIndex, scrollDirection: - widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM - ? Axis.vertical - : Axis.horizontal, + widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM + ? Axis.vertical + : Axis.horizontal, reverse: widget.pagerDirection == ReaderDirection.RIGHT_TO_LEFT, padding: EdgeInsets.only( // 不管全屏与否, 滚动方向如何, 顶部永远保持间距 @@ -1064,9 +1096,9 @@ class _WebToonReaderState extends _ImageReaderContentState { bottom: widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM ? 130 // 纵向滚动 底部永远都是130的空白 : ( // 横向滚动 - widget.struct.fullScreen - ? super._topBarHeight() // 全屏时底部和顶部到屏幕边框距离一样保持美观 - : super._bottomBarHeight()) + widget.struct.fullScreen + ? super._topBarHeight() // 全屏时底部和顶部到屏幕边框距离一样保持美观 + : super._bottomBarHeight()) // 非全屏时, 顶部去掉顶部BAR的高度, 底部去掉底部BAR的高度, 形成看似填充的效果 , ), @@ -1147,17 +1179,47 @@ class _WebToonDownloadImage extends _WebToonReaderImage { } } +// 来自PKZ +class _WebToonPkzImage extends StatelessWidget { + final PkzFile pkzFile; + final int width; + final int height; + final String format; + final Size size; + Function(Size)? onTrueSize; + + _WebToonPkzImage({ + required this.pkzFile, + required this.width, + required this.height, + required this.format, + required this.size, + required this.onTrueSize, + }); + + @override + Widget build(BuildContext context) { + return PkzLoadingImage( + pkzPath: pkzFile.pkzPath, + path: pkzFile.path, + width: size.width, + height: size.height, + onTrueSize: onTrueSize, + ); + } +} + // 来自远端 class _WebToonRemoteImage extends _WebToonReaderImage { final String fileServer; final String path; _WebToonRemoteImage( - this.fileServer, - this.path, - Size size, - Function(Size)? onTrueSize, - ) : super(size, onTrueSize); + this.fileServer, + this.path, + Size size, + Function(Size)? onTrueSize, + ) : super(size, onTrueSize); @override Future imageData() async { @@ -1197,14 +1259,14 @@ class _WebToonReaderImageState extends State<_WebToonReaderImage> { return FutureBuilder( future: _future, builder: ( - BuildContext context, - AsyncSnapshot snapshot, - ) { + BuildContext context, + AsyncSnapshot snapshot, + ) { if (snapshot.hasError) { return GestureDetector( onLongPress: () async { String? choose = - await chooseListDialog(context, '请选择', ['重新加载图片']); + await chooseListDialog(context, '请选择', ['重新加载图片']); switch (choose) { case '重新加载图片': setState(() { @@ -1257,7 +1319,13 @@ class _ListViewReaderState extends _ImageReaderContentState @override void initState() { for (var e in widget.struct.images) { - if (e.downloadLocalPath != null) { + if (e.pkzFile != null && + e.width != null && + e.height != null && + e.width! > 0 && + e.height! > 0) { + _trueSizes.add(Size(e.width!.toDouble(), e.height!.toDouble())); + } else if (e.downloadLocalPath != null) { _trueSizes.add(Size(e.width!.toDouble(), e.height!.toDouble())); } else { _trueSizes.add(null); @@ -1332,7 +1400,16 @@ class _ListViewReaderState extends _ImageReaderContentState } var e = widget.struct.images[index]; - if (e.downloadLocalPath != null) { + if (e.pkzFile != null) { + _images.add(_WebToonPkzImage( + width: e.width!, + height: e.height!, + format: e.format!, + size: renderSize, + onTrueSize: onTrueSize, + pkzFile: e.pkzFile!, + )); + } else if (e.downloadLocalPath != null) { _images.add(_WebToonDownloadImage( fileServer: e.fileServer, path: e.path, @@ -1355,9 +1432,9 @@ class _ListViewReaderState extends _ImageReaderContentState } var list = ListView.builder( scrollDirection: - widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM - ? Axis.vertical - : Axis.horizontal, + widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM + ? Axis.vertical + : Axis.horizontal, reverse: widget.pagerDirection == ReaderDirection.RIGHT_TO_LEFT, padding: EdgeInsets.only( // 不管全屏与否, 滚动方向如何, 顶部永远保持间距 @@ -1365,9 +1442,9 @@ class _ListViewReaderState extends _ImageReaderContentState bottom: widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM ? 130 // 纵向滚动 底部永远都是130的空白 : ( // 横向滚动 - widget.struct.fullScreen - ? super._topBarHeight() // 全屏时底部和顶部到屏幕边框距离一样保持美观 - : super._bottomBarHeight()) + widget.struct.fullScreen + ? super._topBarHeight() // 全屏时底部和顶部到屏幕边框距离一样保持美观 + : super._bottomBarHeight()) // 非全屏时, 顶部去掉顶部BAR的高度, 底部去掉底部BAR的高度, 形成看似填充的效果 , ), @@ -1499,10 +1576,26 @@ class _GalleryReaderState extends _ImageReaderContentState { itemCount: widget.struct.images.length, builder: (BuildContext context, int index) { var item = widget.struct.images[index]; + if (item.pkzFile != null) { + return PhotoViewGalleryPageOptions( + imageProvider: + PkzImageProvider(item.pkzFile!.pkzPath, item.pkzFile!.path), + errorBuilder: (b, e, s) { + print("$e,$s"); + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return buildError( + constraints.maxWidth, constraints.maxHeight); + }, + ); + }, + filterQuality: FilterQuality.high, + ); + } if (item.downloadLocalPath != null) { return PhotoViewGalleryPageOptions( imageProvider: - ResourceDownloadFileImageProvider(item.downloadLocalPath!), + ResourceDownloadFileImageProvider(item.downloadLocalPath!), errorBuilder: (b, e, s) { print("$e,$s"); return LayoutBuilder( @@ -1517,7 +1610,7 @@ class _GalleryReaderState extends _ImageReaderContentState { } return PhotoViewGalleryPageOptions( imageProvider: - ResourceRemoteImageProvider(item.fileServer, item.path), + ResourceRemoteImageProvider(item.fileServer, item.path), errorBuilder: (b, e, s) { print("$e,$s"); return LayoutBuilder( @@ -1535,6 +1628,11 @@ class _GalleryReaderState extends _ImageReaderContentState { child: gallery, onLongPress: () async { if (_current >= 0 && _current < widget.struct.images.length) { + var item = widget.struct.images[_current]; + if (item.pkzFile != null) { + return; + } + Future load() async { var item = widget.struct.images[_current]; if (item.downloadLocalPath != null) { @@ -1545,7 +1643,7 @@ class _GalleryReaderState extends _ImageReaderContentState { } String? choose = - await chooseListDialog(context, '请选择', ['预览图片', '保存图片']); + await chooseListDialog(context, '请选择', ['预览图片', '保存图片']); switch (choose) { case '预览图片': try { @@ -1594,7 +1692,7 @@ class _GalleryReaderState extends _ImageReaderContentState { child: Container( margin: const EdgeInsets.only(bottom: 10), padding: - const EdgeInsets.only(left: 10, right: 10, top: 4, bottom: 4), + const EdgeInsets.only(left: 10, right: 10, top: 4, bottom: 4), decoration: const BoxDecoration( borderRadius: BorderRadius.only( topLeft: Radius.circular(10), @@ -1619,4 +1717,4 @@ class _GalleryReaderState extends _ImageReaderContentState { } } -/////////////////////////////////////////////////////////////////////////////// \ No newline at end of file +/////////////////////////////////////////////////////////////////////////////// diff --git a/lib/screens/components/PkzComicInfoCard.dart b/lib/screens/components/PkzComicInfoCard.dart new file mode 100644 index 0000000..3d05888 --- /dev/null +++ b/lib/screens/components/PkzComicInfoCard.dart @@ -0,0 +1,138 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:pikapika/basic/Cross.dart'; +import 'package:pikapika/basic/Entities.dart'; +import 'package:pikapika/basic/Method.dart'; +import 'package:pikapika/screens/SearchScreen.dart'; +import 'package:pikapika/basic/Navigator.dart'; + +import 'ComicInfoCard.dart'; +import 'PkzImages.dart'; + +// 漫画卡片 +class PkzComicInfoCard extends StatefulWidget { + final String pkzPath; + final PkzComic info; + final bool linkItem; + final PkzComicViewLog? displayViewLog; + + const PkzComicInfoCard({ + required this.info, + required this.pkzPath, + this.linkItem = false, + Key? key, + this.displayViewLog, + }) : super(key: key); + + @override + State createState() => _ComicInfoCard(); +} + +class _ComicInfoCard extends State { + @override + Widget build(BuildContext context) { + var info = widget.info; + final theme = Theme.of(context); + return Container( + padding: const EdgeInsets.all(5), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: theme.dividerColor, + ), + ), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.only(right: 10), + child: PkzImage( + pkzPath: widget.pkzPath, + path: info.coverPath, + width: imageWidth, + height: imageHeight, + ), + ), + Expanded( + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + widget.linkItem + ? GestureDetector( + onLongPress: () { + confirmCopy(context, info.title); + }, + child: Text(info.title, style: titleStyle), + ) + : Text(info.title, style: titleStyle), + Container(height: 5), + widget.linkItem + ? InkWell( + onTap: () { + // todo + }, + onLongPress: () { + confirmCopy(context, info.author); + }, + child: Text(info.author, style: authorStyle), + ) + : Text(info.author, style: authorStyle), + Container(height: 5), + Text.rich( + widget.linkItem + ? TextSpan( + children: [ + const TextSpan(text: '分类 :'), + ...info.categories.map( + (e) => TextSpan( + children: [ + const TextSpan(text: ' '), + TextSpan( + text: e, + recognizer: TapGestureRecognizer() + ..onTap = () { + // todo + }), + ], + ), + ), + ], + ) + : TextSpan( + text: "分类 : ${info.categories.join(' ')}"), + style: TextStyle( + fontSize: 13, + color: Theme.of(context) + .textTheme + .bodyText1! + .color! + .withAlpha(0xCC), + ), + ), + Container(height: 5), + widget.displayViewLog != null && + widget.displayViewLog!.lastViewEpId.isNotEmpty + ? Container( + padding: EdgeInsets.only(bottom: 5), + child: Text( + "上次观看到 ${widget.displayViewLog!.lastViewEpName}", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: authorStyleX, + ), + ) + : Container(), + ], + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/components/PkzImages.dart b/lib/screens/components/PkzImages.dart new file mode 100644 index 0000000..9d3ee71 --- /dev/null +++ b/lib/screens/components/PkzImages.dart @@ -0,0 +1,176 @@ +import 'dart:typed_data'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:pikapika/basic/Common.dart'; +import 'package:pikapika/basic/Cross.dart'; +import 'package:pikapika/basic/Method.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:pikapika/basic/config/ImageAddress.dart'; +import 'dart:io'; +import 'dart:ui' as ui show Codec; + +import '../FilePhotoViewScreen.dart'; +import 'Images.dart'; + +// 从本地加载图片 +class PkzImageProvider extends ImageProvider { + final String pkzPath; + final String path; + final double scale; + + PkzImageProvider(this.pkzPath, this.path, {this.scale = 1.0}); + + @override + ImageStreamCompleter load(PkzImageProvider key, DecoderCallback decode) { + return MultiFrameImageStreamCompleter( + codec: _loadAsync(key), + scale: key.scale, + ); + } + + @override + Future obtainKey(ImageConfiguration configuration) { + return SynchronousFuture(this); + } + + Future _loadAsync(PkzImageProvider key) async { + assert(key == this); + return PaintingBinding.instance!.instantiateImageCodec( + await method.loadPkzFile(pkzPath, path), + ); + } + + @override + bool operator ==(dynamic other) { + if (other.runtimeType != runtimeType) return false; + final PkzImageProvider typedOther = other; + return pkzPath == typedOther.pkzPath && + path == typedOther.path && + scale == typedOther.scale; + } + + @override + int get hashCode => hashValues(path, scale); + + @override + String toString() => '$runtimeType(' + ' pkzPath: ${describeIdentity(pkzPath)},' + ' path: ${describeIdentity(path)},' + ' scale: $scale' + ')'; +} + +// 远端图片 +class PkzImage extends StatefulWidget { + final String pkzPath; + final String path; + final double? width; + final double? height; + final BoxFit fit; + + const PkzImage({ + Key? key, + required this.pkzPath, + required this.path, + this.width, + this.height, + this.fit = BoxFit.cover, + }) : super(key: key); + + @override + State createState() => _PkzImageState(); +} + +class _PkzImageState extends State { + late bool _mock; + + @override + void initState() { + _mock = widget.path == ""; + super.initState(); + } + + @override + Widget build(BuildContext context) { + if (_mock) { + return buildMock(widget.width, widget.height); + } + return Image( + image: PkzImageProvider(widget.pkzPath, widget.path), + width: widget.width, + height: widget.height, + errorBuilder: (a, b, c) { + print("$b"); + print("$c"); + return buildError(widget.width, widget.height); + }, + fit: widget.fit, + ); + } +} + + +// 远端图片 +class PkzLoadingImage extends StatefulWidget { + final String pkzPath; + final String path; + final double? width; + final double? height; + final BoxFit fit; + final Function(Size)? onTrueSize; + + const PkzLoadingImage({ + Key? key, + required this.pkzPath, + required this.path, + this.width, + this.height, + this.fit = BoxFit.cover, + this.onTrueSize, + }) : super(key: key); + + @override + State createState() => _PkzLoadingImageState(); +} + +class _PkzLoadingImageState extends State { + + late bool _mock; + late Future data; + + @override + void initState() { + _mock = widget.path == ""; + if (!_mock) { + data = () async { + final data = await method.loadPkzFile(widget.pkzPath, widget.path); + if (widget.onTrueSize != null) { + var decodedImage = await decodeImageFromList(data); + widget.onTrueSize!(Size( + decodedImage.width.toDouble(), decodedImage.height.toDouble(),),); + } + return data; + }(); + } + super.initState(); + } + + @override + Widget build(BuildContext context) { + if (_mock) { + return buildMock(widget.width, widget.height); + } + return Image( + image: PkzImageProvider(widget.pkzPath, widget.path), + width: widget.width, + height: widget.height, + errorBuilder: (a, b, c) { + print("$b"); + print("$c"); + return buildError(widget.width, widget.height); + }, + fit: widget.fit, + ); + } +} diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 8236f57..a1eb7ee 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,8 +5,10 @@ import FlutterMacOS import Foundation +import app_links_macos import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 7cfbb8d..f4dc71f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -8,6 +8,41 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.1+2" + app_links: + dependency: "direct main" + description: + name: app_links + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.0" + app_links_macos: + dependency: transitive + description: + name: app_links_macos + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + app_links_platform_interface: + dependency: transitive + description: + name: app_links_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + app_links_web: + dependency: transitive + description: + name: app_links_web + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + app_links_windows: + dependency: transitive + description: + name: app_links_windows + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" archive: dependency: transitive description: @@ -171,7 +206,7 @@ packages: name: flutter_svg url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" flutter_test: dependency: "direct dev" description: flutter @@ -300,7 +335,7 @@ packages: name: modal_bottom_sheet url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.1.0" multi_select_flutter: dependency: "direct main" description: @@ -309,7 +344,7 @@ packages: source: hosted version: "4.1.2" path: - dependency: transitive + dependency: "direct main" description: name: path url: "https://pub.dartlang.org" @@ -432,13 +467,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.1" + uri_to_file: + dependency: "direct main" + description: + name: uri_to_file + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0" url_launcher: dependency: "direct main" description: name: url_launcher url: "https://pub.dartlang.org" source: hosted - version: "6.1.3" + version: "6.1.4" url_launcher_android: dependency: transitive description: @@ -511,4 +553,4 @@ packages: version: "6.1.0" sdks: dart: ">=2.17.0 <3.0.0" - flutter: ">=2.11.0-0.1.pre" + flutter: ">=3.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 6f15ccd..c667fc8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -47,6 +47,9 @@ dependencies: file_picker: ^4.5.1 crop_image: ^1.0.2 image: ^3.1.3 + path: ^1.8.0 + app_links: ^3.2.0 + uri_to_file: ^0.2.0 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 4f78848..96d0d6d 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,9 +6,12 @@ #include "generated_plugin_registrant.h" +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + AppLinksWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("AppLinksWindowsPlugin")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 88b22e5..9a732f7 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + app_links_windows url_launcher_windows )