diff --git a/lib/components/gallery_info.dart b/lib/components/gallery_info.dart index 0f56c4c..53962e3 100644 --- a/lib/components/gallery_info.dart +++ b/lib/components/gallery_info.dart @@ -12,11 +12,20 @@ import 'thumbnail_gridview.dart'; class GalleryInfo extends StatefulWidget { const GalleryInfo(this.gData, - {super.key, this.files, this.refreshIndicatorKey, this.onRefresh}); + {super.key, + this.files, + this.refreshIndicatorKey, + this.onRefresh, + this.isSelectMode = false, + this.onSelectChanged, + this.selected}); final GalleryData gData; final EhFiles? files; final GlobalKey? refreshIndicatorKey; final Future Function()? onRefresh; + final bool isSelectMode; + final ValueChanged? onSelectChanged; + final List? selected; @override State createState() => _GalleryInfo(); @@ -66,14 +75,29 @@ class _GalleryInfo extends State with ThemeModeWidget { slivers: [ SliverAppBar( leading: IconButton( - icon: const Icon(Icons.arrow_back), + icon: Icon(widget.isSelectMode ? Icons.close : Icons.arrow_back), onPressed: () { - context.canPop() ? context.pop() : context.go("/"); + if (widget.isSelectMode) { + if (widget.onSelectChanged != null) { + widget.onSelectChanged!(false); + } + } else { + context.canPop() ? context.pop() : context.go("/"); + } }, ), title: SelectableText(widget.gData.meta.preferredTitle, maxLines: 1, minLines: 1), actions: [ + widget.isSelectMode || widget.onSelectChanged == null + ? Container() + : IconButton( + onPressed: () { + if (widget.onSelectChanged != null) { + widget.onSelectChanged!(true); + } + }, + icon: const Icon(Icons.check_box)), buildThemeModeIcon(context), buildMoreVertSettingsButon(context), ], @@ -113,7 +137,10 @@ class _GalleryInfo extends State with ThemeModeWidget { SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: useMobile ? 2 : 5), files: widget.files, - gid: widget.gData.meta.gid), + gid: widget.gData.meta.gid, + isSelectMode: widget.isSelectMode, + selected: widget.selected, + onSelectedChange: () => setState(() {})), ], ); if (widget.refreshIndicatorKey != null && widget.onRefresh != null) { diff --git a/lib/components/thumbnail.dart b/lib/components/thumbnail.dart index c968cbd..5190923 100644 --- a/lib/components/thumbnail.dart +++ b/lib/components/thumbnail.dart @@ -28,7 +28,10 @@ class Thumbnail extends StatefulWidget { this.gid, this.index, this.files, - this.gdata}) + this.gdata, + this.isSelectMode = false, + this.isSelected = false, + this.onSelectedChange}) : _pMeta = pMeta, _max = max ?? 1200, _width = width, @@ -43,6 +46,9 @@ class Thumbnail extends StatefulWidget { final int? index; final EhFiles? files; final GalleryData? gdata; + final bool isSelectMode; + final bool isSelected; + final ValueChanged? onSelectedChange; int get height => _height != null ? _height! @@ -246,7 +252,8 @@ class _Thumbnail extends State { } } - bool get showNsfw => _showNsfw || (prefs.getBool("showNsfw") ?? false); + bool get showNsfw => + widget.isSelectMode || _showNsfw || (prefs.getBool("showNsfw") ?? false); @override void dispose() { @@ -391,6 +398,7 @@ class _Thumbnail extends State { }, child: timg) : timg; + final cs = Theme.of(context).colorScheme; return SizedBox( width: widget.width.toDouble(), height: widget.height.toDouble(), @@ -431,7 +439,21 @@ class _Thumbnail extends State { width: widget.width.toDouble(), height: widget.height.toDouble(), child: img), - moreVertMenu + widget.isSelectMode ? Container() : moreVertMenu, + Visibility( + visible: widget.isSelectMode, + child: const ModalBarrier()), + widget.isSelectMode + ? Center( + child: Checkbox( + value: widget.isSelected, + onChanged: (v) { + if (widget.onSelectedChange != null && + v != null) { + widget.onSelectedChange!(v); + } + })) + : Container(), ]) : Center( child: Column( diff --git a/lib/components/thumbnail_gridview.dart b/lib/components/thumbnail_gridview.dart index cc6954c..92dba6c 100644 --- a/lib/components/thumbnail_gridview.dart +++ b/lib/components/thumbnail_gridview.dart @@ -6,11 +6,19 @@ import 'thumbnail.dart'; class ThumbnailGridView extends StatelessWidget { const ThumbnailGridView(this.gdata, this.gridDelegate, - {super.key, this.files, this.gid}); + {super.key, + this.files, + this.gid, + this.isSelectMode = false, + this.selected, + this.onSelectedChange}); final GalleryData gdata; final int? gid; final EhFiles? files; final SliverGridDelegate gridDelegate; + final bool isSelectMode; + final List? selected; + final Function? onSelectedChange; @override Widget build(BuildContext context) { @@ -27,13 +35,27 @@ class ThumbnailGridView extends StatelessWidget { final key = Key("thumbnail$gid-${page.index}-$fileId"); return Container( padding: const EdgeInsets.all(4), - child: Thumbnail(page, - key: key, - fileId: fileId, - gid: gid, - index: page.index, - files: files, - gdata: gdata)); + child: Thumbnail( + page, + key: key, + fileId: fileId, + gid: gid, + index: page.index, + files: files, + gdata: gdata, + isSelectMode: isSelectMode, + isSelected: selected?.contains(page.token) ?? false, + onSelectedChange: (v) { + if (v) { + selected?.add(page.token); + } else { + selected?.remove(page.token); + } + if (onSelectedChange != null) { + onSelectedChange!(); + } + }, + )); }); } } diff --git a/lib/globals.dart b/lib/globals.dart index bf4ce81..30a6566 100644 --- a/lib/globals.dart +++ b/lib/globals.dart @@ -143,6 +143,8 @@ enum MoreVertSettings { markAsNsfw, markAsSfw, taskManager, + markAsAd, + markAsNonAd, } void onMoreVertSettingsSelected(BuildContext context, MoreVertSettings value) { @@ -162,6 +164,12 @@ void onMoreVertSettingsSelected(BuildContext context, MoreVertSettings value) { case MoreVertSettings.taskManager: context.push("/task_manager"); break; + case MoreVertSettings.markAsAd: + GalleryPage.maybeOf(context)?.markAsAd(true); + break; + case MoreVertSettings.markAsNonAd: + GalleryPage.maybeOf(context)?.markAsAd(false); + break; default: break; } @@ -171,24 +179,23 @@ List> buildMoreVertSettings( BuildContext context) { var list = >[]; var path = GoRouterState.of(context).path; + final i18n = AppLocalizations.of(context)!; if (auth.status != null && auth.status!.noUser && prefs.getBool("skipCreateRootUser") == true && path != "/create_root_user") { list.add(PopupMenuItem( value: MoreVertSettings.createRootUser, - child: Text(AppLocalizations.of(context)!.createRootUser))); + child: Text(i18n.createRootUser))); } if (path == null || (path != "/settings" && !path!.startsWith("/settings/"))) { list.add(PopupMenuItem( - value: MoreVertSettings.settings, - child: Text(AppLocalizations.of(context)!.settings))); + value: MoreVertSettings.settings, child: Text(i18n.settings))); } if (path != "/task_manager" && auth.canManageTasks == true) { list.add(PopupMenuItem( - value: MoreVertSettings.taskManager, - child: Text(AppLocalizations.of(context)!.taskManager))); + value: MoreVertSettings.taskManager, child: Text(i18n.taskManager))); } var showNsfw = prefs.getBool("showNsfw") ?? false; list.add(PopupMenuItem( @@ -206,7 +213,7 @@ List> buildMoreVertSettings( }); } }, - title: Text(AppLocalizations.of(context)!.showNsfw), + title: Text(i18n.showNsfw), ), ))); var displayAd = prefs.getBool("displayAd") ?? false; @@ -225,22 +232,30 @@ List> buildMoreVertSettings( }); } }, - title: Text(AppLocalizations.of(context)!.displayAd), + title: Text(i18n.displayAd), ), ))); if (path == "/gallery/:gid" && auth.canEditGallery == true) { list.add(const PopupMenuDivider()); - final isAllNsfw = GalleryPage.of(context).isAllNsfw; - if (isAllNsfw != null) { + final gp = GalleryPage.of(context); + if (!gp.isSelectMode && gp.isAllNsfw != null) { list.add(PopupMenuItem( - value: isAllNsfw + value: gp.isAllNsfw! ? MoreVertSettings.markAsSfw : MoreVertSettings.markAsNsfw, - child: Text(isAllNsfw - ? AppLocalizations.of(context)!.markAsSfw - : AppLocalizations.of(context)!.markAsNsfw), + child: Text(gp.isAllNsfw! ? i18n.markAsSfw : i18n.markAsNsfw), )); } + if (gp.isSelectMode) { + list.add(PopupMenuItem( + value: MoreVertSettings.markAsNsfw, child: Text(i18n.markAsNsfw))); + list.add(PopupMenuItem( + value: MoreVertSettings.markAsSfw, child: Text(i18n.markAsSfw))); + list.add(PopupMenuItem( + value: MoreVertSettings.markAsAd, child: Text(i18n.markAsAd))); + list.add(PopupMenuItem( + value: MoreVertSettings.markAsNonAd, child: Text(i18n.markAsNonAd))); + } } return list; } diff --git a/lib/pages/gallery.dart b/lib/pages/gallery.dart index 36cd0f4..0ad0039 100644 --- a/lib/pages/gallery.dart +++ b/lib/pages/gallery.dart @@ -44,14 +44,25 @@ class _GalleryPage extends State Object? _error; CancelToken? _cancel; CancelToken? _markAsNsfwCancel; + CancelToken? _markAsAdCancel; bool _isLoading = false; + bool _isSelectMode = false; + final List _selected = []; bool? get isAllNsfw => _data?.isAllNsfw; + bool get isSelectMode => _isSelectMode; Future markGalleryAsNsfw(bool isNsfw) async { try { _markAsNsfwCancel = CancelToken(); - (await api.updateGalleryFileMeta(_gid, - isNsfw: isNsfw, cancel: _markAsNsfwCancel)) - .unwrap(); + if (_isSelectMode) { + if (_selected.isEmpty) return; + (await api.updateFilesMeta(_selected.join(","), + isNsfw: isNsfw, cancel: _markAsNsfwCancel)) + .unwrap(); + } else { + (await api.updateGalleryFileMeta(_gid, + isNsfw: isNsfw, cancel: _markAsNsfwCancel)) + .unwrap(); + } if (!_markAsNsfwCancel!.isCancelled) { _fetchData(); } @@ -62,6 +73,23 @@ class _GalleryPage extends State } } + Future markAsAd(bool isAd) async { + if (!_isSelectMode || _selected.isEmpty) return; + try { + _markAsAdCancel = CancelToken(); + (await api.updateFilesMeta(_selected.join(","), + isAd: isAd, cancel: _markAsAdCancel)) + .unwrap(); + if (!_markAsAdCancel!.isCancelled) { + _fetchData(); + } + } catch (e) { + if (!_markAsAdCancel!.isCancelled) { + _log.warning("Failed to mark gallery $_gid:", e); + } + } + } + Future _fetchData() async { try { _cancel = CancelToken(); @@ -76,6 +104,7 @@ class _GalleryPage extends State setState(() { _files = fileData; _isLoading = false; + _selected.clear(); }); } } catch (e) { @@ -138,11 +167,22 @@ class _GalleryPage extends State body: isLoading ? const Center(child: CircularProgressIndicator()) : _data != null - ? GalleryInfo(_data!, - files: _files, refreshIndicatorKey: _refreshIndicatorKey, + ? GalleryInfo( + _data!, + files: _files, + refreshIndicatorKey: _refreshIndicatorKey, onRefresh: () async { - await _fetchData(); - }) + await _fetchData(); + }, + onSelectChanged: (v) { + setState(() { + _isSelectMode = v; + if (v) _selected.clear(); + }); + }, + isSelectMode: _isSelectMode, + selected: _selected, + ) : Center( child: Text("Error: $_error"), )); @@ -152,6 +192,7 @@ class _GalleryPage extends State void dispose() { _cancel?.cancel(); _markAsNsfwCancel?.cancel(); + _markAsAdCancel?.cancel(); super.dispose(); } }