diff --git a/lib/api/client.dart b/lib/api/client.dart index fa6ec39..c61256b 100644 --- a/lib/api/client.dart +++ b/lib/api/client.dart @@ -170,6 +170,27 @@ abstract class _EHApi { @Query("max_length") int? maxLength, @Query("export_ad") bool? exportAd, @CancelRequest() CancelToken? cancel}); + + @POST('/filemeta') + @MultiPart() + Future> updateGalleryFileMeta(@Part(name: "gid") int gid, + {@Part(name: "is_nsfw") bool? isNsfw, + @Part(name: "is_ad") bool? isAd, + @Part(name: "excludes") String? excludes, + @CancelRequest() CancelToken? cancel}); + @POST('/filemeta') + @MultiPart() + Future> updateFileMeta(@Part(name: "token") String token, + {@Part(name: "is_nsfw") bool? isNsfw, + @Part(name: "is_ad") bool? isAd, + @CancelRequest() CancelToken? cancel}); + @POST('/filemeta') + @MultiPart() + Future> updateFilesMeta( + @Part(name: "tokens") String tokens, + {@Part(name: "is_nsfw") bool? isNsfw, + @Part(name: "is_ad") bool? isAd, + @CancelRequest() CancelToken? cancel}); } class EHApi extends __EHApi { diff --git a/lib/api/client.g.dart b/lib/api/client.g.dart index 4670297..0ef8cd5 100644 --- a/lib/api/client.g.dart +++ b/lib/api/client.g.dart @@ -688,6 +688,175 @@ class __EHApi implements _EHApi { return httpResponse; } + @override + Future> updateGalleryFileMeta( + int gid, { + bool? isNsfw, + bool? isAd, + String? excludes, + CancelToken? cancel, + }) async { + const _extra = {}; + final queryParameters = {}; + queryParameters.removeWhere((k, v) => v == null); + final _headers = {}; + final _data = FormData(); + _data.fields.add(MapEntry( + 'gid', + gid.toString(), + )); + if (isNsfw != null) { + _data.fields.add(MapEntry( + 'is_nsfw', + isNsfw.toString(), + )); + } + if (isAd != null) { + _data.fields.add(MapEntry( + 'is_ad', + isAd.toString(), + )); + } + if (excludes != null) { + _data.fields.add(MapEntry( + 'excludes', + excludes, + )); + } + final _result = await _dio + .fetch>(_setStreamType>(Options( + method: 'POST', + headers: _headers, + extra: _extra, + contentType: 'multipart/form-data', + ) + .compose( + _dio.options, + '/filemeta', + queryParameters: queryParameters, + data: _data, + cancelToken: cancel, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + final value = ApiResult.fromJson( + _result.data!, + (json) => json as dynamic, + ); + return value; + } + + @override + Future> updateFileMeta( + String token, { + bool? isNsfw, + bool? isAd, + CancelToken? cancel, + }) async { + const _extra = {}; + final queryParameters = {}; + queryParameters.removeWhere((k, v) => v == null); + final _headers = {}; + final _data = FormData(); + _data.fields.add(MapEntry( + 'token', + token, + )); + if (isNsfw != null) { + _data.fields.add(MapEntry( + 'is_nsfw', + isNsfw.toString(), + )); + } + if (isAd != null) { + _data.fields.add(MapEntry( + 'is_ad', + isAd.toString(), + )); + } + final _result = await _dio + .fetch>(_setStreamType>(Options( + method: 'POST', + headers: _headers, + extra: _extra, + contentType: 'multipart/form-data', + ) + .compose( + _dio.options, + '/filemeta', + queryParameters: queryParameters, + data: _data, + cancelToken: cancel, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + final value = ApiResult.fromJson( + _result.data!, + (json) => json as dynamic, + ); + return value; + } + + @override + Future> updateFilesMeta( + String tokens, { + bool? isNsfw, + bool? isAd, + CancelToken? cancel, + }) async { + const _extra = {}; + final queryParameters = {}; + queryParameters.removeWhere((k, v) => v == null); + final _headers = {}; + final _data = FormData(); + _data.fields.add(MapEntry( + 'tokens', + tokens, + )); + if (isNsfw != null) { + _data.fields.add(MapEntry( + 'is_nsfw', + isNsfw.toString(), + )); + } + if (isAd != null) { + _data.fields.add(MapEntry( + 'is_ad', + isAd.toString(), + )); + } + final _result = await _dio + .fetch>(_setStreamType>(Options( + method: 'POST', + headers: _headers, + extra: _extra, + contentType: 'multipart/form-data', + ) + .compose( + _dio.options, + '/filemeta', + queryParameters: queryParameters, + data: _data, + cancelToken: cancel, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + final value = ApiResult.fromJson( + _result.data!, + (json) => json as dynamic, + ); + return value; + } + RequestOptions _setStreamType(RequestOptions requestOptions) { if (T != dynamic && !(requestOptions.responseType == ResponseType.bytes || diff --git a/lib/api/gallery.dart b/lib/api/gallery.dart index 3d58312..07c9d97 100644 --- a/lib/api/gallery.dart +++ b/lib/api/gallery.dart @@ -162,6 +162,7 @@ class GalleryData { final GMeta meta; final List tags; final List pages; + bool get isAllNsfw => pages.every((page) => page.isNsfw); factory GalleryData.fromJson(Map json) => _$GalleryDataFromJson(json); diff --git a/lib/gallery.dart b/lib/gallery.dart index 7f05829..7f310a4 100644 --- a/lib/gallery.dart +++ b/lib/gallery.dart @@ -27,6 +27,12 @@ class GalleryPage extends StatefulWidget { @override State createState() => _GalleryPage(); + // ignore: library_private_types_in_public_api + static _GalleryPage of(BuildContext context) => + context.findAncestorStateOfType<_GalleryPage>()!; + // ignore: library_private_types_in_public_api + static _GalleryPage? maybeOf(BuildContext context) => + context.findAncestorStateOfType<_GalleryPage>(); } class _GalleryPage extends State @@ -37,7 +43,24 @@ class _GalleryPage extends State EhFiles? _files; Object? _error; CancelToken? _cancel; + CancelToken? _markAsNsfwCancel; bool _isLoading = false; + bool? get isAllNsfw => _data?.isAllNsfw; + Future markGalleryAsNsfw(bool isNsfw) async { + try { + _markAsNsfwCancel = CancelToken(); + (await api.updateGalleryFileMeta(_gid, + isNsfw: isNsfw, cancel: _markAsNsfwCancel)) + .unwrap(); + if (!_cancel!.isCancelled) { + _fetchData(); + } + } catch (e) { + if (!_cancel!.isCancelled) { + _log.severe("Failed to mark gallery $_gid:", e); + } + } + } Future _fetchData() async { try { @@ -124,6 +147,7 @@ class _GalleryPage extends State @override void dispose() { _cancel?.cancel(); + _markAsNsfwCancel?.cancel(); super.dispose(); } } diff --git a/lib/globals.dart b/lib/globals.dart index e105345..e03d6bf 100644 --- a/lib/globals.dart +++ b/lib/globals.dart @@ -20,6 +20,7 @@ import 'auth.dart'; import 'config/base.dart'; import 'config/shared_preferences.dart'; import 'config/windows.dart'; +import 'gallery.dart'; import 'main.dart'; import 'platform/clipboard.dart'; import 'platform/display.dart'; @@ -133,6 +134,8 @@ enum MoreVertSettings { setServerUrl, createRootUser, settings, + markAsNsfw, + markAsNonNsfw, } void onMoreVertSettingsSelected(BuildContext context, MoreVertSettings value) { @@ -146,6 +149,12 @@ void onMoreVertSettingsSelected(BuildContext context, MoreVertSettings value) { case MoreVertSettings.settings: context.push("/settings"); break; + case MoreVertSettings.markAsNsfw: + GalleryPage.maybeOf(context)?.markGalleryAsNsfw(true); + break; + case MoreVertSettings.markAsNonNsfw: + GalleryPage.maybeOf(context)?.markGalleryAsNsfw(false); + break; default: break; } @@ -183,11 +192,7 @@ List> buildMoreVertSettings( onChanged: (value) { if (value != null) { prefs.setBool("showNsfw", value); - try { - listener.emit("showNsfwChanged", null); - } catch (e) { - // Do nothing. - } + listener.tryEmit("showNsfwChanged", null); setState(() { showNsfw = value; }); @@ -205,11 +210,7 @@ List> buildMoreVertSettings( onChanged: (value) { if (value != null) { prefs.setBool("displayAd", value); - try { - listener.emit("displayAdChanged", null); - } catch (e) { - // Do nothing. - } + listener.tryEmit("displayAdChanged", null); setState(() { displayAd = value; }); @@ -218,6 +219,20 @@ List> buildMoreVertSettings( title: Text(AppLocalizations.of(context)!.displayAd), ), ))); + if (path == "/gallery/:gid") { + list.add(const PopupMenuDivider()); + final isAllNsfw = GalleryPage.of(context).isAllNsfw; + if (isAllNsfw != null) { + list.add(PopupMenuItem( + value: isAllNsfw + ? MoreVertSettings.markAsNonNsfw + : MoreVertSettings.markAsNsfw, + child: Text(isAllNsfw + ? AppLocalizations.of(context)!.markAsNonNsfw + : AppLocalizations.of(context)!.markAsNsfw), + )); + } + } return list; } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 8325d52..d25800e 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -87,5 +87,7 @@ "downloadZipFailed": "Failed to download ZIP file.", "rating": "Rating", "preventScreenCapture": "Prevent screen capture", - "seeMoreInfo": "See more information" + "seeMoreInfo": "See more information", + "markAsNsfw": "Mark as NSFW", + "markAsNonNsfw": "Mark as non-NSFW" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index ea47e31..461d0bf 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -87,5 +87,7 @@ "downloadZipFailed": "Zip文件下载失败。", "rating": "评分", "preventScreenCapture": "防止截屏", - "seeMoreInfo": "显示更多信息" + "seeMoreInfo": "显示更多信息", + "markAsNsfw": "标记为NSFW", + "markAsNonNsfw": "标记为非NSFW" }