Add single page viewer

This commit is contained in:
2023-11-28 15:26:52 +08:00
parent bcb08750eb
commit b9259f5086
11 changed files with 359 additions and 17 deletions

View File

@@ -210,6 +210,36 @@ class EHApi extends __EHApi {
return _getTags(ids.join(","), cancel: cancel); return _getTags(ids.join(","), cancel: cancel);
} }
String getFileUrl(int id) {
final uri = Uri.parse(_combineBaseUrls(_dio.options.baseUrl, baseUrl));
final newUri = uri.resolve("file/$id");
return newUri.toString();
}
String getThumbnailUrl(int id,
{int? max,
int? width,
int? height,
int? quality,
bool? force,
ThumbnailMethod? method,
ThumbnailAlign? align}) {
final uri = Uri.parse(_combineBaseUrls(_dio.options.baseUrl, baseUrl));
final queryParameters = <String, dynamic>{
r'max': max,
r'width': width,
r'height': height,
r'quality': quality,
r'force': force,
r'method': method?.name,
r'align': align?.name,
};
queryParameters.removeWhere((k, v) => v == null);
final newUri =
uri.resolve("thumbnail/$id").replace(queryParameters: queryParameters);
return newUri.toString();
}
String exportGalleryZipUrl(int gid, String exportGalleryZipUrl(int gid,
{bool? jpnTitle, int? maxLength, bool? exportAd}) { {bool? jpnTitle, int? maxLength, bool? exportAd}) {
final uri = Uri.parse(_combineBaseUrls(_dio.options.baseUrl, baseUrl)); final uri = Uri.parse(_combineBaseUrls(_dio.options.baseUrl, baseUrl));

View File

@@ -38,7 +38,9 @@ class GalleryBasicInfo extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [ children: [
ElevatedButton( ElevatedButton(
onPressed: null, onPressed: () {
context.push('/gallery/${gMeta.gid}/page/1');
},
child: Text(AppLocalizations.of(context)!.read)), child: Text(AppLocalizations.of(context)!.read)),
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () {

View File

@@ -68,7 +68,7 @@ class _GalleryInfo extends State<GalleryInfo> with ThemeModeWidget {
]), ]),
), ),
ThumbnailGridView( ThumbnailGridView(
widget.gData.pages, widget.gData,
SliverGridDelegateWithFixedCrossAxisCount( SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: useMobile ? 2 : 5), crossAxisCount: useMobile ? 2 : 5),
files: widget.files, files: widget.files,

View File

@@ -131,6 +131,13 @@ class GalleryInfoDesktop extends StatelessWidget {
SizedBox( SizedBox(
width: 150, width: 150,
child: Column(children: [ child: Column(children: [
ElevatedButton(
onPressed: () {
context.push(
'/gallery/${gData.meta.gid}/page/1');
},
child:
Text(AppLocalizations.of(context)!.read)),
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () {
context.push( context.push(

View File

@@ -3,21 +3,32 @@ import 'dart:ui';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:go_router/go_router.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:palette_generator/palette_generator.dart'; import 'package:palette_generator/palette_generator.dart';
import '../api/client.dart'; import '../api/client.dart';
import '../api/file.dart';
import '../api/gallery.dart'; import '../api/gallery.dart';
import '../globals.dart'; import '../globals.dart';
import '../utils.dart'; import '../utils.dart';
import '../utils/clipboard.dart'; import '../utils/clipboard.dart';
import '../viewer/single.dart';
import 'image.dart'; import 'image.dart';
final _log = Logger("Thumbnail"); final _log = Logger("Thumbnail");
class Thumbnail extends StatefulWidget { class Thumbnail extends StatefulWidget {
const Thumbnail(ExtendedPMeta pMeta, const Thumbnail(ExtendedPMeta pMeta,
{Key? key, int? max, int? width, int? height, int? fileId, this.gid}) {Key? key,
int? max,
int? width,
int? height,
int? fileId,
this.gid,
this.index,
this.files,
this.gdata})
: _pMeta = pMeta, : _pMeta = pMeta,
_max = max ?? 1200, _max = max ?? 1200,
_width = width, _width = width,
@@ -30,6 +41,9 @@ class Thumbnail extends StatefulWidget {
final int? _height; final int? _height;
final int? _fileId; final int? _fileId;
final int? gid; final int? gid;
final int? index;
final EhFiles? files;
final GalleryData? gdata;
int get height => _height != null int get height => _height != null
? _height! ? _height!
@@ -223,6 +237,19 @@ class _Thumbnail extends State<Thumbnail> {
]; ];
return list; return list;
})); }));
final timg = _data != null
? ImageWithContextMenu(_data!,
uri: _uri, fileName: _fileName, dir: _dir)
: null;
final img = widget.gid != null && widget.index != null && _data != null
? GestureDetector(
onTap: () {
context.push("/gallery/${widget.gid}/page/${widget.index}",
extra: SinglePageViewerExtra(
data: widget.gdata, files: widget.files));
},
child: timg)
: timg;
return SizedBox( return SizedBox(
width: widget.width.toDouble(), width: widget.width.toDouble(),
height: widget.height.toDouble(), height: widget.height.toDouble(),
@@ -240,10 +267,7 @@ class _Thumbnail extends State<Thumbnail> {
sigmaX: 10, sigmaX: 10,
sigmaY: 10, sigmaY: 10,
tileMode: TileMode.decal), tileMode: TileMode.decal),
child: ImageWithContextMenu(_data!, child: img)),
uri: _uri,
fileName: _fileName,
dir: _dir))),
SizedBox( SizedBox(
width: widget.width.toDouble(), width: widget.width.toDouble(),
height: widget.height.toDouble(), height: widget.height.toDouble(),
@@ -265,8 +289,7 @@ class _Thumbnail extends State<Thumbnail> {
SizedBox( SizedBox(
width: widget.width.toDouble(), width: widget.width.toDouble(),
height: widget.height.toDouble(), height: widget.height.toDouble(),
child: ImageWithContextMenu(_data!, child: img),
uri: _uri, fileName: _fileName, dir: _dir)),
moreVertMenu moreVertMenu
]) ])
: Center( : Center(

View File

@@ -5,10 +5,10 @@ import '../globals.dart';
import 'thumbnail.dart'; import 'thumbnail.dart';
class ThumbnailGridView extends StatelessWidget { class ThumbnailGridView extends StatelessWidget {
const ThumbnailGridView(this.pages, this.gridDelegate, const ThumbnailGridView(this.gdata, this.gridDelegate,
{Key? key, this.files, this.gid}) {Key? key, this.files, this.gid})
: super(key: key); : super(key: key);
final List<ExtendedPMeta> pages; final GalleryData gdata;
final int? gid; final int? gid;
final EhFiles? files; final EhFiles? files;
final SliverGridDelegate gridDelegate; final SliverGridDelegate gridDelegate;
@@ -16,7 +16,8 @@ class ThumbnailGridView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final displayAd = prefs.getBool("displayAd") ?? false; final displayAd = prefs.getBool("displayAd") ?? false;
final npages = displayAd ? pages : pages.where((e) => !e.isAd).toList(); final npages =
displayAd ? gdata.pages : gdata.pages.where((e) => !e.isAd).toList();
return SliverGrid.builder( return SliverGrid.builder(
gridDelegate: gridDelegate, gridDelegate: gridDelegate,
itemCount: npages.length, itemCount: npages.length,
@@ -26,7 +27,12 @@ class ThumbnailGridView extends StatelessWidget {
files != null ? files!.files[page.token]!.first.id : null; files != null ? files!.files[page.token]!.first.id : null;
return Container( return Container(
padding: const EdgeInsets.all(4), padding: const EdgeInsets.all(4),
child: Thumbnail(page, fileId: fileId, gid: gid)); child: Thumbnail(page,
fileId: fileId,
gid: gid,
index: index + 1,
files: files,
gdata: gdata));
}); });
} }
} }

View File

@@ -19,6 +19,7 @@ import 'logs/file.dart';
import 'set_server.dart'; import 'set_server.dart';
import 'settings.dart'; import 'settings.dart';
import 'utils.dart'; import 'utils.dart';
import 'viewer/single.dart';
final _routerLog = Logger("Router"); final _routerLog = Logger("Router");
@@ -83,7 +84,7 @@ final _router = GoRouter(
return null; return null;
} catch (e) { } catch (e) {
_routerLog.warning("Failed to parse gid:", e); _routerLog.warning("Failed to parse gid:", e);
return "/"; return "/gallery";
} }
}), }),
GoRoute( GoRoute(
@@ -106,6 +107,33 @@ final _router = GoRouter(
return "/"; return "/";
} }
}), }),
GoRoute(
path: SinglePageViewer.routeName,
builder: (context, state) {
final extra = state.extra as SinglePageViewerExtra?;
return SinglePageViewer(
gid: int.parse(state.pathParameters["gid"]!),
index: int.parse(state.pathParameters["index"]!),
key: state.pageKey,
data: extra?.data,
files: extra?.files,
);
},
redirect: (context, state) {
try {
int.parse(state.pathParameters["gid"]!);
} catch (e) {
_routerLog.warning("Failed to parse gid:", e);
return "/gallery";
}
try {
int.parse(state.pathParameters["index"]!);
return null;
} catch (e) {
_routerLog.warning("Failed to parse index:", e);
return "/gallery/${state.pathParameters["gid"]}";
}
}),
], ],
); );

203
lib/viewer/single.dart Normal file
View File

@@ -0,0 +1,203 @@
import 'package:dio/dio.dart';
import 'package:dio_image_provider/dio_image_provider.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:go_router/go_router.dart';
import 'package:keymap/keymap.dart';
import 'package:logging/logging.dart';
import 'package:photo_view/photo_view.dart';
import 'package:photo_view/photo_view_gallery.dart';
import '../api/file.dart';
import '../api/gallery.dart';
import '../globals.dart';
final _log = Logger("SinglePageViewer");
class SinglePageViewerExtra {
const SinglePageViewerExtra({this.data, this.files});
final GalleryData? data;
final EhFiles? files;
}
class SinglePageViewer extends StatefulWidget {
const SinglePageViewer(
{Key? key, required this.gid, required this.index, this.data, this.files})
: super(key: key);
final GalleryData? data;
final EhFiles? files;
final int gid;
final int index;
static const String routeName = '/gallery/:gid/page/:index';
@override
State<SinglePageViewer> createState() => _SinglePageViewer();
}
class _SinglePageViewer extends State<SinglePageViewer> with ThemeModeWidget {
late PageController _pageController;
late int _index;
late GalleryData? _data;
late EhFiles? _files;
CancelToken? _cancel;
bool _isLoading = false;
bool _page_changed = false;
Object? _error;
@override
void initState() {
_index = widget.index - 1;
_pageController = PageController(initialPage: _index);
_data = widget.data;
_files = widget.files;
super.initState();
}
@override
void dispose() {
_cancel?.cancel();
_pageController.dispose();
super.dispose();
}
Future<void> _fetchData() async {
try {
_cancel = CancelToken();
_isLoading = true;
if (_data == null) {
final data = (await api.getGallery(widget.gid)).unwrap();
_data = data;
}
final fileData = (await api.getFiles(
_data!.pages.map((e) => e.token).toList(),
cancel: _cancel))
.unwrap();
if (!_cancel!.isCancelled) {
if (_index < 0) {
_index = 0;
_pageController.jumpToPage(_index);
_page_changed = true;
} else if (_index >= _data!.pages.length) {
_index = _data!.pages.length - 1;
_pageController.jumpToPage(_index);
_page_changed = true;
}
setState(() {
_files = fileData;
_isLoading = false;
});
}
} catch (e) {
if (!_cancel!.isCancelled) {
_log.severe("Failed to load gallery ${widget.gid}:", e);
setState(() {
_error = e;
_isLoading = false;
});
}
}
}
void _onPageChanged(BuildContext context) {
context.replace("/gallery/${widget.gid}/page/${_index + 1}",
extra: SinglePageViewerExtra(data: _data, files: _files));
_page_changed = false;
}
@override
Widget build(BuildContext context) {
final isLoading = _error == null && (_data == null || _files == null);
if (isLoading && !_isLoading) _fetchData();
if (_data == null || _files == null) {
return Scaffold(
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
context.canPop() ? context.pop() : context.go("/");
},
),
title: Text(AppLocalizations.of(context)!.loading),
actions: [
buildThemeModeIcon(context),
buildMoreVertSettingsButon(context),
],
),
body: isLoading
? const Center(child: CircularProgressIndicator())
: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SelectableText("Error $_error"),
ElevatedButton.icon(
onPressed: () {
_fetchData();
setState(() {
_error = null;
});
},
icon: const Icon(Icons.refresh),
label: Text(AppLocalizations.of(context)!.retry))
])));
}
if (_page_changed) {
_onPageChanged(context);
}
return Scaffold(
backgroundColor: Colors.black,
extendBody: true,
extendBodyBehindAppBar: true,
body: KeyboardWidget(
bindings: [
KeyAction(LogicalKeyboardKey.arrowLeft, "previous page", () {
if (_index > 0) {
_index -= 1;
_pageController.previousPage(
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut);
}
}),
KeyAction(LogicalKeyboardKey.arrowRight, "next page", () {
if (_index < _data!.pages.length - 1) {
_index += 1;
_pageController.nextPage(
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut);
}
}),
KeyAction(LogicalKeyboardKey.backspace, "back", () {
context.canPop() ? context.pop() : context.go("/");
}),
],
child: PhotoViewGallery.builder(
scrollPhysics: const BouncingScrollPhysics(),
pageController: _pageController,
itemCount: _data!.pages.length,
builder: (BuildContext context, int index) {
final data = _data!.pages[index];
final f = _files!.files[data.token]!.first;
return PhotoViewGalleryPageOptions(
imageProvider: DioImage.string(
api.getFileUrl(f.id),
dio: dio,
),
initialScale: PhotoViewComputedScale.contained,
heroAttributes: PhotoViewHeroAttributes(
tag: data.token,
transitionOnUserGestures: true,
),
filterQuality: FilterQuality.high,
);
},
onPageChanged: (index) {
_index = index;
SchedulerBinding.instance.addPostFrameCallback((_) {
_onPageChanged(context);
});
},
),
),
);
}
}

View File

@@ -101,9 +101,9 @@ if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
endif() endif()
# Start with a clean build bundle directory every time. # Start with a clean build bundle directory every time.
install(CODE " # install(CODE "
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") # file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
" COMPONENT Runtime) # " COMPONENT Runtime)
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")

View File

@@ -242,6 +242,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.0+1" version: "3.1.0+1"
dio_image_provider:
dependency: "direct main"
description:
name: dio_image_provider
sha256: d7937f2b33c52db70cbf6209cf6ef6a929e006afcadaebfb6b0dc2da80dce52b
url: "https://pub.dev"
source: hosted
version: "0.0.2"
enum_flag: enum_flag:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -316,6 +324,14 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_markdown:
dependency: transitive
description:
name: flutter_markdown
sha256: "35108526a233cc0755664d445f8a6b4b61e6f8fe993b3658b80b4a26827fc196"
url: "https://pub.dev"
source: hosted
version: "0.6.18+2"
flutter_staggered_grid_view: flutter_staggered_grid_view:
dependency: transitive dependency: transitive
description: description:
@@ -462,6 +478,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.7.1" version: "6.7.1"
keymap:
dependency: "direct main"
description:
name: keymap
sha256: "837f37bce794bb41c3a49cd122718544921afec348f34c78f6153c72b43f3c10"
url: "https://pub.dev"
source: hosted
version: "0.0.92"
lints: lints:
dependency: transitive dependency: transitive
description: description:
@@ -478,6 +502,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.0" version: "1.2.0"
markdown:
dependency: transitive
description:
name: markdown
sha256: acf35edccc0463a9d7384e437c015a3535772e09714cf60e07eeef3a15870dcd
url: "https://pub.dev"
source: hosted
version: "7.1.1"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:
@@ -590,6 +622,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.4.0" version: "5.4.0"
photo_view:
dependency: "direct main"
description:
name: photo_view
sha256: "8036802a00bae2a78fc197af8a158e3e2f7b500561ed23b4c458107685e645bb"
url: "https://pub.dev"
source: hosted
version: "0.14.0"
pixel_snap: pixel_snap:
dependency: transitive dependency: transitive
description: description:

View File

@@ -12,6 +12,7 @@ dependencies:
cryptography_flutter: ^2.3.0 cryptography_flutter: ^2.3.0
dio: ^5.3.2 dio: ^5.3.2
dio_cookie_manager: ^3.1.0+1 dio_cookie_manager: ^3.1.0+1
dio_image_provider: any
enum_flag: ^1.0.2 enum_flag: ^1.0.2
event_listener: ^0.2.0 event_listener: ^0.2.0
file: ^6.1.4 file: ^6.1.4
@@ -27,10 +28,12 @@ dependencies:
infinite_scroll_pagination: ^4.0.0 infinite_scroll_pagination: ^4.0.0
intl: any intl: any
json_annotation: ^4.8.1 json_annotation: ^4.8.1
keymap: ^0.0.92
logging: ^1.2.0 logging: ^1.2.0
palette_generator: ^0.3.3+3 palette_generator: ^0.3.3+3
path: ^1.8.3 path: ^1.8.3
path_provider: ^2.1.0 path_provider: ^2.1.0
photo_view: ^0.14.0
retrofit: ^4.0.1 retrofit: ^4.0.1
shared_preferences: ^2.2.0 shared_preferences: ^2.2.0
super_clipboard: ^0.6.4 super_clipboard: ^0.6.4