This commit is contained in:
2023-09-03 22:37:25 +08:00
parent b65dc78fb2
commit 60696e01f6
5 changed files with 194 additions and 14 deletions

View File

@@ -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<HttpResponse> getFile(@Path("id") int id);
@DioResponseType(ResponseType.bytes)
Future<HttpResponse<List<int>>> getFile(@Path("id") int id);
@GET('/file/{id}')
// ignore: unused_element
Future<ApiResult<EhFileExtend>> _getFileData(
@Path("id") int id, @Query("data") bool data);
@GET('/file/random')
Future<HttpResponse> getRandomFile(
@DioResponseType(ResponseType.bytes)
Future<HttpResponse<List<int>>> getRandomFile(
{@Query("is_nsfw") bool? isNsfw,
@Query("is_ad") bool? isAd,
@Query("thumb") bool? thumb});
@GET('/files/{token}')
// ignore: unused_element
Future<ApiResult<EhFiles>> _getFiles(@Path("token") String token);
@GET('/thumbnail/{id}')
@DioResponseType(ResponseType.bytes)
Future<HttpResponse<List<int>>> 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<ApiResult<GalleryData>> getGallery(@Path("gid") int gid);

View File

@@ -281,16 +281,17 @@ class __EHApi implements _EHApi {
}
@override
Future<HttpResponse<dynamic>> getFile(int id) async {
Future<HttpResponse<List<int>>> getFile(int id) async {
const _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
final Map<String, dynamic>? _data = null;
final _result =
await _dio.fetch(_setStreamType<HttpResponse<dynamic>>(Options(
final _result = await _dio
.fetch<List<dynamic>>(_setStreamType<HttpResponse<List<int>>>(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<int>();
final httpResponse = HttpResponse(value, _result);
return httpResponse;
}
@@ -342,7 +343,7 @@ class __EHApi implements _EHApi {
}
@override
Future<HttpResponse<dynamic>> getRandomFile({
Future<HttpResponse<List<int>>> getRandomFile({
bool? isNsfw,
bool? isAd,
bool? thumb,
@@ -356,11 +357,12 @@ class __EHApi implements _EHApi {
queryParameters.removeWhere((k, v) => v == null);
final _headers = <String, dynamic>{};
final Map<String, dynamic>? _data = null;
final _result =
await _dio.fetch(_setStreamType<HttpResponse<dynamic>>(Options(
final _result = await _dio
.fetch<List<dynamic>>(_setStreamType<HttpResponse<List<int>>>(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<int>();
final httpResponse = HttpResponse(value, _result);
return httpResponse;
}
@@ -408,6 +410,53 @@ class __EHApi implements _EHApi {
return value;
}
@override
Future<HttpResponse<List<int>>> getThumbnail(
int id, {
int? max,
int? width,
int? height,
int? quality,
bool? force,
ThumbnailMethod? method,
ThumbnailAlign? align,
}) async {
const _extra = <String, dynamic>{};
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 _headers = <String, dynamic>{};
final Map<String, dynamic>? _data = null;
final _result = await _dio
.fetch<List<dynamic>>(_setStreamType<HttpResponse<List<int>>>(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<int>();
final httpResponse = HttpResponse(value, _result);
return httpResponse;
}
@override
Future<ApiResult<GalleryData>> getGallery(int gid) async {
const _extra = <String, dynamic>{};

View File

@@ -42,11 +42,14 @@ class EhFileExtend {
class EhFiles {
const EhFiles({required this.files});
final Map<String, EhFileBasic> files;
final Map<String, List<EhFileBasic>> files;
factory EhFiles.fromJson(Map<String, dynamic> json) => EhFiles(
files: (json).map(
(k, e) =>
MapEntry(k, EhFileBasic.fromJson(e as Map<String, dynamic>)),
(k, e) => MapEntry(
k,
(e as List<dynamic>)
.map((e) => EhFileBasic.fromJson(e as Map<String, dynamic>))
.toList()),
),
);
}

View File

@@ -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<Thumbnail> createState() => _Thumbnail();
}
class _Thumbnail extends State<Thumbnail> {
Uint8List? _data;
bool _isLoading = false;
Object? _error;
int? _fileId;
Future<void> _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"));
}
}

View File

@@ -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<GalleryPage> with ThemeModeWidget {
? const Center(child: CircularProgressIndicator())
: _data != null
? Center(
child: Text("Gallery $_gid"),
child: Thumbnail(_data!.pages[0]!),
)
: Center(
child: Text("Error: $_error"),