diff --git a/lib/api/client.dart b/lib/api/client.dart index 352249a..d1685a7 100644 --- a/lib/api/client.dart +++ b/lib/api/client.dart @@ -6,6 +6,7 @@ import 'package:retrofit/retrofit.dart'; import 'api_result.dart'; import 'gallery.dart'; import 'status.dart'; +import 'tags.dart'; import 'token.dart'; import 'user.dart'; @@ -113,6 +114,12 @@ abstract class _EHApi { {@Query("all") bool? all, @Query("offset") int? offset, @Query("limit") int? limit}); + + @GET('/tag/{id}') + // ignore: unused_element + Future> _getTags(@Path("id") String id); + @GET('/tag/rows') + Future>> getRowTags(); } class EHApi extends __EHApi { @@ -145,4 +152,8 @@ class EHApi extends __EHApi { Future> getFiles(List tokens) { return _getFiles(tokens.join(",")); } + + Future> getTags(List ids) { + return _getTags(ids.join(",")); + } } diff --git a/lib/api/client.g.dart b/lib/api/client.g.dart index e3367ac..00ec60d 100644 --- a/lib/api/client.g.dart +++ b/lib/api/client.g.dart @@ -530,6 +530,70 @@ class __EHApi implements _EHApi { return value; } + @override + Future> _getTags(String id) async { + const _extra = {}; + final queryParameters = {}; + final _headers = {}; + final Map? _data = null; + final _result = await _dio + .fetch>(_setStreamType>(Options( + method: 'GET', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/tag/${id}', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + final value = ApiResult.fromJson( + _result.data!, + (json) => Tags.fromJson(json as Map), + ); + return value; + } + + @override + Future>> getRowTags() async { + const _extra = {}; + final queryParameters = {}; + final _headers = {}; + final Map? _data = null; + final _result = await _dio.fetch>( + _setStreamType>>(Options( + method: 'GET', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/tag/rows', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + final value = ApiResult>.fromJson( + _result.data!, + (json) => json is List + ? json + .map((i) => Tag.fromJson(i as Map)) + .toList() + : List.empty(), + ); + return value; + } + RequestOptions _setStreamType(RequestOptions requestOptions) { if (T != dynamic && !(requestOptions.responseType == ResponseType.bytes || diff --git a/lib/api/tags.dart b/lib/api/tags.dart new file mode 100644 index 0000000..330a9fe --- /dev/null +++ b/lib/api/tags.dart @@ -0,0 +1,15 @@ +import 'api_result.dart'; +import 'gallery.dart'; + +class Tags { + const Tags({required this.tags}); + final Map> tags; + factory Tags.fromJson(Map json) => Tags( + tags: (json).map( + (k, e) => MapEntry( + k, + ApiResult.fromJson(e as Map, + (e) => Tag.fromJson(e as Map))), + ), + ); +} diff --git a/lib/components/gallery_info.dart b/lib/components/gallery_info.dart index da75737..25a0680 100644 --- a/lib/components/gallery_info.dart +++ b/lib/components/gallery_info.dart @@ -1,3 +1,4 @@ +import 'package:eh_downloader_flutter/globals.dart'; import 'package:flutter/material.dart'; import '../api/gallery.dart'; import 'gallery_basic_info.dart'; @@ -12,6 +13,16 @@ class GalleryInfo extends StatefulWidget { } class _GalleryInfo extends State { + void showNsfwChanged(dynamic _) { + setState(() {}); + } + + @override + void initState() { + listener.on("showNsfwChanged", showNsfwChanged); + super.initState(); + } + @override Widget build(BuildContext context) { bool useMobile = MediaQuery.of(context).size.width <= 810; @@ -19,12 +30,24 @@ class _GalleryInfo extends State { return SingleChildScrollView( child: ConstrainedBox( constraints: BoxConstraints(minHeight: constraints.maxHeight), - child: useMobile ? Column(children: [ - GalleryBasicInfo(widget.gData.meta, widget.gData.pages.first), - ], - ) : Column(children: [ - GalleryInfoDesktop(widget.gData), - ],))); + child: useMobile + ? Column( + children: [ + GalleryBasicInfo( + widget.gData.meta, widget.gData.pages.first), + ], + ) + : Column( + children: [ + GalleryInfoDesktop(widget.gData), + ], + ))); }); } + + @override + void dispose() { + listener.removeEventListener("showNsfwChanged", showNsfwChanged); + super.dispose(); + } } diff --git a/lib/components/gallery_info_desktop.dart b/lib/components/gallery_info_desktop.dart index 4a87a3a..2d7a33c 100644 --- a/lib/components/gallery_info_desktop.dart +++ b/lib/components/gallery_info_desktop.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../api/gallery.dart'; +import 'tags.dart'; import 'thumbnail.dart'; class GalleryInfoDesktop extends StatelessWidget { @@ -45,7 +46,7 @@ class GalleryInfoDesktop extends StatelessWidget { SelectableText(gData.meta.uploader), ])), const VerticalDivider(indent: 10, endIndent: 10), - Expanded(child: Column(children: [])), + Expanded(child: TagsPanel(gData.tags)), const VerticalDivider(indent: 10, endIndent: 10), SizedBox(width: 150, child: Column(children: [])), ])), diff --git a/lib/components/tags.dart b/lib/components/tags.dart new file mode 100644 index 0000000..b1ab201 --- /dev/null +++ b/lib/components/tags.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import '../api/gallery.dart'; +import '../globals.dart'; + +class TagsPanel extends StatefulWidget { + const TagsPanel(this.tags, {Key? key}) : super(key: key); + final List tags; + + @override + State createState() => _TagsPanel(); +} + +String _getTag(Tag tag) { + final tags = tag.tag.split(":"); + if (tags.length < 2) return tag.translated ?? tag.tag; + final name = tags[1]!; + return tag.translated ?? name; +} + +class _TagsPanel extends State { + List<(String, List)>? data; + @override + void initState() { + Map> maps = {}; + for (var e in widget.tags) { + final tags = e.tag.split(":"); + if (tags.length < 2) { + final list = maps[""] ?? []; + list.add(e); + maps[""] = list; + continue; + } + final name = tags[0]; + final list = maps[name] ?? []; + list.add(e); + maps[name] = list; + } + data = []; + maps.forEach((key, value) { + data!.add((key, value)); + }); + if (tags.rows == null) { + tags.getRows().then((re) { + if (re) setState(() {}); + }); + } + super.initState(); + } + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + return ListView.builder( + padding: const EdgeInsets.all(8), + itemCount: data!.length, + itemBuilder: (context, index) { + final t = data![index].$1; + final ta = data![index].$2; + final namespace = + "${tags.getTagTranslate(t) ?? t}${AppLocalizations.of(context)!.colon}"; + return Wrap( + children: List.generate(ta.length + 1, (index) { + if (index == 0) { + return Container( + margin: const EdgeInsets.all(2), + child: SelectableText(namespace)); + } else { + return Container( + margin: const EdgeInsets.all(2), + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(4.0)), + border: Border.all(width: 1, color: cs.primary), + ), + child: SelectableText(_getTag(ta[index - 1]!))); + } + })); + }); + } +} diff --git a/lib/globals.dart b/lib/globals.dart index 8375158..b71b1ab 100644 --- a/lib/globals.dart +++ b/lib/globals.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:cookie_jar/cookie_jar.dart'; import 'package:dio/dio.dart'; import 'package:dio_cookie_manager/dio_cookie_manager.dart'; +import 'package:event_listener/event_listener.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; @@ -18,6 +19,7 @@ import 'config/shared_preferences.dart'; import 'config/windows.dart'; import 'main.dart'; import 'platform/path.dart'; +import 'tags.dart'; import 'utils.dart'; final dio = Dio() @@ -111,8 +113,10 @@ EHApi get api { final AuthInfo auth = AuthInfo(); final Path platformPath = Path(); +final TagsInfo tags = TagsInfo(); final GlobalKey rootScaffoldMessengerKey = GlobalKey(); +final EventListener listener = EventListener(); enum MoreVertSettings { setServerUrl, @@ -159,6 +163,24 @@ List> buildMoreVertSettings( value: MoreVertSettings.settings, child: Text(AppLocalizations.of(context)!.settings))); } + var showNsfw = prefs.getBool("showNsfw") ?? false; + list.add(PopupMenuItem( + child: StatefulBuilder( + builder: (context, setState) => CheckboxListTile( + controlAffinity: ListTileControlAffinity.leading, + value: showNsfw, + onChanged: (value) { + if (value != null) { + prefs.setBool("showNsfw", value); + listener.emit("showNsfwChanged", null); + setState(() { + showNsfw = value; + }); + } + }, + title: Text(AppLocalizations.of(context)!.showNsfw), + ), + ))); return list; } @@ -219,6 +241,7 @@ final _authLog = Logger("AuthLog"); void clearAllStates(BuildContext context) { auth.clear(); + tags.clear(); checkAuth(context); } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 72145f1..174c860 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -25,5 +25,6 @@ "useTitleJpn": "Use Japanese title first", "showNsfw": "Show NSFW image by default", "read": "Read", - "download": "Download" + "download": "Download", + "colon": ":" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index c2656ba..085bbab 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -25,5 +25,6 @@ "useTitleJpn": "优先使用日语标题", "showNsfw": "默认显示NSFW图片", "read": "阅读", - "download": "下载" + "download": "下载", + "colon": ":" } diff --git a/lib/tags.dart b/lib/tags.dart new file mode 100644 index 0000000..c3d76ec --- /dev/null +++ b/lib/tags.dart @@ -0,0 +1,34 @@ +import 'package:logging/logging.dart'; +import 'api/gallery.dart'; +import 'globals.dart'; + +final _log = Logger("TagsInfo"); + +class TagsInfo { + TagsInfo(); + List? _rows; + List? get rows => _rows; + + void clear() { + _rows = null; + } + + Future getRows() async { + try { + _rows = (await api.getRowTags()).unwrap(); + return true; + } catch (e) { + _log.warning("Failed to load row tags:", e); + _rows = null; + return false; + } + } + + String? getTagTranslate(String row) { + final key = "rows:$row"; + if (_rows == null) return null; + final tag = _rows!.indexWhere((e) => e.tag == key); + if (tag == -1) return null; + return _rows![tag].translated; + } +}