mirror of
https://github.com/lifegpc/eh_downloader_flutter.git
synced 2026-06-06 05:49:03 +08:00
Update
This commit is contained in:
@@ -6,6 +6,7 @@ import 'package:retrofit/retrofit.dart';
|
|||||||
import 'api_result.dart';
|
import 'api_result.dart';
|
||||||
import 'gallery.dart';
|
import 'gallery.dart';
|
||||||
import 'status.dart';
|
import 'status.dart';
|
||||||
|
import 'tags.dart';
|
||||||
import 'token.dart';
|
import 'token.dart';
|
||||||
import 'user.dart';
|
import 'user.dart';
|
||||||
|
|
||||||
@@ -113,6 +114,12 @@ abstract class _EHApi {
|
|||||||
{@Query("all") bool? all,
|
{@Query("all") bool? all,
|
||||||
@Query("offset") int? offset,
|
@Query("offset") int? offset,
|
||||||
@Query("limit") int? limit});
|
@Query("limit") int? limit});
|
||||||
|
|
||||||
|
@GET('/tag/{id}')
|
||||||
|
// ignore: unused_element
|
||||||
|
Future<ApiResult<Tags>> _getTags(@Path("id") String id);
|
||||||
|
@GET('/tag/rows')
|
||||||
|
Future<ApiResult<List<Tag>>> getRowTags();
|
||||||
}
|
}
|
||||||
|
|
||||||
class EHApi extends __EHApi {
|
class EHApi extends __EHApi {
|
||||||
@@ -145,4 +152,8 @@ class EHApi extends __EHApi {
|
|||||||
Future<ApiResult<EhFiles>> getFiles(List<String> tokens) {
|
Future<ApiResult<EhFiles>> getFiles(List<String> tokens) {
|
||||||
return _getFiles(tokens.join(","));
|
return _getFiles(tokens.join(","));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<ApiResult<Tags>> getTags(List<int> ids) {
|
||||||
|
return _getTags(ids.join(","));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -530,6 +530,70 @@ class __EHApi implements _EHApi {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<ApiResult<Tags>> _getTags(String id) async {
|
||||||
|
const _extra = <String, dynamic>{};
|
||||||
|
final queryParameters = <String, dynamic>{};
|
||||||
|
final _headers = <String, dynamic>{};
|
||||||
|
final Map<String, dynamic>? _data = null;
|
||||||
|
final _result = await _dio
|
||||||
|
.fetch<Map<String, dynamic>>(_setStreamType<ApiResult<Tags>>(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<Tags>.fromJson(
|
||||||
|
_result.data!,
|
||||||
|
(json) => Tags.fromJson(json as Map<String, dynamic>),
|
||||||
|
);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<ApiResult<List<Tag>>> getRowTags() async {
|
||||||
|
const _extra = <String, dynamic>{};
|
||||||
|
final queryParameters = <String, dynamic>{};
|
||||||
|
final _headers = <String, dynamic>{};
|
||||||
|
final Map<String, dynamic>? _data = null;
|
||||||
|
final _result = await _dio.fetch<Map<String, dynamic>>(
|
||||||
|
_setStreamType<ApiResult<List<Tag>>>(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<List<Tag>>.fromJson(
|
||||||
|
_result.data!,
|
||||||
|
(json) => json is List<dynamic>
|
||||||
|
? json
|
||||||
|
.map<Tag>((i) => Tag.fromJson(i as Map<String, dynamic>))
|
||||||
|
.toList()
|
||||||
|
: List.empty(),
|
||||||
|
);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
RequestOptions _setStreamType<T>(RequestOptions requestOptions) {
|
RequestOptions _setStreamType<T>(RequestOptions requestOptions) {
|
||||||
if (T != dynamic &&
|
if (T != dynamic &&
|
||||||
!(requestOptions.responseType == ResponseType.bytes ||
|
!(requestOptions.responseType == ResponseType.bytes ||
|
||||||
|
|||||||
15
lib/api/tags.dart
Normal file
15
lib/api/tags.dart
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import 'api_result.dart';
|
||||||
|
import 'gallery.dart';
|
||||||
|
|
||||||
|
class Tags {
|
||||||
|
const Tags({required this.tags});
|
||||||
|
final Map<String, ApiResult<Tag>> tags;
|
||||||
|
factory Tags.fromJson(Map<String, dynamic> json) => Tags(
|
||||||
|
tags: (json).map(
|
||||||
|
(k, e) => MapEntry(
|
||||||
|
k,
|
||||||
|
ApiResult<Tag>.fromJson(e as Map<String, dynamic>,
|
||||||
|
(e) => Tag.fromJson(e as Map<String, dynamic>))),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:eh_downloader_flutter/globals.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../api/gallery.dart';
|
import '../api/gallery.dart';
|
||||||
import 'gallery_basic_info.dart';
|
import 'gallery_basic_info.dart';
|
||||||
@@ -12,6 +13,16 @@ class GalleryInfo extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _GalleryInfo extends State<GalleryInfo> {
|
class _GalleryInfo extends State<GalleryInfo> {
|
||||||
|
void showNsfwChanged(dynamic _) {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
listener.on("showNsfwChanged", showNsfwChanged);
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
bool useMobile = MediaQuery.of(context).size.width <= 810;
|
bool useMobile = MediaQuery.of(context).size.width <= 810;
|
||||||
@@ -19,12 +30,24 @@ class _GalleryInfo extends State<GalleryInfo> {
|
|||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: BoxConstraints(minHeight: constraints.maxHeight),
|
constraints: BoxConstraints(minHeight: constraints.maxHeight),
|
||||||
child: useMobile ? Column(children: [
|
child: useMobile
|
||||||
GalleryBasicInfo(widget.gData.meta, widget.gData.pages.first),
|
? Column(
|
||||||
],
|
children: [
|
||||||
) : Column(children: [
|
GalleryBasicInfo(
|
||||||
GalleryInfoDesktop(widget.gData),
|
widget.gData.meta, widget.gData.pages.first),
|
||||||
],)));
|
],
|
||||||
|
)
|
||||||
|
: Column(
|
||||||
|
children: [
|
||||||
|
GalleryInfoDesktop(widget.gData),
|
||||||
|
],
|
||||||
|
)));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
listener.removeEventListener("showNsfwChanged", showNsfwChanged);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
import '../api/gallery.dart';
|
import '../api/gallery.dart';
|
||||||
|
import 'tags.dart';
|
||||||
import 'thumbnail.dart';
|
import 'thumbnail.dart';
|
||||||
|
|
||||||
class GalleryInfoDesktop extends StatelessWidget {
|
class GalleryInfoDesktop extends StatelessWidget {
|
||||||
@@ -45,7 +46,7 @@ class GalleryInfoDesktop extends StatelessWidget {
|
|||||||
SelectableText(gData.meta.uploader),
|
SelectableText(gData.meta.uploader),
|
||||||
])),
|
])),
|
||||||
const VerticalDivider(indent: 10, endIndent: 10),
|
const VerticalDivider(indent: 10, endIndent: 10),
|
||||||
Expanded(child: Column(children: [])),
|
Expanded(child: TagsPanel(gData.tags)),
|
||||||
const VerticalDivider(indent: 10, endIndent: 10),
|
const VerticalDivider(indent: 10, endIndent: 10),
|
||||||
SizedBox(width: 150, child: Column(children: [])),
|
SizedBox(width: 150, child: Column(children: [])),
|
||||||
])),
|
])),
|
||||||
|
|||||||
80
lib/components/tags.dart
Normal file
80
lib/components/tags.dart
Normal file
@@ -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<Tag> tags;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<TagsPanel> 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<TagsPanel> {
|
||||||
|
List<(String, List<Tag>)>? data;
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
Map<String, List<Tag>> 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]!)));
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import 'dart:io';
|
|||||||
import 'package:cookie_jar/cookie_jar.dart';
|
import 'package:cookie_jar/cookie_jar.dart';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:dio_cookie_manager/dio_cookie_manager.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/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
@@ -18,6 +19,7 @@ import 'config/shared_preferences.dart';
|
|||||||
import 'config/windows.dart';
|
import 'config/windows.dart';
|
||||||
import 'main.dart';
|
import 'main.dart';
|
||||||
import 'platform/path.dart';
|
import 'platform/path.dart';
|
||||||
|
import 'tags.dart';
|
||||||
import 'utils.dart';
|
import 'utils.dart';
|
||||||
|
|
||||||
final dio = Dio()
|
final dio = Dio()
|
||||||
@@ -111,8 +113,10 @@ EHApi get api {
|
|||||||
|
|
||||||
final AuthInfo auth = AuthInfo();
|
final AuthInfo auth = AuthInfo();
|
||||||
final Path platformPath = Path();
|
final Path platformPath = Path();
|
||||||
|
final TagsInfo tags = TagsInfo();
|
||||||
final GlobalKey<ScaffoldMessengerState> rootScaffoldMessengerKey =
|
final GlobalKey<ScaffoldMessengerState> rootScaffoldMessengerKey =
|
||||||
GlobalKey<ScaffoldMessengerState>();
|
GlobalKey<ScaffoldMessengerState>();
|
||||||
|
final EventListener listener = EventListener();
|
||||||
|
|
||||||
enum MoreVertSettings {
|
enum MoreVertSettings {
|
||||||
setServerUrl,
|
setServerUrl,
|
||||||
@@ -159,6 +163,24 @@ List<PopupMenuEntry<MoreVertSettings>> buildMoreVertSettings(
|
|||||||
value: MoreVertSettings.settings,
|
value: MoreVertSettings.settings,
|
||||||
child: Text(AppLocalizations.of(context)!.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;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,6 +241,7 @@ final _authLog = Logger("AuthLog");
|
|||||||
|
|
||||||
void clearAllStates(BuildContext context) {
|
void clearAllStates(BuildContext context) {
|
||||||
auth.clear();
|
auth.clear();
|
||||||
|
tags.clear();
|
||||||
checkAuth(context);
|
checkAuth(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,5 +25,6 @@
|
|||||||
"useTitleJpn": "Use Japanese title first",
|
"useTitleJpn": "Use Japanese title first",
|
||||||
"showNsfw": "Show NSFW image by default",
|
"showNsfw": "Show NSFW image by default",
|
||||||
"read": "Read",
|
"read": "Read",
|
||||||
"download": "Download"
|
"download": "Download",
|
||||||
|
"colon": ":"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,5 +25,6 @@
|
|||||||
"useTitleJpn": "优先使用日语标题",
|
"useTitleJpn": "优先使用日语标题",
|
||||||
"showNsfw": "默认显示NSFW图片",
|
"showNsfw": "默认显示NSFW图片",
|
||||||
"read": "阅读",
|
"read": "阅读",
|
||||||
"download": "下载"
|
"download": "下载",
|
||||||
|
"colon": ":"
|
||||||
}
|
}
|
||||||
|
|||||||
34
lib/tags.dart
Normal file
34
lib/tags.dart
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'api/gallery.dart';
|
||||||
|
import 'globals.dart';
|
||||||
|
|
||||||
|
final _log = Logger("TagsInfo");
|
||||||
|
|
||||||
|
class TagsInfo {
|
||||||
|
TagsInfo();
|
||||||
|
List<Tag>? _rows;
|
||||||
|
List<Tag>? get rows => _rows;
|
||||||
|
|
||||||
|
void clear() {
|
||||||
|
_rows = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user