diff --git a/lib/api/client.dart b/lib/api/client.dart index 6a8d10d..352249a 100644 --- a/lib/api/client.dart +++ b/lib/api/client.dart @@ -26,6 +26,22 @@ final _pbkdf2b = Pbkdf2( const _utf8Encoder = Utf8Encoder(); final _salt = _utf8Encoder.convert("eh-downloader-salt"); +enum ThumbnailMethod { + unknown, + cover, + contain, + fill; +} + +enum ThumbnailAlign { + left, + center, + right; + + static const top = left; + static const bottom = right; +} + @RestApi() abstract class _EHApi { factory _EHApi(Dio dio, {required String baseUrl}) = __EHApi; @@ -64,19 +80,31 @@ abstract class _EHApi { {@Query("token") String? token}); @GET('/file/{id}') - Future getFile(@Path("id") int id); + @DioResponseType(ResponseType.bytes) + Future>> getFile(@Path("id") int id); @GET('/file/{id}') // ignore: unused_element Future> _getFileData( @Path("id") int id, @Query("data") bool data); @GET('/file/random') - Future getRandomFile( + @DioResponseType(ResponseType.bytes) + Future>> getRandomFile( {@Query("is_nsfw") bool? isNsfw, @Query("is_ad") bool? isAd, @Query("thumb") bool? thumb}); @GET('/files/{token}') // ignore: unused_element Future> _getFiles(@Path("token") String token); + @GET('/thumbnail/{id}') + @DioResponseType(ResponseType.bytes) + Future>> getThumbnail(@Path("id") int id, + {@Query("max") int? max, + @Query("width") int? width, + @Query("height") int? height, + @Query("quality") int? quality, + @Query("force") bool? force, + @Query("method") ThumbnailMethod? method, + @Query("align") ThumbnailAlign? align}); @GET('/gallery/{gid}') Future> getGallery(@Path("gid") int gid); diff --git a/lib/api/client.g.dart b/lib/api/client.g.dart index 959d356..e3367ac 100644 --- a/lib/api/client.g.dart +++ b/lib/api/client.g.dart @@ -281,16 +281,17 @@ class __EHApi implements _EHApi { } @override - Future> getFile(int id) async { + Future>> getFile(int id) async { const _extra = {}; final queryParameters = {}; final _headers = {}; final Map? _data = null; - final _result = - await _dio.fetch(_setStreamType>(Options( + final _result = await _dio + .fetch>(_setStreamType>>(Options( method: 'GET', headers: _headers, extra: _extra, + responseType: ResponseType.bytes, ) .compose( _dio.options, @@ -303,7 +304,7 @@ class __EHApi implements _EHApi { _dio.options.baseUrl, baseUrl, )))); - final value = _result.data; + final value = _result.data!.cast(); final httpResponse = HttpResponse(value, _result); return httpResponse; } @@ -342,7 +343,7 @@ class __EHApi implements _EHApi { } @override - Future> getRandomFile({ + Future>> getRandomFile({ bool? isNsfw, bool? isAd, bool? thumb, @@ -356,11 +357,12 @@ class __EHApi implements _EHApi { queryParameters.removeWhere((k, v) => v == null); final _headers = {}; final Map? _data = null; - final _result = - await _dio.fetch(_setStreamType>(Options( + final _result = await _dio + .fetch>(_setStreamType>>(Options( method: 'GET', headers: _headers, extra: _extra, + responseType: ResponseType.bytes, ) .compose( _dio.options, @@ -373,7 +375,7 @@ class __EHApi implements _EHApi { _dio.options.baseUrl, baseUrl, )))); - final value = _result.data; + final value = _result.data!.cast(); final httpResponse = HttpResponse(value, _result); return httpResponse; } @@ -408,6 +410,53 @@ class __EHApi implements _EHApi { return value; } + @override + Future>> getThumbnail( + int id, { + int? max, + int? width, + int? height, + int? quality, + bool? force, + ThumbnailMethod? method, + ThumbnailAlign? align, + }) async { + const _extra = {}; + final queryParameters = { + 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 _headers = {}; + final Map? _data = null; + final _result = await _dio + .fetch>(_setStreamType>>(Options( + method: 'GET', + headers: _headers, + extra: _extra, + responseType: ResponseType.bytes, + ) + .compose( + _dio.options, + '/thumbnail/${id}', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + final value = _result.data!.cast(); + final httpResponse = HttpResponse(value, _result); + return httpResponse; + } + @override Future> getGallery(int gid) async { const _extra = {}; diff --git a/lib/api/file.dart b/lib/api/file.dart index 6404db4..9f83bf7 100644 --- a/lib/api/file.dart +++ b/lib/api/file.dart @@ -42,11 +42,14 @@ class EhFileExtend { class EhFiles { const EhFiles({required this.files}); - final Map files; + final Map> files; factory EhFiles.fromJson(Map json) => EhFiles( files: (json).map( - (k, e) => - MapEntry(k, EhFileBasic.fromJson(e as Map)), + (k, e) => MapEntry( + k, + (e as List) + .map((e) => EhFileBasic.fromJson(e as Map)) + .toList()), ), ); } diff --git a/lib/components/thumbnail.dart b/lib/components/thumbnail.dart new file mode 100644 index 0000000..49250fc --- /dev/null +++ b/lib/components/thumbnail.dart @@ -0,0 +1,99 @@ +import 'dart:typed_data'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:logging/logging.dart'; +import '../api/client.dart'; +import '../api/gallery.dart'; +import '../globals.dart'; + +final _log = Logger("Thumbnail"); + +class Thumbnail extends StatefulWidget { + const Thumbnail(ExtendedPMeta pMeta, + {Key? key, int? max, int? width, int? height, int? fileId}) + : _pMeta = pMeta, + _max = max ?? 1200, + _width = width, + _height = height, + _fileId = fileId, + super(key: key); + final ExtendedPMeta _pMeta; + final int _max; + final int? _width; + final int? _height; + final int? _fileId; + + int get height => _height != null + ? _height! + : _pMeta.height > _pMeta.width + ? _max + : _max * _pMeta.height ~/ _pMeta.width; + int get width => _width != null + ? _width! + : _pMeta.width > _pMeta.height + ? _max + : _max * _pMeta.width ~/ _pMeta.height; + + @override + State createState() => _Thumbnail(); +} + +class _Thumbnail extends State { + Uint8List? _data; + bool _isLoading = false; + Object? _error; + int? _fileId; + Future _fetchData() async { + try { + _isLoading = true; + if (_fileId == null) { + final token = widget._pMeta.token; + _fileId = (await api.getFiles([token])).unwrap().files[token]![0]!.id; + } + final re = await api.getThumbnail(_fileId!, + max: widget._max, + width: widget._width, + height: widget._height, + method: ThumbnailMethod.contain, + align: ThumbnailAlign.center); + if (re.response.statusCode != 200) { + throw Exception( + 'Failed to get thumbnail: ${re.response.statusCode} ${re.response.statusMessage}'); + } + final data = Uint8List.fromList(re.data); + setState(() { + _isLoading = false; + _data = data; + }); + } catch (e) { + _log.warning("Failed to get file data:", e); + setState(() { + _isLoading = false; + _error = e; + }); + } + } + + @override + void initState() { + _data = null; + _isLoading = false; + _error = null; + _fileId = widget._fileId; + super.initState(); + } + + @override + Widget build(BuildContext context) { + final isLoading = _data == null && _error == null; + if (isLoading && !_isLoading) _fetchData(); + return SizedBox( + width: widget.width.toDouble(), + height: widget.height.toDouble(), + child: isLoading + ? const Center(child: CircularProgressIndicator()) + : _data != null + ? Image.memory(_data!) + : Text("Error $_error")); + } +} diff --git a/lib/gallery.dart b/lib/gallery.dart index 49da547..fbe0a41 100644 --- a/lib/gallery.dart +++ b/lib/gallery.dart @@ -3,6 +3,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:go_router/go_router.dart'; import 'package:logging/logging.dart'; import 'api/gallery.dart'; +import 'components/thumbnail.dart'; import 'globals.dart'; final _log = Logger("GalleryPage"); @@ -79,7 +80,7 @@ class _GalleryPage extends State with ThemeModeWidget { ? const Center(child: CircularProgressIndicator()) : _data != null ? Center( - child: Text("Gallery $_gid"), + child: Thumbnail(_data!.pages[0]!), ) : Center( child: Text("Error: $_error"),