preload image from network in gallery mode

This commit is contained in:
niuhuan 2021-09-30 15:12:23 +08:00
parent fd175feb80
commit f5adcbd2e0
17 changed files with 278 additions and 156 deletions

View File

@ -109,12 +109,13 @@ VPN->代理->分流, 这三个功能如果同时设置, 您会在您手机的VPN
sudo apt install xorg-dev sudo apt install xorg-dev
``` ```
- 字体不显示 - 字体不显示
1. 将字体文件复制到项目目录下
```shell ```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 ```yaml
# 编辑 pubspec.yaml
fonts: fonts:
- family: Roboto - family: Roboto
fonts: fonts:

View File

@ -35,10 +35,11 @@ func main() {
if height <= 0 { if height <= 0 {
height = 900 height = 900
} }
sizeOption := flutter.WindowInitialDimensions(width, height) var runOptions []flutter.Option
options = append(options, sizeOption) runOptions = append(runOptions, flutter.WindowInitialDimensions(width, height))
// runOptions = append(runOptions, options...)
err := flutter.Run(append(options, mainOptions...)...) // ------
err := flutter.Run(append(runOptions, mainOptions...)...)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)

View File

@ -169,10 +169,46 @@ func remoteImageData(params string) (string, error) {
defer lock.Unlock() defer lock.Unlock()
cache := comic_center.FindRemoteImage(fileServer, path) cache := comic_center.FindRemoteImage(fileServer, path)
if cache == nil { if cache == nil {
remote, err := decodeAndSaveImage(fileServer, path)
if err != nil {
return "", err
}
cache = remote
}
display := DisplayImageData{
FileSize: cache.FileSize,
Format: cache.Format,
Width: cache.Width,
Height: cache.Height,
FinalPath: remotePath(cache.LocalPath),
}
return serialize(&display, nil)
}
func remoteImagePreload(params string) error {
var paramsStruct struct {
FileServer string `json:"fileServer"`
Path string `json:"path"`
}
json.Unmarshal([]byte(params), &paramsStruct)
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) buff, img, format, err := decodeFromUrl(fileServer, path)
if err != nil { if err != nil {
println(fmt.Sprintf("decode error : %s/static/%s %s", fileServer, path, err.Error())) println(fmt.Sprintf("decode error : %s/static/%s %s", fileServer, path, err.Error()))
return "", err return nil, err
} }
local := local :=
fmt.Sprintf("%x", fmt.Sprintf("%x",
@ -184,7 +220,7 @@ func remoteImageData(params string) (string, error) {
buff, os.FileMode(0600), buff, os.FileMode(0600),
) )
if err != nil { if err != nil {
return "", err return nil, err
} }
remote := comic_center.RemoteImage{ remote := comic_center.RemoteImage{
FileServer: fileServer, FileServer: fileServer,
@ -196,19 +232,7 @@ func remoteImageData(params string) (string, error) {
LocalPath: local, LocalPath: local,
} }
err = comic_center.SaveRemoteImage(&remote) err = comic_center.SaveRemoteImage(&remote)
if err != nil { return &remote, err
return "", err
}
cache = &remote
}
display := DisplayImageData{
FileSize: cache.FileSize,
Format: cache.Format,
Width: cache.Width,
Height: cache.Height,
FinalPath: remotePath(cache.LocalPath),
}
return serialize(&display, nil)
} }
func downloadImagePath(path string) (string, error) { func downloadImagePath(path string) (string, error) {
@ -565,6 +589,8 @@ func FlatInvoke(method string, params string) (string, error) {
return "", importComicDownloadUsingSocket(params) return "", importComicDownloadUsingSocket(params)
case "remoteImageData": case "remoteImageData":
return remoteImageData(params) return remoteImageData(params)
case "remoteImagePreload":
return "", remoteImagePreload(params)
case "clientIpSet": case "clientIpSet":
return clientIpSet() return clientIpSet()
case "downloadImagePath": case "downloadImagePath":

View File

@ -150,6 +150,13 @@ class Method {
return RemoteImageData.fromJson(json.decode(data)); return RemoteImageData.fromJson(json.decode(data));
} }
Future<dynamic> remoteImagePreload(String fileServer, String path) async {
return _flatInvoke("remoteImagePreload", {
"fileServer": fileServer,
"path": path,
});
}
Future<String> downloadImagePath(String path) async { Future<String> downloadImagePath(String path) async {
return await _flatInvoke("downloadImagePath", path); return await _flatInvoke("downloadImagePath", path);
} }

View File

@ -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;

View File

@ -50,6 +50,6 @@ Widget shadowCategoriesActionButton(BuildContext context) {
onPressed: () { onPressed: () {
chooseShadowCategories(context); chooseShadowCategories(context);
}, },
icon: Icon(Icons.dnd_forwardslash), icon: Icon(Icons.hide_source),
); );
} }

View File

@ -165,7 +165,8 @@ Future<dynamic> initTheme() async {
_androidVersion = await method.androidGetVersion(); _androidVersion = await method.androidGetVersion();
if (_androidVersion >= 29) { if (_androidVersion >= 29) {
_androidNightMode = _androidNightMode =
(await method.loadProperty(_nightModePropertyName, "false")) == "true"; (await method.loadProperty(_nightModePropertyName, "false")) ==
"true";
_systemNight = (await method.androidGetUiMode()) == "NIGHT"; _systemNight = (await method.androidGetUiMode()) == "NIGHT";
EventChannel("ui_mode").receiveBroadcastStream().listen((event) { EventChannel("ui_mode").receiveBroadcastStream().listen((event) {
_systemNight = "$event" == "NIGHT"; _systemNight = "$event" == "NIGHT";
@ -185,13 +186,7 @@ Future<dynamic> chooseTheme(BuildContext buildContext) async {
builder: (BuildContext context, StateSetter setState) { builder: (BuildContext context, StateSetter setState) {
var list = <SimpleDialogOption>[]; var list = <SimpleDialogOption>[];
if (_androidVersion >= 29) { if (_androidVersion >= 29) {
list.add( var onChange = (bool? v) async {
SimpleDialogOption(
child: Row(
children: [
Checkbox(
value: _androidNightMode,
onChanged: (bool? v) async {
if (v != null) { if (v != null) {
await method.saveProperty( await method.saveProperty(
_nightModePropertyName, "$v"); _nightModePropertyName, "$v");
@ -199,11 +194,43 @@ Future<dynamic> chooseTheme(BuildContext buildContext) async {
} }
setState(() {}); setState(() {});
themeEvent.broadcast(); themeEvent.broadcast();
}), };
list.add(
SimpleDialogOption(
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("随手机进入夜间模式"), Text("随手机进入夜间模式"),
], ],
), ),
), ),
),
),
); );
} }
list.addAll(_themePackages list.addAll(_themePackages

View File

@ -1,4 +1,5 @@
const ERROR_TYPE_NETWORK = "NETWORK_ERROR"; const ERROR_TYPE_NETWORK = "NETWORK_ERROR";
const ERROR_TYPE_PERMISSION = "PERMISSION_ERROR";
// , 便 // , 便
String errorType(String error) { String errorType(String error) {
@ -11,5 +12,8 @@ String errorType(String error) {
error.contains("deadline")) { error.contains("deadline")) {
return ERROR_TYPE_NETWORK; return ERROR_TYPE_NETWORK;
} }
if (error.contains("permission denied")) {
return ERROR_TYPE_PERMISSION;
}
return ""; return "";
} }

View File

@ -60,23 +60,6 @@ class _ComicsScreenState extends State<ComicsScreen> {
}, },
); );
@override
void initState() {
shadowCategoriesEvent.subscribe(_onShadowChange);
super.initState();
}
@override
void dispose() {
shadowCategoriesEvent.unsubscribe(_onShadowChange);
super.dispose();
}
void _onShadowChange(EventArgs? args) {
setState(() {
});
}
Widget _chooseCategoryAction() => IconButton( Widget _chooseCategoryAction() => IconButton(
onPressed: () async { onPressed: () async {
String? category = await chooseListDialog(context, '请选择分类', [ String? category = await chooseListDialog(context, '请选择分类', [
@ -142,6 +125,7 @@ class _ComicsScreenState extends State<ComicsScreen> {
appBar = AppBar( appBar = AppBar(
title: Text(title), title: Text(title),
actions: [ actions: [
shadowCategoriesActionButton(context),
chooseLayoutAction(context), chooseLayoutAction(context),
_chooseCategoryAction(), _chooseCategoryAction(),
], ],

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:pikapi/basic/Entities.dart'; import 'package:pikapi/basic/Entities.dart';
import 'package:pikapi/basic/Method.dart'; import 'package:pikapi/basic/Method.dart';
import 'package:pikapi/basic/config/ListLayout.dart'; import 'package:pikapi/basic/config/ListLayout.dart';
import 'package:pikapi/basic/config/ShadowCategories.dart';
import 'components/ComicListBuilder.dart'; import 'components/ComicListBuilder.dart';
@ -25,6 +26,7 @@ class _RandomComicsScreenState extends State<RandomComicsScreen> {
appBar: AppBar( appBar: AppBar(
title: Text('随机本子'), title: Text('随机本子'),
actions: [ actions: [
shadowCategoriesActionButton(context),
chooseLayoutAction(context), chooseLayoutAction(context),
], ],
), ),

View File

@ -2,10 +2,12 @@ import 'package:flutter/material.dart';
import 'package:pikapi/basic/Entities.dart'; import 'package:pikapi/basic/Entities.dart';
import 'package:pikapi/basic/Method.dart'; import 'package:pikapi/basic/Method.dart';
import 'package:pikapi/basic/config/ListLayout.dart'; import 'package:pikapi/basic/config/ListLayout.dart';
import 'package:pikapi/basic/config/ShadowCategories.dart';
import 'components/ComicListBuilder.dart'; import 'components/ComicListBuilder.dart';
class RankingsScreen extends StatelessWidget { class RankingsScreen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var theme = Theme.of(context); var theme = Theme.of(context);
@ -13,6 +15,7 @@ class RankingsScreen extends StatelessWidget {
appBar: AppBar( appBar: AppBar(
title: Text('排行榜'), title: Text('排行榜'),
actions: [ actions: [
shadowCategoriesActionButton(context),
chooseLayoutAction(context), chooseLayoutAction(context),
], ],
), ),

View File

@ -96,23 +96,6 @@ class _SearchScreenState extends State<SearchScreen> {
} }
} }
@override
void initState() {
shadowCategoriesEvent.subscribe(_onShadowChange);
super.initState();
}
@override
void dispose() {
shadowCategoriesEvent.unsubscribe(_onShadowChange);
super.dispose();
}
void _onShadowChange(EventArgs? args) {
setState(() {
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(

View File

@ -1,28 +1,51 @@
import 'package:event/event.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pikapi/basic/Entities.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/ComicList.dart';
import 'package:pikapi/screens/components/FitButton.dart'; import 'package:pikapi/screens/components/FitButton.dart';
import 'ContentBuilder.dart'; import 'ContentBuilder.dart';
class ComicListBuilder extends StatelessWidget { class ComicListBuilder extends StatefulWidget {
final Future<List<ComicSimple>> future; final Future<List<ComicSimple>> future;
final Future Function() reload; final Future Function() reload;
ComicListBuilder(this.future, this.reload); ComicListBuilder(this.future, this.reload);
@override
State<StatefulWidget> createState() => _ComicListBuilderState();
}
class _ComicListBuilderState extends State<ComicListBuilder> {
@override
void initState() {
shadowCategoriesEvent.subscribe(_onShadowChange);
super.initState();
}
@override
void dispose() {
shadowCategoriesEvent.unsubscribe(_onShadowChange);
super.dispose();
}
void _onShadowChange(EventArgs? args) {
setState(() {});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ContentBuilder( return ContentBuilder(
future: future, future: widget.future,
onRefresh: reload, onRefresh: widget.reload,
successBuilder: successBuilder:
(BuildContext context, AsyncSnapshot<List<ComicSimple>> snapshot) { (BuildContext context, AsyncSnapshot<List<ComicSimple>> snapshot) {
return RefreshIndicator( return RefreshIndicator(
onRefresh: reload, onRefresh: widget.reload,
child: ComicList( child: ComicList(
snapshot.data!, snapshot.data!,
appendWidget: FitButton( appendWidget: FitButton(
onPressed: reload, onPressed: widget.reload,
text: '刷新', text: '刷新',
), ),
), ),

View File

@ -1,7 +1,9 @@
import 'package:event/event.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:pikapi/basic/Entities.dart'; import 'package:pikapi/basic/Entities.dart';
import 'package:pikapi/basic/config/PagerAction.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/basic/enum/Sort.dart';
import 'package:pikapi/screens/components/ComicList.dart'; import 'package:pikapi/screens/components/ComicList.dart';
import 'package:pikapi/screens/components/ContentError.dart'; import 'package:pikapi/screens/components/ContentError.dart';
@ -9,18 +11,41 @@ import 'package:pikapi/screens/components/FitButton.dart';
import 'ContentLoading.dart'; import 'ContentLoading.dart';
// //
class ComicPager extends StatelessWidget { class ComicPager extends StatefulWidget {
final Future<ComicsPage> Function(String sort, int page) fetchPage; final Future<ComicsPage> Function(String sort, int page) fetchPage;
const ComicPager({required this.fetchPage}); const ComicPager({required this.fetchPage});
@override
State<StatefulWidget> createState() => _ComicPagerState();
}
class _ComicPagerState extends State<ComicPager> {
@override
void initState() {
shadowCategoriesEvent.subscribe(_onShadowChange);
super.initState();
}
@override
void dispose() {
shadowCategoriesEvent.unsubscribe(_onShadowChange);
super.dispose();
}
void _onShadowChange(EventArgs? args) {
setState(() {
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
switch (currentPagerAction) { switch (currentPagerAction) {
case PagerAction.CONTROLLER: case PagerAction.CONTROLLER:
return ControllerComicPager(fetchPage: fetchPage); return ControllerComicPager(fetchPage: widget.fetchPage);
case PagerAction.STREAM: case PagerAction.STREAM:
return StreamComicPager(fetchPage: fetchPage); return StreamComicPager(fetchPage: widget.fetchPage);
default: default:
return Container(); return Container();
} }

View File

@ -24,11 +24,15 @@ class ContentError extends StatelessWidget {
case ERROR_TYPE_NETWORK: case ERROR_TYPE_NETWORK:
message = "连接不上啦, 请检查网络"; message = "连接不上啦, 请检查网络";
break; break;
case ERROR_TYPE_PERMISSION:
message = "没有权限或路径不可用";
break;
default: default:
message = "啊哦, 被玩坏了"; message = "啊哦, 被玩坏了";
break; break;
} }
return LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) { return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
print("$error"); print("$error");
print("$stackTrace"); print("$stackTrace");
var width = constraints.maxWidth; var width = constraints.maxWidth;
@ -117,6 +121,7 @@ class ContentError extends StatelessWidget {
], ],
), ),
); );
},); },
);
} }
} }

View File

@ -12,10 +12,10 @@ import 'package:pikapi/basic/Cross.dart';
import 'package:pikapi/basic/Entities.dart'; import 'package:pikapi/basic/Entities.dart';
import 'package:pikapi/basic/Method.dart'; import 'package:pikapi/basic/Method.dart';
import 'package:pikapi/basic/config/FullScreenAction.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/KeyboardController.dart';
import 'package:pikapi/basic/config/ReaderDirection.dart'; import 'package:pikapi/basic/config/ReaderDirection.dart';
import 'package:pikapi/basic/config/ReaderType.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 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import '../FilePhotoViewScreen.dart'; import '../FilePhotoViewScreen.dart';
import 'gesture_zoom_box.dart'; import 'gesture_zoom_box.dart';
@ -685,6 +685,27 @@ class _GalleryReaderState extends State<_GalleryReader> {
_current = value + 1; _current = value + 1;
_slider = value + 1; _slider = value + 1;
widget.struct.onPositionChange(value); 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, itemCount: widget.struct.images.length,

View File

@ -39,9 +39,9 @@ dependencies:
filesystem_picker: ^2.0.0-nullsafety.0 filesystem_picker: ^2.0.0-nullsafety.0
url_launcher: ^6.0.9 url_launcher: ^6.0.9
clipboard: ^0.1.3 clipboard: ^0.1.3
flutter_datetime_picker: ^1.5.1
photo_view: ^0.12.0 photo_view: ^0.12.0
multi_select_flutter: ^4.0.0 multi_select_flutter: ^4.0.0
flutter_datetime_picker: ^1.5.1
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: