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

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

View File

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

View File

@ -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), &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)
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":

View File

@ -150,6 +150,13 @@ class Method {
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 {
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: () {
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();
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<dynamic> chooseTheme(BuildContext buildContext) async {
builder: (BuildContext context, StateSetter setState) {
var list = <SimpleDialogOption>[];
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("随手机进入夜间模式"),
],
),
),
),
),
);

View File

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

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(
onPressed: () async {
String? category = await chooseListDialog(context, '请选择分类', [
@ -142,6 +125,7 @@ class _ComicsScreenState extends State<ComicsScreen> {
appBar = AppBar(
title: Text(title),
actions: [
shadowCategoriesActionButton(context),
chooseLayoutAction(context),
_chooseCategoryAction(),
],

View File

@ -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<RandomComicsScreen> {
appBar: AppBar(
title: Text('随机本子'),
actions: [
shadowCategoriesActionButton(context),
chooseLayoutAction(context),
],
),

View File

@ -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),
],
),

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
Widget build(BuildContext context) {
return Scaffold(

View File

@ -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<List<ComicSimple>> future;
final Future Function() 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
Widget build(BuildContext context) {
return ContentBuilder(
future: future,
onRefresh: reload,
future: widget.future,
onRefresh: widget.reload,
successBuilder:
(BuildContext context, AsyncSnapshot<List<ComicSimple>> snapshot) {
return RefreshIndicator(
onRefresh: reload,
onRefresh: widget.reload,
child: ComicList(
snapshot.data!,
appendWidget: FitButton(
onPressed: reload,
onPressed: widget.reload,
text: '刷新',
),
),

View File

@ -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<ComicsPage> Function(String sort, int page) 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
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();
}

View File

@ -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()),
],
),
),
],
),
);
},);
},
);
}
}

View File

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

View File

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