diff --git a/lib/api/client.dart b/lib/api/client.dart index d1685a7..431f167 100644 --- a/lib/api/client.dart +++ b/lib/api/client.dart @@ -52,13 +52,17 @@ abstract class _EHApi { Future> createUser( @Part(name: "name") String name, @Part(name: "password") String password, {@Part(name: "is_admin") bool? isAdmin, - @Part(name: "permissions") int? permissions}); + @Part(name: "permissions") int? permissions, + @CancelRequest() CancelToken? cancel}); @GET('/user') Future> getUser( - {@Query("id") int? id, @Query("username") String? username}); + {@Query("id") int? id, + @Query("username") String? username, + @CancelRequest() CancelToken? cancel}); @GET('/status') - Future> getStatus(); + Future> getStatus( + {@CancelRequest() CancelToken? cancel}); @PUT('/token') @MultiPart() @@ -72,30 +76,40 @@ abstract class _EHApi { // ignore: unused_element @Part(name: "http_only") bool? httpOnly, // ignore: unused_element - @Part(name: "secure") bool? secure}); + @Part(name: "secure") bool? secure, + // ignore: unused_element + @CancelRequest() CancelToken? cancel}); @DELETE('/token') @MultiPart() - Future> deleteToken({@Part(name: "token") String? token}); + Future> deleteToken( + {@Part(name: "token") String? token, + @CancelRequest() CancelToken? cancel}); @GET('/token') Future> getToken( - {@Query("token") String? token}); + {@Query("token") String? token, @CancelRequest() CancelToken? cancel}); @GET('/file/{id}') @DioResponseType(ResponseType.bytes) - Future>> getFile(@Path("id") int id); + Future>> getFile(@Path("id") int id, + {@CancelRequest() CancelToken? cancel}); @GET('/file/{id}') // ignore: unused_element Future> _getFileData( - @Path("id") int id, @Query("data") bool data); + @Path("id") int id, @Query("data") bool data, + // ignore: unused_element + {@CancelRequest() CancelToken? cancel}); @GET('/file/random') @DioResponseType(ResponseType.bytes) Future>> getRandomFile( {@Query("is_nsfw") bool? isNsfw, @Query("is_ad") bool? isAd, - @Query("thumb") bool? thumb}); + @Query("thumb") bool? thumb, + @CancelRequest() CancelToken? cancel}); @GET('/files/{token}') // ignore: unused_element - Future> _getFiles(@Path("token") String token); + Future> _getFiles(@Path("token") String token, + // ignore: unused_element + {@CancelRequest() CancelToken? cancel}); @GET('/thumbnail/{id}') @DioResponseType(ResponseType.bytes) Future>> getThumbnail(@Path("id") int id, @@ -105,21 +119,27 @@ abstract class _EHApi { @Query("quality") int? quality, @Query("force") bool? force, @Query("method") ThumbnailMethod? method, - @Query("align") ThumbnailAlign? align}); + @Query("align") ThumbnailAlign? align, + @CancelRequest() CancelToken? cancel}); @GET('/gallery/{gid}') - Future> getGallery(@Path("gid") int gid); + Future> getGallery(@Path("gid") int gid, + {@CancelRequest() CancelToken? cancel}); @GET('/gallery/list') Future>> listGalleries( {@Query("all") bool? all, @Query("offset") int? offset, - @Query("limit") int? limit}); + @Query("limit") int? limit, + @CancelRequest() CancelToken? cancel}); @GET('/tag/{id}') // ignore: unused_element - Future> _getTags(@Path("id") String id); + Future> _getTags(@Path("id") String id, + // ignore: unused_element + {@CancelRequest() CancelToken? cancel}); @GET('/tag/rows') - Future>> getRowTags(); + Future>> getRowTags( + {@CancelRequest() CancelToken? cancel}); } class EHApi extends __EHApi { @@ -129,7 +149,8 @@ class EHApi extends __EHApi { required String password, bool? setCookie, bool? httpOnly, - bool? secure}) async { + bool? secure, + CancelToken? cancel}) async { int t = DateTime.now().millisecondsSinceEpoch; final p = await _pbkdf2a.deriveKeyFromPassword(password: password, nonce: _salt); @@ -142,18 +163,20 @@ class EHApi extends __EHApi { t: t, setCookie: setCookie, httpOnly: httpOnly, - secure: secure); + secure: secure, + cancel: cancel); } - Future> getFileData(int id) { - return _getFileData(id, true); + Future> getFileData(int id, {CancelToken? cancel}) { + return _getFileData(id, true, cancel: cancel); } - Future> getFiles(List tokens) { - return _getFiles(tokens.join(",")); + Future> getFiles(List tokens, + {CancelToken? cancel}) { + return _getFiles(tokens.join(","), cancel: cancel); } - Future> getTags(List ids) { - return _getTags(ids.join(",")); + Future> getTags(List ids, {CancelToken? cancel}) { + return _getTags(ids.join(","), cancel: cancel); } } diff --git a/lib/api/client.g.dart b/lib/api/client.g.dart index 00ec60d..d400d7f 100644 --- a/lib/api/client.g.dart +++ b/lib/api/client.g.dart @@ -24,6 +24,7 @@ class __EHApi implements _EHApi { String password, { bool? isAdmin, int? permissions, + CancelToken? cancel, }) async { const _extra = {}; final queryParameters = {}; @@ -62,6 +63,7 @@ class __EHApi implements _EHApi { '/user', queryParameters: queryParameters, data: _data, + cancelToken: cancel, ) .copyWith( baseUrl: _combineBaseUrls( @@ -79,6 +81,7 @@ class __EHApi implements _EHApi { Future> getUser({ int? id, String? username, + CancelToken? cancel, }) async { const _extra = {}; final queryParameters = { @@ -99,6 +102,7 @@ class __EHApi implements _EHApi { '/user', queryParameters: queryParameters, data: _data, + cancelToken: cancel, ) .copyWith( baseUrl: _combineBaseUrls( @@ -113,9 +117,10 @@ class __EHApi implements _EHApi { } @override - Future> getStatus() async { + Future> getStatus({CancelToken? cancel}) async { const _extra = {}; final queryParameters = {}; + queryParameters.removeWhere((k, v) => v == null); final _headers = {}; final Map? _data = null; final _result = await _dio.fetch>( @@ -129,6 +134,7 @@ class __EHApi implements _EHApi { '/status', queryParameters: queryParameters, data: _data, + cancelToken: cancel, ) .copyWith( baseUrl: _combineBaseUrls( @@ -150,6 +156,7 @@ class __EHApi implements _EHApi { bool? setCookie, bool? httpOnly, bool? secure, + CancelToken? cancel, }) async { const _extra = {}; final queryParameters = {}; @@ -198,6 +205,7 @@ class __EHApi implements _EHApi { '/token', queryParameters: queryParameters, data: _data, + cancelToken: cancel, ) .copyWith( baseUrl: _combineBaseUrls( @@ -212,7 +220,10 @@ class __EHApi implements _EHApi { } @override - Future> deleteToken({String? token}) async { + Future> deleteToken({ + String? token, + CancelToken? cancel, + }) async { const _extra = {}; final queryParameters = {}; queryParameters.removeWhere((k, v) => v == null); @@ -236,6 +247,7 @@ class __EHApi implements _EHApi { '/token', queryParameters: queryParameters, data: _data, + cancelToken: cancel, ) .copyWith( baseUrl: _combineBaseUrls( @@ -250,7 +262,10 @@ class __EHApi implements _EHApi { } @override - Future> getToken({String? token}) async { + Future> getToken({ + String? token, + CancelToken? cancel, + }) async { const _extra = {}; final queryParameters = {r'token': token}; queryParameters.removeWhere((k, v) => v == null); @@ -267,6 +282,7 @@ class __EHApi implements _EHApi { '/token', queryParameters: queryParameters, data: _data, + cancelToken: cancel, ) .copyWith( baseUrl: _combineBaseUrls( @@ -281,9 +297,13 @@ class __EHApi implements _EHApi { } @override - Future>> getFile(int id) async { + Future>> getFile( + int id, { + CancelToken? cancel, + }) async { const _extra = {}; final queryParameters = {}; + queryParameters.removeWhere((k, v) => v == null); final _headers = {}; final Map? _data = null; final _result = await _dio @@ -298,6 +318,7 @@ class __EHApi implements _EHApi { '/file/${id}', queryParameters: queryParameters, data: _data, + cancelToken: cancel, ) .copyWith( baseUrl: _combineBaseUrls( @@ -312,10 +333,12 @@ class __EHApi implements _EHApi { @override Future> _getFileData( int id, - bool data, - ) async { + bool data, { + CancelToken? cancel, + }) async { const _extra = {}; final queryParameters = {r'data': data}; + queryParameters.removeWhere((k, v) => v == null); final _headers = {}; final Map? _data = null; final _result = await _dio.fetch>( @@ -329,6 +352,7 @@ class __EHApi implements _EHApi { '/file/${id}', queryParameters: queryParameters, data: _data, + cancelToken: cancel, ) .copyWith( baseUrl: _combineBaseUrls( @@ -347,6 +371,7 @@ class __EHApi implements _EHApi { bool? isNsfw, bool? isAd, bool? thumb, + CancelToken? cancel, }) async { const _extra = {}; final queryParameters = { @@ -369,6 +394,7 @@ class __EHApi implements _EHApi { '/file/random', queryParameters: queryParameters, data: _data, + cancelToken: cancel, ) .copyWith( baseUrl: _combineBaseUrls( @@ -381,9 +407,13 @@ class __EHApi implements _EHApi { } @override - Future> _getFiles(String token) async { + Future> _getFiles( + String token, { + CancelToken? cancel, + }) async { const _extra = {}; final queryParameters = {}; + queryParameters.removeWhere((k, v) => v == null); final _headers = {}; final Map? _data = null; final _result = await _dio @@ -397,6 +427,7 @@ class __EHApi implements _EHApi { '/files/${token}', queryParameters: queryParameters, data: _data, + cancelToken: cancel, ) .copyWith( baseUrl: _combineBaseUrls( @@ -420,6 +451,7 @@ class __EHApi implements _EHApi { bool? force, ThumbnailMethod? method, ThumbnailAlign? align, + CancelToken? cancel, }) async { const _extra = {}; final queryParameters = { @@ -446,6 +478,7 @@ class __EHApi implements _EHApi { '/thumbnail/${id}', queryParameters: queryParameters, data: _data, + cancelToken: cancel, ) .copyWith( baseUrl: _combineBaseUrls( @@ -458,9 +491,13 @@ class __EHApi implements _EHApi { } @override - Future> getGallery(int gid) async { + Future> getGallery( + int gid, { + CancelToken? cancel, + }) async { const _extra = {}; final queryParameters = {}; + queryParameters.removeWhere((k, v) => v == null); final _headers = {}; final Map? _data = null; final _result = await _dio.fetch>( @@ -474,6 +511,7 @@ class __EHApi implements _EHApi { '/gallery/${gid}', queryParameters: queryParameters, data: _data, + cancelToken: cancel, ) .copyWith( baseUrl: _combineBaseUrls( @@ -492,6 +530,7 @@ class __EHApi implements _EHApi { bool? all, int? offset, int? limit, + CancelToken? cancel, }) async { const _extra = {}; final queryParameters = { @@ -513,6 +552,7 @@ class __EHApi implements _EHApi { '/gallery/list', queryParameters: queryParameters, data: _data, + cancelToken: cancel, ) .copyWith( baseUrl: _combineBaseUrls( @@ -531,9 +571,13 @@ class __EHApi implements _EHApi { } @override - Future> _getTags(String id) async { + Future> _getTags( + String id, { + CancelToken? cancel, + }) async { const _extra = {}; final queryParameters = {}; + queryParameters.removeWhere((k, v) => v == null); final _headers = {}; final Map? _data = null; final _result = await _dio @@ -547,6 +591,7 @@ class __EHApi implements _EHApi { '/tag/${id}', queryParameters: queryParameters, data: _data, + cancelToken: cancel, ) .copyWith( baseUrl: _combineBaseUrls( @@ -561,9 +606,10 @@ class __EHApi implements _EHApi { } @override - Future>> getRowTags() async { + Future>> getRowTags({CancelToken? cancel}) async { const _extra = {}; final queryParameters = {}; + queryParameters.removeWhere((k, v) => v == null); final _headers = {}; final Map? _data = null; final _result = await _dio.fetch>( @@ -577,6 +623,7 @@ class __EHApi implements _EHApi { '/tag/rows', queryParameters: queryParameters, data: _data, + cancelToken: cancel, ) .copyWith( baseUrl: _combineBaseUrls( diff --git a/lib/components/thumbnail.dart b/lib/components/thumbnail.dart index 600fa41..d987bc8 100644 --- a/lib/components/thumbnail.dart +++ b/lib/components/thumbnail.dart @@ -1,5 +1,6 @@ import 'dart:typed_data'; import 'dart:ui'; +import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:logging/logging.dart'; @@ -47,35 +48,47 @@ class _Thumbnail extends State { int? _fileId; bool _showNsfw = false; String? _uri; + CancelToken? _cancel; Future _fetchData() async { try { + _cancel = CancelToken(); _isLoading = true; if (_fileId == null) { final token = widget._pMeta.token; - _fileId = (await api.getFiles([token])).unwrap().files[token]![0]!.id; + _fileId = (await api.getFiles([token], cancel: _cancel)) + .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); + align: ThumbnailAlign.center, + cancel: _cancel); if (re.response.statusCode != 200) { throw Exception( 'Failed to get thumbnail: ${re.response.statusCode} ${re.response.statusMessage}'); } _uri = re.response.realUri.toString(); final data = Uint8List.fromList(re.data); - setState(() { - _isLoading = false; - _data = data; - }); + if (!_cancel!.isCancelled) { + setState(() { + _isLoading = false; + _data = data; + _cancel = null; + }); + } } catch (e) { - _log.warning("Failed to get file data:", e); - setState(() { - _isLoading = false; - _error = e; - }); + if (!_cancel!.isCancelled) { + _log.warning("Failed to get file data:", e); + setState(() { + _isLoading = false; + _error = e; + _cancel = null; + }); + } } } @@ -92,6 +105,12 @@ class _Thumbnail extends State { bool get showNsfw => _showNsfw || (prefs.getBool("showNsfw") ?? false); + @override + void dispose() { + _cancel?.cancel(); + super.dispose(); + } + @override Widget build(BuildContext context) { final isLoading = _data == null && _error == null; @@ -132,6 +151,20 @@ class _Thumbnail extends State { ], ) : ImageWithContextMenu(_data!, uri: _uri) - : Text("Error $_error")); + : 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)) + ]))); } } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 363c629..d39601d 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -28,5 +28,6 @@ "download": "Download", "colon": ":", "copyImage": "Copy image to clipboard", - "copyImgUrl": "Copy image URL to clipboard" + "copyImgUrl": "Copy image URL to clipboard", + "retry": "Retry" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index e95c500..6295968 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -28,5 +28,6 @@ "download": "下载", "colon": ":", "copyImage": "复制图片到剪贴板", - "copyImgUrl": "复制图片链接到剪贴板" + "copyImgUrl": "复制图片链接到剪贴板", + "retry": "重试" }