mirror of
https://github.com/lifegpc/eh_downloader_flutter.git
synced 2026-07-08 01:30:28 +08:00
Fix setState called after dispose
This commit is contained in:
@@ -52,13 +52,17 @@ abstract class _EHApi {
|
||||
Future<ApiResult<int>> 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<ApiResult<BUser>> getUser(
|
||||
{@Query("id") int? id, @Query("username") String? username});
|
||||
{@Query("id") int? id,
|
||||
@Query("username") String? username,
|
||||
@CancelRequest() CancelToken? cancel});
|
||||
|
||||
@GET('/status')
|
||||
Future<ApiResult<ServerStatus>> getStatus();
|
||||
Future<ApiResult<ServerStatus>> 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<ApiResult<bool>> deleteToken({@Part(name: "token") String? token});
|
||||
Future<ApiResult<bool>> deleteToken(
|
||||
{@Part(name: "token") String? token,
|
||||
@CancelRequest() CancelToken? cancel});
|
||||
@GET('/token')
|
||||
Future<ApiResult<TokenWithUserInfo>> getToken(
|
||||
{@Query("token") String? token});
|
||||
{@Query("token") String? token, @CancelRequest() CancelToken? cancel});
|
||||
|
||||
@GET('/file/{id}')
|
||||
@DioResponseType(ResponseType.bytes)
|
||||
Future<HttpResponse<List<int>>> getFile(@Path("id") int id);
|
||||
Future<HttpResponse<List<int>>> getFile(@Path("id") int id,
|
||||
{@CancelRequest() CancelToken? cancel});
|
||||
@GET('/file/{id}')
|
||||
// ignore: unused_element
|
||||
Future<ApiResult<EhFileExtend>> _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<HttpResponse<List<int>>> 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<ApiResult<EhFiles>> _getFiles(@Path("token") String token);
|
||||
Future<ApiResult<EhFiles>> _getFiles(@Path("token") String token,
|
||||
// ignore: unused_element
|
||||
{@CancelRequest() CancelToken? cancel});
|
||||
@GET('/thumbnail/{id}')
|
||||
@DioResponseType(ResponseType.bytes)
|
||||
Future<HttpResponse<List<int>>> 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<ApiResult<GalleryData>> getGallery(@Path("gid") int gid);
|
||||
Future<ApiResult<GalleryData>> getGallery(@Path("gid") int gid,
|
||||
{@CancelRequest() CancelToken? cancel});
|
||||
@GET('/gallery/list')
|
||||
Future<ApiResult<List<GMeta>>> 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<ApiResult<Tags>> _getTags(@Path("id") String id);
|
||||
Future<ApiResult<Tags>> _getTags(@Path("id") String id,
|
||||
// ignore: unused_element
|
||||
{@CancelRequest() CancelToken? cancel});
|
||||
@GET('/tag/rows')
|
||||
Future<ApiResult<List<Tag>>> getRowTags();
|
||||
Future<ApiResult<List<Tag>>> 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<ApiResult<EhFileExtend>> getFileData(int id) {
|
||||
return _getFileData(id, true);
|
||||
Future<ApiResult<EhFileExtend>> getFileData(int id, {CancelToken? cancel}) {
|
||||
return _getFileData(id, true, cancel: cancel);
|
||||
}
|
||||
|
||||
Future<ApiResult<EhFiles>> getFiles(List<String> tokens) {
|
||||
return _getFiles(tokens.join(","));
|
||||
Future<ApiResult<EhFiles>> getFiles(List<String> tokens,
|
||||
{CancelToken? cancel}) {
|
||||
return _getFiles(tokens.join(","), cancel: cancel);
|
||||
}
|
||||
|
||||
Future<ApiResult<Tags>> getTags(List<int> ids) {
|
||||
return _getTags(ids.join(","));
|
||||
Future<ApiResult<Tags>> getTags(List<int> ids, {CancelToken? cancel}) {
|
||||
return _getTags(ids.join(","), cancel: cancel);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ class __EHApi implements _EHApi {
|
||||
String password, {
|
||||
bool? isAdmin,
|
||||
int? permissions,
|
||||
CancelToken? cancel,
|
||||
}) async {
|
||||
const _extra = <String, dynamic>{};
|
||||
final queryParameters = <String, dynamic>{};
|
||||
@@ -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<ApiResult<BUser>> getUser({
|
||||
int? id,
|
||||
String? username,
|
||||
CancelToken? cancel,
|
||||
}) async {
|
||||
const _extra = <String, dynamic>{};
|
||||
final queryParameters = <String, dynamic>{
|
||||
@@ -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<ApiResult<ServerStatus>> getStatus() async {
|
||||
Future<ApiResult<ServerStatus>> getStatus({CancelToken? cancel}) async {
|
||||
const _extra = <String, dynamic>{};
|
||||
final queryParameters = <String, dynamic>{};
|
||||
queryParameters.removeWhere((k, v) => v == null);
|
||||
final _headers = <String, dynamic>{};
|
||||
final Map<String, dynamic>? _data = null;
|
||||
final _result = await _dio.fetch<Map<String, dynamic>>(
|
||||
@@ -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 = <String, dynamic>{};
|
||||
final queryParameters = <String, dynamic>{};
|
||||
@@ -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<ApiResult<bool>> deleteToken({String? token}) async {
|
||||
Future<ApiResult<bool>> deleteToken({
|
||||
String? token,
|
||||
CancelToken? cancel,
|
||||
}) async {
|
||||
const _extra = <String, dynamic>{};
|
||||
final queryParameters = <String, dynamic>{};
|
||||
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<ApiResult<TokenWithUserInfo>> getToken({String? token}) async {
|
||||
Future<ApiResult<TokenWithUserInfo>> getToken({
|
||||
String? token,
|
||||
CancelToken? cancel,
|
||||
}) async {
|
||||
const _extra = <String, dynamic>{};
|
||||
final queryParameters = <String, dynamic>{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<HttpResponse<List<int>>> getFile(int id) async {
|
||||
Future<HttpResponse<List<int>>> getFile(
|
||||
int id, {
|
||||
CancelToken? cancel,
|
||||
}) async {
|
||||
const _extra = <String, dynamic>{};
|
||||
final queryParameters = <String, dynamic>{};
|
||||
queryParameters.removeWhere((k, v) => v == null);
|
||||
final _headers = <String, dynamic>{};
|
||||
final Map<String, dynamic>? _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<ApiResult<EhFileExtend>> _getFileData(
|
||||
int id,
|
||||
bool data,
|
||||
) async {
|
||||
bool data, {
|
||||
CancelToken? cancel,
|
||||
}) async {
|
||||
const _extra = <String, dynamic>{};
|
||||
final queryParameters = <String, dynamic>{r'data': data};
|
||||
queryParameters.removeWhere((k, v) => v == null);
|
||||
final _headers = <String, dynamic>{};
|
||||
final Map<String, dynamic>? _data = null;
|
||||
final _result = await _dio.fetch<Map<String, dynamic>>(
|
||||
@@ -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 = <String, dynamic>{};
|
||||
final queryParameters = <String, dynamic>{
|
||||
@@ -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<ApiResult<EhFiles>> _getFiles(String token) async {
|
||||
Future<ApiResult<EhFiles>> _getFiles(
|
||||
String token, {
|
||||
CancelToken? cancel,
|
||||
}) async {
|
||||
const _extra = <String, dynamic>{};
|
||||
final queryParameters = <String, dynamic>{};
|
||||
queryParameters.removeWhere((k, v) => v == null);
|
||||
final _headers = <String, dynamic>{};
|
||||
final Map<String, dynamic>? _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 = <String, dynamic>{};
|
||||
final queryParameters = <String, dynamic>{
|
||||
@@ -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<ApiResult<GalleryData>> getGallery(int gid) async {
|
||||
Future<ApiResult<GalleryData>> getGallery(
|
||||
int gid, {
|
||||
CancelToken? cancel,
|
||||
}) async {
|
||||
const _extra = <String, dynamic>{};
|
||||
final queryParameters = <String, dynamic>{};
|
||||
queryParameters.removeWhere((k, v) => v == null);
|
||||
final _headers = <String, dynamic>{};
|
||||
final Map<String, dynamic>? _data = null;
|
||||
final _result = await _dio.fetch<Map<String, dynamic>>(
|
||||
@@ -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 = <String, dynamic>{};
|
||||
final queryParameters = <String, dynamic>{
|
||||
@@ -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<ApiResult<Tags>> _getTags(String id) async {
|
||||
Future<ApiResult<Tags>> _getTags(
|
||||
String id, {
|
||||
CancelToken? cancel,
|
||||
}) async {
|
||||
const _extra = <String, dynamic>{};
|
||||
final queryParameters = <String, dynamic>{};
|
||||
queryParameters.removeWhere((k, v) => v == null);
|
||||
final _headers = <String, dynamic>{};
|
||||
final Map<String, dynamic>? _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<ApiResult<List<Tag>>> getRowTags() async {
|
||||
Future<ApiResult<List<Tag>>> getRowTags({CancelToken? cancel}) async {
|
||||
const _extra = <String, dynamic>{};
|
||||
final queryParameters = <String, dynamic>{};
|
||||
queryParameters.removeWhere((k, v) => v == null);
|
||||
final _headers = <String, dynamic>{};
|
||||
final Map<String, dynamic>? _data = null;
|
||||
final _result = await _dio.fetch<Map<String, dynamic>>(
|
||||
@@ -577,6 +623,7 @@ class __EHApi implements _EHApi {
|
||||
'/tag/rows',
|
||||
queryParameters: queryParameters,
|
||||
data: _data,
|
||||
cancelToken: cancel,
|
||||
)
|
||||
.copyWith(
|
||||
baseUrl: _combineBaseUrls(
|
||||
|
||||
@@ -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<Thumbnail> {
|
||||
int? _fileId;
|
||||
bool _showNsfw = false;
|
||||
String? _uri;
|
||||
CancelToken? _cancel;
|
||||
Future<void> _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<Thumbnail> {
|
||||
|
||||
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<Thumbnail> {
|
||||
],
|
||||
)
|
||||
: 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))
|
||||
])));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -28,5 +28,6 @@
|
||||
"download": "下载",
|
||||
"colon": ":",
|
||||
"copyImage": "复制图片到剪贴板",
|
||||
"copyImgUrl": "复制图片链接到剪贴板"
|
||||
"copyImgUrl": "复制图片链接到剪贴板",
|
||||
"retry": "重试"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user