From f5adcbd2e07cb5dcd01fc69ecbf2f1563bc7b7b1 Mon Sep 17 00:00:00 2001 From: niuhuan Date: Thu, 30 Sep 2021 15:12:23 +0800 Subject: [PATCH] preload image from network in gallery mode --- README.md | 17 +-- go/cmd/main.go | 9 +- go/pikapi/controller/pikapi.go | 82 ++++++++----- lib/basic/Method.dart | 7 ++ lib/basic/config/GalleryPreloadCount.dart | 10 ++ lib/basic/config/ShadowCategories.dart | 2 +- lib/basic/config/Themes.dart | 59 +++++++--- lib/basic/enum/ErrorTypes.dart | 4 + lib/screens/ComicsScreen.dart | 18 +-- lib/screens/RandomComicsScreen.dart | 2 + lib/screens/RankingsScreen.dart | 3 + lib/screens/SearchScreen.dart | 17 --- lib/screens/components/ComicListBuilder.dart | 33 +++++- lib/screens/components/ComicPager.dart | 31 ++++- lib/screens/components/ContentError.dart | 115 ++++++++++--------- lib/screens/components/ImageReader.dart | 23 +++- pubspec.yaml | 2 +- 17 files changed, 278 insertions(+), 156 deletions(-) create mode 100644 lib/basic/config/GalleryPreloadCount.dart diff --git a/README.md b/README.md index b6d1b5e..2765163 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ PIKAPI - 漫画客户端 ### 分流 VPN->代理->分流, 这三个功能如果同时设置, 您会在您手机的VPN上访问代理, 使用代理请求分流服务器。 - + ### 漫画分类/搜索 ![分类](images/categories_screen.png) ![列表](images/comic_list.png) @@ -109,16 +109,17 @@ VPN->代理->分流, 这三个功能如果同时设置, 您会在您手机的VPN sudo apt install xorg-dev ``` - 字体不显示 - 1. 将字体文件复制到项目目录下 ```shell - cp /usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf fonts/ + # 将字体文件复制到项目目录下 + mkdir -p fonts + cp -f /usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf fonts/ ``` - 2. 设置flutter打包的字体 ```yaml - fonts: - - family: Roboto - fonts: - - asset: fonts/DroidSansFallbackFull.ttf + # 编辑 pubspec.yaml + fonts: + - family: Roboto + fonts: + - asset: fonts/DroidSansFallbackFull.ttf ``` ### 移动端 (gomobile) diff --git a/go/cmd/main.go b/go/cmd/main.go index 166c11c..3fce810 100644 --- a/go/cmd/main.go +++ b/go/cmd/main.go @@ -35,10 +35,11 @@ func main() { if height <= 0 { height = 900 } - sizeOption := flutter.WindowInitialDimensions(width, height) - options = append(options, sizeOption) - // - err := flutter.Run(append(options, mainOptions...)...) + var runOptions []flutter.Option + runOptions = append(runOptions, flutter.WindowInitialDimensions(width, height)) + runOptions = append(runOptions, options...) + // ------ + err := flutter.Run(append(runOptions, mainOptions...)...) if err != nil { fmt.Println(err) os.Exit(1) diff --git a/go/pikapi/controller/pikapi.go b/go/pikapi/controller/pikapi.go index e2c3894..69d11c4 100644 --- a/go/pikapi/controller/pikapi.go +++ b/go/pikapi/controller/pikapi.go @@ -169,37 +169,11 @@ func remoteImageData(params string) (string, error) { defer lock.Unlock() cache := comic_center.FindRemoteImage(fileServer, path) if cache == nil { - buff, img, format, err := decodeFromUrl(fileServer, path) - if err != nil { - println(fmt.Sprintf("decode error : %s/static/%s %s", fileServer, path, err.Error())) - return "", err - } - local := - fmt.Sprintf("%x", - md5.Sum([]byte(fmt.Sprintf("%s$%s", fileServer, path))), - ) - real := remotePath(local) - err = ioutil.WriteFile( - real, - buff, os.FileMode(0600), - ) + remote, err := decodeAndSaveImage(fileServer, path) if err != nil { return "", err } - remote := comic_center.RemoteImage{ - FileServer: fileServer, - Path: path, - FileSize: int64(len(buff)), - Format: format, - Width: int32(img.Bounds().Dx()), - Height: int32(img.Bounds().Dy()), - LocalPath: local, - } - err = comic_center.SaveRemoteImage(&remote) - if err != nil { - return "", err - } - cache = &remote + cache = remote } display := DisplayImageData{ FileSize: cache.FileSize, @@ -211,6 +185,56 @@ func remoteImageData(params string) (string, error) { return serialize(&display, nil) } +func remoteImagePreload(params string) error { + var paramsStruct struct { + FileServer string `json:"fileServer"` + Path string `json:"path"` + } + json.Unmarshal([]byte(params), ¶msStruct) + fileServer := paramsStruct.FileServer + path := paramsStruct.Path + lock := utils.HashLock(fmt.Sprintf("%s$%s", fileServer, path)) + lock.Lock() + defer lock.Unlock() + cache := comic_center.FindRemoteImage(fileServer, path) + var err error + if cache == nil { + _, err = decodeAndSaveImage(fileServer, path) + } + return err +} + +func decodeAndSaveImage(fileServer string, path string) (*comic_center.RemoteImage, error) { + buff, img, format, err := decodeFromUrl(fileServer, path) + if err != nil { + println(fmt.Sprintf("decode error : %s/static/%s %s", fileServer, path, err.Error())) + return nil, err + } + local := + fmt.Sprintf("%x", + md5.Sum([]byte(fmt.Sprintf("%s$%s", fileServer, path))), + ) + real := remotePath(local) + err = ioutil.WriteFile( + real, + buff, os.FileMode(0600), + ) + if err != nil { + return nil, err + } + remote := comic_center.RemoteImage{ + FileServer: fileServer, + Path: path, + FileSize: int64(len(buff)), + Format: format, + Width: int32(img.Bounds().Dx()), + Height: int32(img.Bounds().Dy()), + LocalPath: local, + } + err = comic_center.SaveRemoteImage(&remote) + return &remote, err +} + func downloadImagePath(path string) (string, error) { return downloadPath(path), nil } @@ -565,6 +589,8 @@ func FlatInvoke(method string, params string) (string, error) { return "", importComicDownloadUsingSocket(params) case "remoteImageData": return remoteImageData(params) + case "remoteImagePreload": + return "", remoteImagePreload(params) case "clientIpSet": return clientIpSet() case "downloadImagePath": diff --git a/lib/basic/Method.dart b/lib/basic/Method.dart index a903c8a..c1d0ffa 100644 --- a/lib/basic/Method.dart +++ b/lib/basic/Method.dart @@ -150,6 +150,13 @@ class Method { return RemoteImageData.fromJson(json.decode(data)); } + Future remoteImagePreload(String fileServer, String path) async { + return _flatInvoke("remoteImagePreload", { + "fileServer": fileServer, + "path": path, + }); + } + Future downloadImagePath(String path) async { return await _flatInvoke("downloadImagePath", path); } diff --git a/lib/basic/config/GalleryPreloadCount.dart b/lib/basic/config/GalleryPreloadCount.dart new file mode 100644 index 0000000..0a36284 --- /dev/null +++ b/lib/basic/config/GalleryPreloadCount.dart @@ -0,0 +1,10 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:pikapi/basic/Method.dart'; + +import '../Common.dart'; + +const galleryPrePreloadCount = 1; +const galleryPreloadCount = 2; diff --git a/lib/basic/config/ShadowCategories.dart b/lib/basic/config/ShadowCategories.dart index aa74574..0118e01 100644 --- a/lib/basic/config/ShadowCategories.dart +++ b/lib/basic/config/ShadowCategories.dart @@ -50,6 +50,6 @@ Widget shadowCategoriesActionButton(BuildContext context) { onPressed: () { chooseShadowCategories(context); }, - icon: Icon(Icons.dnd_forwardslash), + icon: Icon(Icons.hide_source), ); } diff --git a/lib/basic/config/Themes.dart b/lib/basic/config/Themes.dart index c0846f3..c96a965 100644 --- a/lib/basic/config/Themes.dart +++ b/lib/basic/config/Themes.dart @@ -165,7 +165,8 @@ Future initTheme() async { _androidVersion = await method.androidGetVersion(); if (_androidVersion >= 29) { _androidNightMode = - (await method.loadProperty(_nightModePropertyName, "false")) == "true"; + (await method.loadProperty(_nightModePropertyName, "false")) == + "true"; _systemNight = (await method.androidGetUiMode()) == "NIGHT"; EventChannel("ui_mode").receiveBroadcastStream().listen((event) { _systemNight = "$event" == "NIGHT"; @@ -185,23 +186,49 @@ Future chooseTheme(BuildContext buildContext) async { builder: (BuildContext context, StateSetter setState) { var list = []; if (_androidVersion >= 29) { + var onChange = (bool? v) async { + if (v != null) { + await method.saveProperty( + _nightModePropertyName, "$v"); + _androidNightMode = v; + } + setState(() {}); + themeEvent.broadcast(); + }; list.add( SimpleDialogOption( - child: Row( - children: [ - Checkbox( - value: _androidNightMode, - onChanged: (bool? v) async { - if (v != null) { - await method.saveProperty( - _nightModePropertyName, "$v"); - _androidNightMode = v; - } - setState(() {}); - themeEvent.broadcast(); - }), - Text("随手机进入夜间模式"), - ], + child: GestureDetector( + onTap: () { + onChange(!_androidNightMode); + }, + child: Container( + margin: EdgeInsets.only(top: 3, bottom: 3), + decoration: BoxDecoration( + border: Border( + top: BorderSide( + color: Theme + .of(context) + .dividerColor, + width: 0.5, + ), + bottom: BorderSide( + color: Theme + .of(context) + .dividerColor, + width: 0.5 + ), + ), + ), + child: Row( + children: [ + Checkbox( + value: _androidNightMode, + onChanged: onChange, + ), + Text("随手机进入夜间模式"), + ], + ), + ), ), ), ); diff --git a/lib/basic/enum/ErrorTypes.dart b/lib/basic/enum/ErrorTypes.dart index c9127e5..6e6bd72 100644 --- a/lib/basic/enum/ErrorTypes.dart +++ b/lib/basic/enum/ErrorTypes.dart @@ -1,4 +1,5 @@ const ERROR_TYPE_NETWORK = "NETWORK_ERROR"; +const ERROR_TYPE_PERMISSION = "PERMISSION_ERROR"; // 错误的类型, 方便照展示和谐的提示 String errorType(String error) { @@ -11,5 +12,8 @@ String errorType(String error) { error.contains("deadline")) { return ERROR_TYPE_NETWORK; } + if (error.contains("permission denied")) { + return ERROR_TYPE_PERMISSION; + } return ""; } diff --git a/lib/screens/ComicsScreen.dart b/lib/screens/ComicsScreen.dart index 8e0b423..1808abb 100644 --- a/lib/screens/ComicsScreen.dart +++ b/lib/screens/ComicsScreen.dart @@ -60,23 +60,6 @@ class _ComicsScreenState extends State { }, ); - @override - void initState() { - shadowCategoriesEvent.subscribe(_onShadowChange); - super.initState(); - } - - @override - void dispose() { - shadowCategoriesEvent.unsubscribe(_onShadowChange); - super.dispose(); - } - - void _onShadowChange(EventArgs? args) { - setState(() { - }); - } - Widget _chooseCategoryAction() => IconButton( onPressed: () async { String? category = await chooseListDialog(context, '请选择分类', [ @@ -142,6 +125,7 @@ class _ComicsScreenState extends State { appBar = AppBar( title: Text(title), actions: [ + shadowCategoriesActionButton(context), chooseLayoutAction(context), _chooseCategoryAction(), ], diff --git a/lib/screens/RandomComicsScreen.dart b/lib/screens/RandomComicsScreen.dart index a867284..2cc1d20 100644 --- a/lib/screens/RandomComicsScreen.dart +++ b/lib/screens/RandomComicsScreen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:pikapi/basic/Entities.dart'; import 'package:pikapi/basic/Method.dart'; import 'package:pikapi/basic/config/ListLayout.dart'; +import 'package:pikapi/basic/config/ShadowCategories.dart'; import 'components/ComicListBuilder.dart'; @@ -25,6 +26,7 @@ class _RandomComicsScreenState extends State { appBar: AppBar( title: Text('随机本子'), actions: [ + shadowCategoriesActionButton(context), chooseLayoutAction(context), ], ), diff --git a/lib/screens/RankingsScreen.dart b/lib/screens/RankingsScreen.dart index 5f54513..3404469 100644 --- a/lib/screens/RankingsScreen.dart +++ b/lib/screens/RankingsScreen.dart @@ -2,10 +2,12 @@ import 'package:flutter/material.dart'; import 'package:pikapi/basic/Entities.dart'; import 'package:pikapi/basic/Method.dart'; import 'package:pikapi/basic/config/ListLayout.dart'; +import 'package:pikapi/basic/config/ShadowCategories.dart'; import 'components/ComicListBuilder.dart'; class RankingsScreen extends StatelessWidget { + @override Widget build(BuildContext context) { var theme = Theme.of(context); @@ -13,6 +15,7 @@ class RankingsScreen extends StatelessWidget { appBar: AppBar( title: Text('排行榜'), actions: [ + shadowCategoriesActionButton(context), chooseLayoutAction(context), ], ), diff --git a/lib/screens/SearchScreen.dart b/lib/screens/SearchScreen.dart index d15dce0..ab0a66e 100644 --- a/lib/screens/SearchScreen.dart +++ b/lib/screens/SearchScreen.dart @@ -96,23 +96,6 @@ class _SearchScreenState extends State { } } - @override - void initState() { - shadowCategoriesEvent.subscribe(_onShadowChange); - super.initState(); - } - - @override - void dispose() { - shadowCategoriesEvent.unsubscribe(_onShadowChange); - super.dispose(); - } - - void _onShadowChange(EventArgs? args) { - setState(() { - }); - } - @override Widget build(BuildContext context) { return Scaffold( diff --git a/lib/screens/components/ComicListBuilder.dart b/lib/screens/components/ComicListBuilder.dart index cf48b19..c13fca0 100644 --- a/lib/screens/components/ComicListBuilder.dart +++ b/lib/screens/components/ComicListBuilder.dart @@ -1,28 +1,51 @@ +import 'package:event/event.dart'; import 'package:flutter/material.dart'; import 'package:pikapi/basic/Entities.dart'; +import 'package:pikapi/basic/config/ShadowCategories.dart'; import 'package:pikapi/screens/components/ComicList.dart'; import 'package:pikapi/screens/components/FitButton.dart'; import 'ContentBuilder.dart'; -class ComicListBuilder extends StatelessWidget { +class ComicListBuilder extends StatefulWidget { final Future> future; final Future Function() reload; ComicListBuilder(this.future, this.reload); + @override + State createState() => _ComicListBuilderState(); +} + +class _ComicListBuilderState extends State { + @override + void initState() { + shadowCategoriesEvent.subscribe(_onShadowChange); + super.initState(); + } + + @override + void dispose() { + shadowCategoriesEvent.unsubscribe(_onShadowChange); + super.dispose(); + } + + void _onShadowChange(EventArgs? args) { + setState(() {}); + } + @override Widget build(BuildContext context) { return ContentBuilder( - future: future, - onRefresh: reload, + future: widget.future, + onRefresh: widget.reload, successBuilder: (BuildContext context, AsyncSnapshot> snapshot) { return RefreshIndicator( - onRefresh: reload, + onRefresh: widget.reload, child: ComicList( snapshot.data!, appendWidget: FitButton( - onPressed: reload, + onPressed: widget.reload, text: '刷新', ), ), diff --git a/lib/screens/components/ComicPager.dart b/lib/screens/components/ComicPager.dart index 18d15a4..e1bd1d8 100644 --- a/lib/screens/components/ComicPager.dart +++ b/lib/screens/components/ComicPager.dart @@ -1,7 +1,9 @@ +import 'package:event/event.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:pikapi/basic/Entities.dart'; import 'package:pikapi/basic/config/PagerAction.dart'; +import 'package:pikapi/basic/config/ShadowCategories.dart'; import 'package:pikapi/basic/enum/Sort.dart'; import 'package:pikapi/screens/components/ComicList.dart'; import 'package:pikapi/screens/components/ContentError.dart'; @@ -9,18 +11,41 @@ import 'package:pikapi/screens/components/FitButton.dart'; import 'ContentLoading.dart'; // 漫画列页 -class ComicPager extends StatelessWidget { +class ComicPager extends StatefulWidget { final Future Function(String sort, int page) fetchPage; const ComicPager({required this.fetchPage}); + @override + State createState() => _ComicPagerState(); +} + +class _ComicPagerState extends State { + + @override + void initState() { + shadowCategoriesEvent.subscribe(_onShadowChange); + super.initState(); + } + + @override + void dispose() { + shadowCategoriesEvent.unsubscribe(_onShadowChange); + super.dispose(); + } + + void _onShadowChange(EventArgs? args) { + setState(() { + }); + } + @override Widget build(BuildContext context) { switch (currentPagerAction) { case PagerAction.CONTROLLER: - return ControllerComicPager(fetchPage: fetchPage); + return ControllerComicPager(fetchPage: widget.fetchPage); case PagerAction.STREAM: - return StreamComicPager(fetchPage: fetchPage); + return StreamComicPager(fetchPage: widget.fetchPage); default: return Container(); } diff --git a/lib/screens/components/ContentError.dart b/lib/screens/components/ContentError.dart index e787de7..b219cc4 100644 --- a/lib/screens/components/ContentError.dart +++ b/lib/screens/components/ContentError.dart @@ -24,24 +24,67 @@ class ContentError extends StatelessWidget { case ERROR_TYPE_NETWORK: message = "连接不上啦, 请检查网络"; break; + case ERROR_TYPE_PERMISSION: + message = "没有权限或路径不可用"; + break; default: message = "啊哦, 被玩坏了"; break; } - return LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) { - print("$error"); - print("$stackTrace"); - var width = constraints.maxWidth; - var height = constraints.maxHeight; - var min = width < height ? width : height; - var iconSize = min / 2.3; - var textSize = min / 16; - var tipSize = min / 20; - var infoSize = min / 30; - if (contentFailedReloadAction == - ContentFailedReloadAction.TOUCH_LOADER) { - return GestureDetector( - onTap: onRefresh, + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + print("$error"); + print("$stackTrace"); + var width = constraints.maxWidth; + var height = constraints.maxHeight; + var min = width < height ? width : height; + var iconSize = min / 2.3; + var textSize = min / 16; + var tipSize = min / 20; + var infoSize = min / 30; + if (contentFailedReloadAction == + ContentFailedReloadAction.TOUCH_LOADER) { + return GestureDetector( + onTap: onRefresh, + child: ListView( + children: [ + Container( + height: height, + child: Column( + children: [ + Expanded(child: Container()), + Container( + child: Icon( + Icons.wifi_off_rounded, + size: iconSize, + color: Colors.grey.shade600, + ), + ), + Container(height: min / 10), + Container( + padding: EdgeInsets.only( + left: 30, + right: 30, + ), + child: Text( + message, + style: TextStyle(fontSize: textSize), + textAlign: TextAlign.center, + ), + ), + Text('(点击刷新)', style: TextStyle(fontSize: tipSize)), + Container(height: min / 15), + Text('$error', style: TextStyle(fontSize: infoSize)), + Expanded(child: Container()), + ], + ), + ), + ], + ), + ); + } + return RefreshIndicator( + onRefresh: onRefresh, child: ListView( children: [ Container( @@ -68,7 +111,7 @@ class ContentError extends StatelessWidget { textAlign: TextAlign.center, ), ), - Text('(点击刷新)', style: TextStyle(fontSize: tipSize)), + Text('(下拉刷新)', style: TextStyle(fontSize: tipSize)), Container(height: min / 15), Text('$error', style: TextStyle(fontSize: infoSize)), Expanded(child: Container()), @@ -78,45 +121,7 @@ class ContentError extends StatelessWidget { ], ), ); - } - return RefreshIndicator( - onRefresh: onRefresh, - child: ListView( - children: [ - Container( - height: height, - child: Column( - children: [ - Expanded(child: Container()), - Container( - child: Icon( - Icons.wifi_off_rounded, - size: iconSize, - color: Colors.grey.shade600, - ), - ), - Container(height: min / 10), - Container( - padding: EdgeInsets.only( - left: 30, - right: 30, - ), - child: Text( - message, - style: TextStyle(fontSize: textSize), - textAlign: TextAlign.center, - ), - ), - Text('(下拉刷新)', style: TextStyle(fontSize: tipSize)), - Container(height: min / 15), - Text('$error', style: TextStyle(fontSize: infoSize)), - Expanded(child: Container()), - ], - ), - ), - ], - ), - ); - },); + }, + ); } } diff --git a/lib/screens/components/ImageReader.dart b/lib/screens/components/ImageReader.dart index e5d8657..0cc2971 100644 --- a/lib/screens/components/ImageReader.dart +++ b/lib/screens/components/ImageReader.dart @@ -12,10 +12,10 @@ import 'package:pikapi/basic/Cross.dart'; import 'package:pikapi/basic/Entities.dart'; import 'package:pikapi/basic/Method.dart'; import 'package:pikapi/basic/config/FullScreenAction.dart'; +import 'package:pikapi/basic/config/GalleryPreloadCount.dart'; import 'package:pikapi/basic/config/KeyboardController.dart'; import 'package:pikapi/basic/config/ReaderDirection.dart'; import 'package:pikapi/basic/config/ReaderType.dart'; -import 'package:pikapi/basic/config/VolumeController.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import '../FilePhotoViewScreen.dart'; import 'gesture_zoom_box.dart'; @@ -685,6 +685,27 @@ class _GalleryReaderState extends State<_GalleryReader> { _current = value + 1; _slider = value + 1; widget.struct.onPositionChange(value); + if (galleryPrePreloadCount > 0) { + for (var count = 1; + count <= galleryPrePreloadCount && value - count >= 0; + count++) { + var target = widget.struct.images[value - count]; + if (target.downloadLocalPath == null) { + method.remoteImagePreload(target.fileServer, target.path); + } + } + } + if (galleryPreloadCount > 0) { + for (var count = 1; + count <= galleryPreloadCount && + value + count < widget.struct.images.length; + count++) { + var target = widget.struct.images[value + count]; + if (target.downloadLocalPath == null) { + method.remoteImagePreload(target.fileServer, target.path); + } + } + } }); }, itemCount: widget.struct.images.length, diff --git a/pubspec.yaml b/pubspec.yaml index d688ac4..0741ffe 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -39,9 +39,9 @@ dependencies: filesystem_picker: ^2.0.0-nullsafety.0 url_launcher: ^6.0.9 clipboard: ^0.1.3 - flutter_datetime_picker: ^1.5.1 photo_view: ^0.12.0 multi_select_flutter: ^4.0.0 + flutter_datetime_picker: ^1.5.1 dev_dependencies: flutter_test: