This commit is contained in:
niuhuan 2022-06-30 03:02:01 +08:00
parent b164b74e31
commit cc5b16ee33
25 changed files with 1804 additions and 140 deletions

View File

@ -48,6 +48,19 @@
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:mimeType="*/*"
android:pathPattern=".*\\.pkz"
android:scheme="content" />
<data
android:mimeType="*/*"
android:pathPattern=".*\\.pkz"
android:scheme="file" />
</intent-filter>
</activity> </activity>
<!-- Don't delete the meta-data below. <!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --> This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->

View File

@ -14,7 +14,6 @@ import (
) )
func main() { func main() {
return
// get version // get version
var version commons.Version var version commons.Version
codeFile, err := ioutil.ReadFile("version.code.txt") codeFile, err := ioutil.ReadFile("version.code.txt")

View File

@ -687,3 +687,328 @@ class Collection {
.toList(); .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<PkzComic> comics;
late final int comicCount;
late final int volumesCount;
late final int chapterCount;
late final int pictureCount;
PkzArchive.fromJson(Map<String, dynamic> 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<String, dynamic> toJson() {
final _data = <String, dynamic>{};
_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<String> categories;
late final List<String> 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<PkzVolume> 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<String, dynamic> json) {
id = json['id'];
title = json['title'];
categories = List.castFrom<dynamic, String>(json['categories']);
tags = List.castFrom<dynamic, String>(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<String, dynamic> toJson() {
final _data = <String, dynamic>{};
_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<PkzChapter> chapters;
late final int chapterCount;
late final int pictureCount;
late final int idx;
PkzVolume.fromJson(Map<String, dynamic> 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<String, dynamic> toJson() {
final _data = <String, dynamic>{};
_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<PkzPicture> pictures;
late final int pictureCount;
late final int idx;
PkzChapter.fromJson(Map<String, dynamic> 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<String, dynamic> toJson() {
final _data = <String, dynamic>{};
_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<String, dynamic> json) {
id = json['id'];
title = json['title'];
width = json['width'];
height = json['height'];
format = json['format'];
picturePath = json['picture_path'];
idx = json['idx'];
}
Map<String, dynamic> toJson() {
final _data = <String, dynamic>{};
_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<String, dynamic> 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<String, dynamic> 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<String, dynamic> toJson() {
final _data = <String, dynamic>{};
_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;
}
}

View File

@ -560,6 +560,19 @@ class Method {
}); });
} }
/// PKZ
Future<dynamic> exportComicDownloadToPkz(
List<String> comicIds,
String dir,
String name,
) {
return _flatInvoke("exportComicDownloadToPkz", {
"comicIds": comicIds,
"dir": dir,
"name": name,
});
}
/// 使 /// 使
Future<int> exportComicUsingSocket(String comicId) async { Future<int> exportComicUsingSocket(String comicId) async {
return int.parse(await _flatInvoke("exportComicUsingSocket", comicId)); return int.parse(await _flatInvoke("exportComicUsingSocket", comicId));
@ -721,4 +734,89 @@ class Method {
Future<bool> verifyAuthentication() async { Future<bool> verifyAuthentication() async {
return await _channel.invokeMethod("verifyAuthentication"); return await _channel.invokeMethod("verifyAuthentication");
} }
Future<PkzArchive> pkzInfo(String pkzPath) async {
return PkzArchive.fromJson(
jsonDecode(await _flatInvoke("pkzInfo", pkzPath)));
}
Future<Uint8List> loadPkzFile(String pkzPath, String path) async {
return base64Decode(await _flatInvoke("loadPkzFile", {
"pkzPath": pkzPath,
"path": path,
}));
}
Future<List<PkzComicViewLog>> pkzComicViewLogs(
String fileName,
String comicId,
) async {
return List.of(jsonDecode(await _flatInvoke("pkzComicViewLogs", fileName)))
.map((e) => PkzComicViewLog.fromJson(e))
.toList();
}
Future<PkzComicViewLog?> 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<List<Knight>> leaderboardOfKnight() async {
return List.of(jsonDecode(await _flatInvoke("leaderboardOfKnight", "")))
.map((e) => Knight.fromJson(e))
.toList();
}
} }

View File

@ -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/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pikapika/basic/Common.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/RegisterScreen.dart';
import 'package:pikapika/screens/SettingsScreen.dart'; import 'package:pikapika/screens/SettingsScreen.dart';
import 'package:pikapika/screens/components/NetworkSetting.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 'AppScreen.dart';
import 'DownloadListScreen.dart'; import 'DownloadListScreen.dart';
import 'PkzArchiveScreen.dart';
import 'ThemeScreen.dart'; import 'ThemeScreen.dart';
import 'components/ContentLoading.dart'; import 'components/ContentLoading.dart';
@ -25,13 +32,34 @@ class _AccountScreenState extends State<AccountScreen> {
late bool _logging = false; late bool _logging = false;
late String _username = ""; late String _username = "";
late String _password = ""; late String _password = "";
late StreamSubscription<Uri> _linkSubscription;
@override @override
void initState() { 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(); _loadProperties();
super.initState(); super.initState();
} }
@override
void dispose() {
_linkSubscription.cancel();
super.dispose();
}
Future _loadProperties() async { Future _loadProperties() async {
var username = await method.getUsername(); var username = await method.getUsername();
var password = await method.getPassword(); var password = await method.getPassword();

View File

@ -1,8 +1,14 @@
import 'dart:async';
import 'dart:io';
import 'package:app_links/app_links.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pikapika/basic/config/Version.dart'; import 'package:pikapika/basic/config/Version.dart';
import 'package:pikapika/screens/components/Badge.dart'; import 'package:pikapika/screens/components/Badge.dart';
import 'package:uri_to_file/uri_to_file.dart';
import 'CategoriesScreen.dart'; import 'CategoriesScreen.dart';
import 'PkzArchiveScreen.dart';
import 'SpaceScreen.dart'; import 'SpaceScreen.dart';
// MAIN UI // MAIN UI
@ -14,15 +20,32 @@ class AppScreen extends StatefulWidget {
} }
class _AppScreenState extends State<AppScreen> { class _AppScreenState extends State<AppScreen> {
late StreamSubscription<Uri> _linkSubscription;
@override @override
void initState() { void initState() {
versionEvent.subscribe(_onVersion); 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(); super.initState();
} }
@override @override
void dispose() { void dispose() {
versionEvent.unsubscribe(_onVersion); versionEvent.unsubscribe(_onVersion);
_linkSubscription.cancel();
super.dispose(); super.dispose();
} }

View File

@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_search_bar/flutter_search_bar.dart'; import 'package:flutter_search_bar/flutter_search_bar.dart';
import 'package:pikapika/basic/Entities.dart'; import 'package:pikapika/basic/Entities.dart';
import 'package:pikapika/basic/config/ShadowCategoriesEvent.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/store/Categories.dart';
import 'package:pikapika/basic/config/ShadowCategories.dart'; import 'package:pikapika/basic/config/ShadowCategories.dart';
import 'package:pikapika/screens/ComicCollectionsScreen.dart'; import 'package:pikapika/screens/ComicCollectionsScreen.dart';

View File

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_search_bar/flutter_search_bar.dart'; import 'package:flutter_search_bar/flutter_search_bar.dart';
import 'package:pikapika/basic/Common.dart'; import 'package:pikapika/basic/Common.dart';
import 'package:pikapika/basic/config/ShadowCategories.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/store/Categories.dart';
import 'package:pikapika/basic/config/ListLayout.dart'; import 'package:pikapika/basic/config/ListLayout.dart';
import 'package:pikapika/basic/Method.dart'; import 'package:pikapika/basic/Method.dart';

View File

@ -62,7 +62,7 @@ class _DownloadExportToFileScreenState
} }
@override @override
Widget build(BuildContext context){ Widget build(BuildContext context) {
return rightClickPop( return rightClickPop(
child: buildScreen(context), child: buildScreen(context),
context: context, context: context,
@ -185,6 +185,59 @@ class _DownloadExportToFileScreenState
child: _buildButtonInner('导出到HTML+JPG\n(可直接在相册中打开观看)'), child: _buildButtonInner('导出到HTML+JPG\n(可直接在相册中打开观看)'),
)); ));
widgets.add(Container(height: 10)); 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( widgets.add(MaterialButton(
onPressed: () async { onPressed: () async {
late String? path; late String? path;

View File

@ -76,7 +76,7 @@ class _DownloadReaderScreenState extends State<DownloadReaderScreen> {
} }
FutureOr<dynamic> _onDownload() async { FutureOr<dynamic> _onDownload() async {
defaultToast(context, "已经在下载阅读"); defaultToast(context, "阅读的是下载漫画");
} }
FutureOr<dynamic> _onChangeEp(int epOrder) { FutureOr<dynamic> _onChangeEp(int epOrder) {

View File

@ -1,3 +1,6 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pikapika/basic/config/Address.dart'; import 'package:pikapika/basic/config/Address.dart';
import 'package:pikapika/basic/config/AndroidDisplayMode.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/UsingRightClickPop.dart';
import 'package:pikapika/basic/config/Version.dart'; import 'package:pikapika/basic/config/Version.dart';
import 'package:pikapika/basic/config/VolumeController.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 '../basic/config/ExportRename.dart';
import 'AccountScreen.dart'; import 'AccountScreen.dart';
import 'AppScreen.dart'; import 'AppScreen.dart';
@ -88,6 +93,32 @@ class _InitScreenState extends State<InitScreen> {
await initUsingRightClickPop(); await initUsingRightClickPop();
await initAuthentication(); await initAuthentication();
autoCheckNewVersion(); 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(() { setState(() {
_authenticating = currentAuthentication(); _authenticating = currentAuthentication();
}); });
@ -112,7 +143,8 @@ class _InitScreenState extends State<InitScreen> {
onPressed: () { onPressed: () {
_goAuthentication(); _goAuthentication();
}, },
child: const Text('您在之前使用APP时开启了身份验证, 请点这段文字进行身份核查, 核查通过后将会进入APP'), child:
const Text('您在之前使用APP时开启了身份验证, 请点这段文字进行身份核查, 核查通过后将会进入APP'),
), ),
), ),
), ),

View File

@ -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<StatefulWidget> createState() => _PkzArchiveScreenState();
}
class _PkzArchiveScreenState extends State<PkzArchiveScreen> with RouteAware {
Map<String, PkzComicViewLog> _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(),
]);
},
),
);
}
}

View File

@ -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<StatefulWidget> createState() => _PkzComicInfoScreenState();
}
class _PkzComicInfoScreenState extends State<PkzComicInfoScreen>
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<Widget> 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<String, PkzChapter> 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,
),
]),
);
}
}

View File

@ -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<PkzChapter> 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<StatefulWidget> createState() => _PkzReaderScreenState();
}
class _PkzReaderScreenState extends State<PkzReaderScreen> {
late PkzChapter _ep;
late int _epOrder;
late bool _fullScreen = false;
late List<PkzPicture> 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<dynamic> _onDownload() async {
defaultToast(context, "您阅读的是下载漫画");
}
FutureOr<dynamic> _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<dynamic> _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<dynamic> 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 = <int, String>{};
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;
});
}
}

View File

@ -25,7 +25,7 @@ import 'package:pikapika/basic/config/Themes.dart';
import 'package:pikapika/basic/config/TimeOffsetHour.dart'; import 'package:pikapika/basic/config/TimeOffsetHour.dart';
import 'package:pikapika/basic/config/Version.dart'; import 'package:pikapika/basic/config/Version.dart';
import 'package:pikapika/basic/config/VolumeController.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/NetworkSetting.dart';
import 'package:pikapika/screens/components/RightClickPop.dart'; import 'package:pikapika/screens/components/RightClickPop.dart';

View File

@ -367,3 +367,8 @@ final authorStyle = TextStyle(
fontSize: 13, fontSize: 13,
color: Colors.pink.shade300, color: Colors.pink.shade300,
); );
final authorStyleX = TextStyle(
fontSize: 13,
color: Colors.pink.shade300.withOpacity(.7),
);

View File

@ -7,7 +7,7 @@ import 'package:pikapika/basic/Entities.dart';
import 'package:pikapika/basic/Method.dart'; import 'package:pikapika/basic/Method.dart';
import 'package:pikapika/basic/config/ShadowCategories.dart'; import 'package:pikapika/basic/config/ShadowCategories.dart';
import 'package:pikapika/basic/config/ListLayout.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 'ComicInfoCard.dart';
import 'Images.dart'; import 'Images.dart';

View File

@ -21,6 +21,7 @@ import 'package:pikapika/basic/config/ReaderDirection.dart';
import 'package:pikapika/basic/config/ReaderSliderPosition.dart'; import 'package:pikapika/basic/config/ReaderSliderPosition.dart';
import 'package:pikapika/basic/config/ReaderType.dart'; import 'package:pikapika/basic/config/ReaderType.dart';
import 'package:pikapika/basic/config/VolumeController.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 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import '../FilePhotoViewScreen.dart'; import '../FilePhotoViewScreen.dart';
import 'gesture_zoom_box.dart'; import 'gesture_zoom_box.dart';
@ -30,7 +31,7 @@ import 'Images.dart';
/////////////// ///////////////
Event<_ReaderControllerEventArgs> _readerControllerEvent = Event<_ReaderControllerEventArgs> _readerControllerEvent =
Event<_ReaderControllerEventArgs>(); Event<_ReaderControllerEventArgs>();
class _ReaderControllerEventArgs extends EventArgs { class _ReaderControllerEventArgs extends EventArgs {
final String key; final String key;
@ -93,6 +94,13 @@ void delVolumeListen() {
// Reader的传参以及封装 // Reader的传参以及封装
class PkzFile {
final String pkzPath;
final String path;
PkzFile(this.pkzPath, this.path);
}
class ReaderImageInfo { class ReaderImageInfo {
final String fileServer; final String fileServer;
final String path; final String path;
@ -101,9 +109,18 @@ class ReaderImageInfo {
final int? height; final int? height;
final String? format; final String? format;
final int? fileSize; final int? fileSize;
final PkzFile? pkzFile;
ReaderImageInfo(this.fileServer, this.path, this.downloadLocalPath, ReaderImageInfo(
this.width, this.height, this.format, this.fileSize); this.fileServer,
this.path,
this.downloadLocalPath,
this.width,
this.height,
this.format,
this.fileSize, {
this.pkzFile,
});
} }
class ImageReaderStruct { class ImageReaderStruct {
@ -156,7 +173,7 @@ class _ImageReaderState extends State<ImageReader> {
late final FullScreenAction _fullScreenAction = currentFullScreenAction(); late final FullScreenAction _fullScreenAction = currentFullScreenAction();
late final ReaderSliderPosition _readerSliderPosition = late final ReaderSliderPosition _readerSliderPosition =
currentReaderSliderPosition(); currentReaderSliderPosition();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -186,12 +203,12 @@ class _ImageReaderContent extends StatefulWidget {
final ImageReaderStruct struct; final ImageReaderStruct struct;
const _ImageReaderContent( const _ImageReaderContent(
this.struct, this.struct,
this.pagerDirection, this.pagerDirection,
this.pagerType, this.pagerType,
this.fullScreenAction, this.fullScreenAction,
this.readerSliderPosition, this.readerSliderPosition,
); );
@override @override
State<StatefulWidget> createState() { State<StatefulWidget> createState() {
@ -290,7 +307,7 @@ abstract class _ImageReaderContentState extends State<_ImageReaderContent> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
switch (currentFullScreenAction()) { switch (currentFullScreenAction()) {
// //
case FullScreenAction.CONTROLLER: case FullScreenAction.CONTROLLER:
return Stack( return Stack(
children: [ children: [
@ -339,37 +356,37 @@ abstract class _ImageReaderContentState extends State<_ImageReaderContent> {
widget.struct.fullScreen widget.struct.fullScreen
? Container() ? Container()
: Container( : Container(
height: 45, height: 45,
color: const Color(0x88000000), color: const Color(0x88000000),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Container(width: 15), Container(width: 15),
IconButton( IconButton(
icon: const Icon(Icons.fullscreen), icon: const Icon(Icons.fullscreen),
color: Colors.white, color: Colors.white,
onPressed: () { onPressed: () {
widget.struct widget.struct
.onFullScreenChange(!widget.struct.fullScreen); .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: case ReaderSliderPosition.RIGHT:
@ -406,19 +423,19 @@ abstract class _ImageReaderContentState extends State<_ImageReaderContent> {
Widget _buildAppBar() => widget.struct.fullScreen Widget _buildAppBar() => widget.struct.fullScreen
? Container() ? Container()
: AppBar( : AppBar(
title: Text( title: Text(
"${widget.struct.epNameMap[widget.struct.epOrder] ?? ""} - ${widget.struct.comicTitle}"), "${widget.struct.epNameMap[widget.struct.epOrder] ?? ""} - ${widget.struct.comicTitle}"),
actions: [ actions: [
IconButton( IconButton(
onPressed: _onChooseEp, onPressed: _onChooseEp,
icon: const Icon(Icons.menu_open), icon: const Icon(Icons.menu_open),
), ),
IconButton( IconButton(
onPressed: _onMoreSetting, onPressed: _onMoreSetting,
icon: const Icon(Icons.more_horiz), icon: const Icon(Icons.more_horiz),
), ),
], ],
); );
Widget _buildSliderBottom() { Widget _buildSliderBottom() {
return Column( return Column(
@ -436,52 +453,52 @@ abstract class _ImageReaderContentState extends State<_ImageReaderContent> {
Widget _buildSliderLeft() => widget.struct.fullScreen Widget _buildSliderLeft() => widget.struct.fullScreen
? Container() ? Container()
: Align( : Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Material( child: Material(
color: Colors.transparent, color: Colors.transparent,
child: Container( child: Container(
width: 35, width: 35,
height: 300, height: 300,
decoration: const BoxDecoration( decoration: const BoxDecoration(
color: Color(0x66000000), color: Color(0x66000000),
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
topRight: Radius.circular(10), topRight: Radius.circular(10),
bottomRight: 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 Widget _buildSliderRight() => widget.struct.fullScreen
? Container() ? Container()
: Align( : Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: Material( child: Material(
color: Colors.transparent, color: Colors.transparent,
child: Container( child: Container(
width: 35, width: 35,
height: 300, height: 300,
decoration: const BoxDecoration( decoration: const BoxDecoration(
color: Color(0x66000000), color: Color(0x66000000),
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
topLeft: Radius.circular(10), topLeft: Radius.circular(10),
bottomLeft: 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) { Widget _buildSliderWidget(Axis axis) {
return FlutterSlider( return FlutterSlider(
@ -544,7 +561,7 @@ abstract class _ImageReaderContentState extends State<_ImageReaderContent> {
color: Colors.transparent, color: Colors.transparent,
child: Container( child: Container(
padding: 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), margin: const EdgeInsets.only(bottom: 10),
decoration: const BoxDecoration( decoration: const BoxDecoration(
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
@ -930,7 +947,13 @@ class _WebToonReaderState extends _ImageReaderContentState {
@override @override
void initState() { void initState() {
for (var e in widget.struct.images) { 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())); _trueSizes.add(Size(e.width!.toDouble(), e.height!.toDouble()));
} else { } else {
_trueSizes.add(null); _trueSizes.add(null);
@ -1030,7 +1053,16 @@ class _WebToonReaderState extends _ImageReaderContentState {
} }
var e = widget.struct.images[index]; 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( _images.add(_WebToonDownloadImage(
fileServer: e.fileServer, fileServer: e.fileServer,
path: e.path, path: e.path,
@ -1054,9 +1086,9 @@ class _WebToonReaderState extends _ImageReaderContentState {
return ScrollablePositionedList.builder( return ScrollablePositionedList.builder(
initialScrollIndex: super._startIndex, initialScrollIndex: super._startIndex,
scrollDirection: scrollDirection:
widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM
? Axis.vertical ? Axis.vertical
: Axis.horizontal, : Axis.horizontal,
reverse: widget.pagerDirection == ReaderDirection.RIGHT_TO_LEFT, reverse: widget.pagerDirection == ReaderDirection.RIGHT_TO_LEFT,
padding: EdgeInsets.only( padding: EdgeInsets.only(
// , , // , ,
@ -1064,9 +1096,9 @@ class _WebToonReaderState extends _ImageReaderContentState {
bottom: widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM bottom: widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM
? 130 // 130 ? 130 // 130
: ( // : ( //
widget.struct.fullScreen widget.struct.fullScreen
? super._topBarHeight() // ? super._topBarHeight() //
: super._bottomBarHeight()) : super._bottomBarHeight())
// , BAR的高度, BAR的高度, // , 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 { class _WebToonRemoteImage extends _WebToonReaderImage {
final String fileServer; final String fileServer;
final String path; final String path;
_WebToonRemoteImage( _WebToonRemoteImage(
this.fileServer, this.fileServer,
this.path, this.path,
Size size, Size size,
Function(Size)? onTrueSize, Function(Size)? onTrueSize,
) : super(size, onTrueSize); ) : super(size, onTrueSize);
@override @override
Future<RemoteImageData> imageData() async { Future<RemoteImageData> imageData() async {
@ -1197,14 +1259,14 @@ class _WebToonReaderImageState extends State<_WebToonReaderImage> {
return FutureBuilder( return FutureBuilder(
future: _future, future: _future,
builder: ( builder: (
BuildContext context, BuildContext context,
AsyncSnapshot<RemoteImageData> snapshot, AsyncSnapshot<RemoteImageData> snapshot,
) { ) {
if (snapshot.hasError) { if (snapshot.hasError) {
return GestureDetector( return GestureDetector(
onLongPress: () async { onLongPress: () async {
String? choose = String? choose =
await chooseListDialog(context, '请选择', ['重新加载图片']); await chooseListDialog(context, '请选择', ['重新加载图片']);
switch (choose) { switch (choose) {
case '重新加载图片': case '重新加载图片':
setState(() { setState(() {
@ -1257,7 +1319,13 @@ class _ListViewReaderState extends _ImageReaderContentState
@override @override
void initState() { void initState() {
for (var e in widget.struct.images) { 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())); _trueSizes.add(Size(e.width!.toDouble(), e.height!.toDouble()));
} else { } else {
_trueSizes.add(null); _trueSizes.add(null);
@ -1332,7 +1400,16 @@ class _ListViewReaderState extends _ImageReaderContentState
} }
var e = widget.struct.images[index]; 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( _images.add(_WebToonDownloadImage(
fileServer: e.fileServer, fileServer: e.fileServer,
path: e.path, path: e.path,
@ -1355,9 +1432,9 @@ class _ListViewReaderState extends _ImageReaderContentState
} }
var list = ListView.builder( var list = ListView.builder(
scrollDirection: scrollDirection:
widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM
? Axis.vertical ? Axis.vertical
: Axis.horizontal, : Axis.horizontal,
reverse: widget.pagerDirection == ReaderDirection.RIGHT_TO_LEFT, reverse: widget.pagerDirection == ReaderDirection.RIGHT_TO_LEFT,
padding: EdgeInsets.only( padding: EdgeInsets.only(
// , , // , ,
@ -1365,9 +1442,9 @@ class _ListViewReaderState extends _ImageReaderContentState
bottom: widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM bottom: widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM
? 130 // 130 ? 130 // 130
: ( // : ( //
widget.struct.fullScreen widget.struct.fullScreen
? super._topBarHeight() // ? super._topBarHeight() //
: super._bottomBarHeight()) : super._bottomBarHeight())
// , BAR的高度, BAR的高度, // , BAR的高度, BAR的高度,
, ,
), ),
@ -1499,10 +1576,26 @@ class _GalleryReaderState extends _ImageReaderContentState {
itemCount: widget.struct.images.length, itemCount: widget.struct.images.length,
builder: (BuildContext context, int index) { builder: (BuildContext context, int index) {
var item = widget.struct.images[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) { if (item.downloadLocalPath != null) {
return PhotoViewGalleryPageOptions( return PhotoViewGalleryPageOptions(
imageProvider: imageProvider:
ResourceDownloadFileImageProvider(item.downloadLocalPath!), ResourceDownloadFileImageProvider(item.downloadLocalPath!),
errorBuilder: (b, e, s) { errorBuilder: (b, e, s) {
print("$e,$s"); print("$e,$s");
return LayoutBuilder( return LayoutBuilder(
@ -1517,7 +1610,7 @@ class _GalleryReaderState extends _ImageReaderContentState {
} }
return PhotoViewGalleryPageOptions( return PhotoViewGalleryPageOptions(
imageProvider: imageProvider:
ResourceRemoteImageProvider(item.fileServer, item.path), ResourceRemoteImageProvider(item.fileServer, item.path),
errorBuilder: (b, e, s) { errorBuilder: (b, e, s) {
print("$e,$s"); print("$e,$s");
return LayoutBuilder( return LayoutBuilder(
@ -1535,6 +1628,11 @@ class _GalleryReaderState extends _ImageReaderContentState {
child: gallery, child: gallery,
onLongPress: () async { onLongPress: () async {
if (_current >= 0 && _current < widget.struct.images.length) { if (_current >= 0 && _current < widget.struct.images.length) {
var item = widget.struct.images[_current];
if (item.pkzFile != null) {
return;
}
Future<String> load() async { Future<String> load() async {
var item = widget.struct.images[_current]; var item = widget.struct.images[_current];
if (item.downloadLocalPath != null) { if (item.downloadLocalPath != null) {
@ -1545,7 +1643,7 @@ class _GalleryReaderState extends _ImageReaderContentState {
} }
String? choose = String? choose =
await chooseListDialog(context, '请选择', ['预览图片', '保存图片']); await chooseListDialog(context, '请选择', ['预览图片', '保存图片']);
switch (choose) { switch (choose) {
case '预览图片': case '预览图片':
try { try {
@ -1594,7 +1692,7 @@ class _GalleryReaderState extends _ImageReaderContentState {
child: Container( child: Container(
margin: const EdgeInsets.only(bottom: 10), margin: const EdgeInsets.only(bottom: 10),
padding: 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( decoration: const BoxDecoration(
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
topLeft: Radius.circular(10), topLeft: Radius.circular(10),

View File

@ -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<StatefulWidget> createState() => _ComicInfoCard();
}
class _ComicInfoCard extends State<PkzComicInfoCard> {
@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(),
],
),
),
],
),
),
],
),
);
}
}

View File

@ -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<PkzImageProvider> {
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<PkzImageProvider> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture<PkzImageProvider>(this);
}
Future<ui.Codec> _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<StatefulWidget> createState() => _PkzImageState();
}
class _PkzImageState extends State<PkzImage> {
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<StatefulWidget> createState() => _PkzLoadingImageState();
}
class _PkzLoadingImageState extends State<PkzLoadingImage> {
late bool _mock;
late Future<Uint8List> 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,
);
}
}

View File

@ -5,8 +5,10 @@
import FlutterMacOS import FlutterMacOS
import Foundation import Foundation
import app_links_macos
import url_launcher_macos import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
} }

View File

@ -8,6 +8,41 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.1+2" 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: archive:
dependency: transitive dependency: transitive
description: description:
@ -171,7 +206,7 @@ packages:
name: flutter_svg name: flutter_svg
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.0" version: "1.1.1"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@ -300,7 +335,7 @@ packages:
name: modal_bottom_sheet name: modal_bottom_sheet
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.1" version: "2.1.0"
multi_select_flutter: multi_select_flutter:
dependency: "direct main" dependency: "direct main"
description: description:
@ -309,7 +344,7 @@ packages:
source: hosted source: hosted
version: "4.1.2" version: "4.1.2"
path: path:
dependency: transitive dependency: "direct main"
description: description:
name: path name: path
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
@ -432,13 +467,20 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.3.1" 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: url_launcher:
dependency: "direct main" dependency: "direct main"
description: description:
name: url_launcher name: url_launcher
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "6.1.3" version: "6.1.4"
url_launcher_android: url_launcher_android:
dependency: transitive dependency: transitive
description: description:
@ -511,4 +553,4 @@ packages:
version: "6.1.0" version: "6.1.0"
sdks: sdks:
dart: ">=2.17.0 <3.0.0" dart: ">=2.17.0 <3.0.0"
flutter: ">=2.11.0-0.1.pre" flutter: ">=3.0.0"

View File

@ -47,6 +47,9 @@ dependencies:
file_picker: ^4.5.1 file_picker: ^4.5.1
crop_image: ^1.0.2 crop_image: ^1.0.2
image: ^3.1.3 image: ^3.1.3
path: ^1.8.0
app_links: ^3.2.0
uri_to_file: ^0.2.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View File

@ -6,9 +6,12 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <app_links_windows/app_links_windows_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h> #include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
AppLinksWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("AppLinksWindowsPlugin"));
UrlLauncherWindowsRegisterWithRegistrar( UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows")); registry->GetRegistrarForPlugin("UrlLauncherWindows"));
} }

View File

@ -3,6 +3,7 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
app_links_windows
url_launcher_windows url_launcher_windows
) )