diff --git a/lib/api/gallery.dart b/lib/api/gallery.dart index 07c9d97..4fbbb91 100644 --- a/lib/api/gallery.dart +++ b/lib/api/gallery.dart @@ -126,7 +126,7 @@ class Tag { @JsonSerializable() class ExtendedPMeta { - const ExtendedPMeta({ + ExtendedPMeta({ required this.gid, required this.index, required this.token, @@ -143,9 +143,9 @@ class ExtendedPMeta { final int width; final int height; @JsonKey(name: 'is_nsfw') - final bool isNsfw; + bool isNsfw; @JsonKey(name: 'is_ad') - final bool isAd; + bool isAd; factory ExtendedPMeta.fromJson(Map json) => _$ExtendedPMetaFromJson(json); diff --git a/lib/components/gallery_info.dart b/lib/components/gallery_info.dart index ba17042..a424431 100644 --- a/lib/components/gallery_info.dart +++ b/lib/components/gallery_info.dart @@ -24,10 +24,29 @@ class _GalleryInfo extends State with ThemeModeWidget { setState(() {}); } + void _onAdChanged(dynamic args) { + final arguments = args as (String, bool)?; + if (arguments == null) return; + final token = arguments.$1; + final isAd = arguments.$2; + var changed = false; + for (var e in widget.gData.pages) { + if (e.token == token) { + e.isAd = isAd; + changed = true; + break; + } + } + if (changed) { + setState(() {}); + } + } + @override void initState() { listener.on("showNsfwChanged", stateChanged); listener.on("displayAdChanged", stateChanged); + listener.on("adChanged", _onAdChanged); super.initState(); } @@ -100,6 +119,7 @@ class _GalleryInfo extends State with ThemeModeWidget { controller.dispose(); listener.removeEventListener("showNsfwChanged", stateChanged); listener.removeEventListener("displayAdChanged", stateChanged); + listener.removeEventListener("adChanged", _onAdChanged); super.dispose(); } } diff --git a/lib/components/image.dart b/lib/components/image.dart index 80387bb..43bc8d4 100644 --- a/lib/components/image.dart +++ b/lib/components/image.dart @@ -10,13 +10,25 @@ final _log = Logger("ImageWithContextMenu"); class ImageWithContextMenu extends StatelessWidget { const ImageWithContextMenu(this.data, - {Key? key, this.uri, this.dir, this.fileName, this.fmt = ImageFmt.jpg}) + {Key? key, + this.uri, + this.dir, + this.fileName, + this.fmt = ImageFmt.jpg, + this.isNsfw, + this.changeNsfw, + this.isAd, + this.changeAd}) : super(key: key); final Uint8List data; final String? uri; final ImageFmt fmt; final String? fileName; final String? dir; + final bool Function()? isNsfw; + final Function(bool isNsfw)? changeNsfw; + final bool Function()? isAd; + final Function(bool isAd)? changeAd; @override Widget build(BuildContext context) { return ContextMenuWidget( @@ -53,6 +65,26 @@ class ImageWithContextMenu extends StatelessWidget { } })); } + if (isNsfw != null && changeNsfw != null) { + final nsfw = isNsfw!(); + list.add(MenuAction( + title: nsfw + ? AppLocalizations.of(context)!.markAsSfw + : AppLocalizations.of(context)!.markAsNsfw, + callback: () { + changeNsfw!(!isNsfw!()); + })); + } + if (isAd != null && changeAd != null) { + final ad = isAd!(); + list.add(MenuAction( + title: ad + ? AppLocalizations.of(context)!.markAsNonAd + : AppLocalizations.of(context)!.markAsAd, + callback: () { + changeAd!(!isAd!()); + })); + } return Menu(children: list); }, child: Image.memory(data)); diff --git a/lib/components/thumbnail.dart b/lib/components/thumbnail.dart index ec9f595..063df82 100644 --- a/lib/components/thumbnail.dart +++ b/lib/components/thumbnail.dart @@ -64,6 +64,10 @@ enum _ThumbnailMenu { copyImage, copyImgUrl, saveAs, + markAsNsfw, + markAsSfw, + markAsAd, + markAsNonAd, } class _Thumbnail extends State { @@ -74,11 +78,52 @@ class _Thumbnail extends State { bool _showNsfw = false; String? _uri; CancelToken? _cancel; + CancelToken? _markAsNsfwCancel; + CancelToken? _markAsAdCancel; String? _fileName; String _dir = ""; Color? _iconColor; double? _iconSize; bool _disposed = false; + void _onNsfwChanged(dynamic args) { + final arguments = args as (String, bool)?; + if (arguments == null) return; + final token = arguments.$1; + final isNsfw = arguments.$2; + if (token != widget._pMeta.token) return; + widget._pMeta.isNsfw = isNsfw; + setState(() {}); + } + + Future _markAsNsfw(bool isNsfw) async { + try { + _markAsNsfwCancel = CancelToken(); + final token = widget._pMeta.token; + (await api.updateFileMeta(token, + isNsfw: isNsfw, cancel: _markAsNsfwCancel)) + .unwrap(); + listener.tryEmit("nsfwChanged", (token, isNsfw)); + } catch (e) { + if (!_markAsNsfwCancel!.isCancelled) { + _log.warning("Failed to mark as nsfw:", e); + } + } + } + + Future _markAsAd(bool isAd) async { + try { + _markAsAdCancel = CancelToken(); + final token = widget._pMeta.token; + (await api.updateFileMeta(token, isAd: isAd, cancel: _markAsAdCancel)) + .unwrap(); + listener.tryEmit("adChanged", (token, isAd)); + } catch (e) { + if (!_markAsAdCancel!.isCancelled) { + _log.warning("Failed to mark as ad:", e); + } + } + } + Future _fetchData() async { try { _cancel = CancelToken(); @@ -133,6 +178,7 @@ class _Thumbnail extends State { _uri = null; _fileName = "${basenameWithoutExtension(widget._pMeta.name)}_thumb"; _dir = isAndroid && widget.gid != null ? widget.gid!.toString() : ""; + listener.on("nsfwChanged", _onNsfwChanged); super.initState(); } @@ -174,6 +220,9 @@ class _Thumbnail extends State { void dispose() { _disposed = true; _cancel?.cancel(); + _markAsNsfwCancel?.cancel(); + _markAsAdCancel?.cancel(); + listener.removeEventListener("nsfwChanged", _onNsfwChanged); super.dispose(); } @@ -201,6 +250,18 @@ class _Thumbnail extends State { _log.warning("Failed to save image:", err); } break; + case _ThumbnailMenu.markAsNsfw: + await _markAsNsfw(true); + break; + case _ThumbnailMenu.markAsSfw: + await _markAsNsfw(false); + break; + case _ThumbnailMenu.markAsAd: + await _markAsAd(true); + break; + case _ThumbnailMenu.markAsNonAd: + await _markAsAd(false); + break; } } @@ -208,6 +269,7 @@ class _Thumbnail extends State { Widget build(BuildContext context) { final isLoading = _data == null && _error == null; final isNsfw = widget._pMeta.isNsfw; + final isAd = widget._pMeta.isAd; if (isLoading && !_isLoading) _fetchData(); _iconSize ??= Theme.of(context).iconTheme.size; final iconSize = MediaQuery.of(context).size.width < 400 @@ -234,12 +296,37 @@ class _Thumbnail extends State { PopupMenuItem( value: _ThumbnailMenu.saveAs, child: Text(AppLocalizations.of(context)!.saveAs)), + const PopupMenuDivider(), + PopupMenuItem( + value: isNsfw + ? _ThumbnailMenu.markAsSfw + : _ThumbnailMenu.markAsNsfw, + child: Text(isNsfw + ? AppLocalizations.of(context)!.markAsSfw + : AppLocalizations.of(context)!.markAsNsfw)), + PopupMenuItem( + value: isAd + ? _ThumbnailMenu.markAsNonAd + : _ThumbnailMenu.markAsAd, + child: Text(isAd + ? AppLocalizations.of(context)!.markAsNonAd + : AppLocalizations.of(context)!.markAsAd)), ]; return list; })); final timg = _data != null ? ImageWithContextMenu(_data!, - uri: _uri, fileName: _fileName, dir: _dir) + uri: _uri, + fileName: _fileName, + dir: _dir, + isNsfw: () => widget._pMeta.isNsfw, + changeNsfw: (isNsfw) { + _markAsNsfw(isNsfw); + }, + isAd: () => widget._pMeta.isAd, + changeAd: (isAd) { + _markAsAd(isAd); + }) : null; final img = widget.gid != null && widget.index != null && _data != null ? GestureDetector( diff --git a/lib/components/thumbnail_gridview.dart b/lib/components/thumbnail_gridview.dart index 6a2d53d..0893f0f 100644 --- a/lib/components/thumbnail_gridview.dart +++ b/lib/components/thumbnail_gridview.dart @@ -25,9 +25,11 @@ class ThumbnailGridView extends StatelessWidget { final page = npages[index]!; final fileId = files != null ? files!.files[page.token]!.first.id : null; + final key = Key("thumbnail$gid-${page.index}"); return Container( padding: const EdgeInsets.all(4), child: Thumbnail(page, + key: key, fileId: fileId, gid: gid, index: index + 1, diff --git a/lib/gallery.dart b/lib/gallery.dart index 7f310a4..9e98e7a 100644 --- a/lib/gallery.dart +++ b/lib/gallery.dart @@ -52,12 +52,12 @@ class _GalleryPage extends State (await api.updateGalleryFileMeta(_gid, isNsfw: isNsfw, cancel: _markAsNsfwCancel)) .unwrap(); - if (!_cancel!.isCancelled) { + if (!_markAsNsfwCancel!.isCancelled) { _fetchData(); } } catch (e) { - if (!_cancel!.isCancelled) { - _log.severe("Failed to mark gallery $_gid:", e); + if (!_markAsNsfwCancel!.isCancelled) { + _log.warning("Failed to mark gallery $_gid:", e); } } } diff --git a/lib/globals.dart b/lib/globals.dart index e03d6bf..3cd5124 100644 --- a/lib/globals.dart +++ b/lib/globals.dart @@ -128,14 +128,14 @@ final Path platformPath = Path(); final TagsInfo tags = TagsInfo(); final GlobalKey rootScaffoldMessengerKey = GlobalKey(); -final EventListener listener = EventListener(); +final EventListener listener = EventListener()..maxListeners = 0; enum MoreVertSettings { setServerUrl, createRootUser, settings, markAsNsfw, - markAsNonNsfw, + markAsSfw, } void onMoreVertSettingsSelected(BuildContext context, MoreVertSettings value) { @@ -152,7 +152,7 @@ void onMoreVertSettingsSelected(BuildContext context, MoreVertSettings value) { case MoreVertSettings.markAsNsfw: GalleryPage.maybeOf(context)?.markGalleryAsNsfw(true); break; - case MoreVertSettings.markAsNonNsfw: + case MoreVertSettings.markAsSfw: GalleryPage.maybeOf(context)?.markGalleryAsNsfw(false); break; default: @@ -225,10 +225,10 @@ List> buildMoreVertSettings( if (isAllNsfw != null) { list.add(PopupMenuItem( value: isAllNsfw - ? MoreVertSettings.markAsNonNsfw + ? MoreVertSettings.markAsSfw : MoreVertSettings.markAsNsfw, child: Text(isAllNsfw - ? AppLocalizations.of(context)!.markAsNonNsfw + ? AppLocalizations.of(context)!.markAsSfw : AppLocalizations.of(context)!.markAsNsfw), )); } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index d25800e..d126b63 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -89,5 +89,7 @@ "preventScreenCapture": "Prevent screen capture", "seeMoreInfo": "See more information", "markAsNsfw": "Mark as NSFW", - "markAsNonNsfw": "Mark as non-NSFW" + "markAsSfw": "Mark as SFW", + "markAsAd": "Mark as Ad", + "markAsNonAd": "Mark as non-Ad" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 461d0bf..e65416e 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -89,5 +89,7 @@ "preventScreenCapture": "防止截屏", "seeMoreInfo": "显示更多信息", "markAsNsfw": "标记为NSFW", - "markAsNonNsfw": "标记为非NSFW" + "markAsSfw": "标记为SFW", + "markAsAd": "标记为广告", + "markAsNonAd": "标记为非广告" }