diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..ec09b5e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,15 @@ +{ + "cSpell.words": [ + "appdata", + "Bungo", + "gjzr", + "microtask", + "mypixiv", + "pawoo", + "Rorigod", + "sleepinglife", + "Ugoira", + "vocaloidhm", + "vsync" + ] +} \ No newline at end of file diff --git a/assets/tr.json b/assets/tr.json index 170897e..0ef648f 100644 --- a/assets/tr.json +++ b/assets/tr.json @@ -64,9 +64,9 @@ "Weekly Manga": "每周漫画", "Monthly Manga": "每月漫画", "R18": "R18", - "Account": "账户", + "Account": "账号", "Logout": "登出", - "Account Settings": "账户设置", + "Account Settings": "账号设置", "Edit": "编辑", "Download": "下载", "Manage": "管理", @@ -138,7 +138,16 @@ "Line Height": "行高", "Paragraph Spacing": "段间距", "light": "浅色", - "dark": "深色" + "dark": "深色", + "block": "屏蔽", + "Block": "屏蔽", + "Block(Account)": "屏蔽(账号)", + "Block(Local)": "屏蔽(本地)", + "Add": "添加", + "Submit": "提交", + "Local": "本地", + "Both": "同时", + "This artwork is blocked": "此作品已被屏蔽" }, "zh_TW": { "Search": "搜索", @@ -279,6 +288,15 @@ "Line Height": "行高", "Paragraph Spacing": "段間距", "light": "淺色", - "dark": "深色" + "dark": "深色", + "block": "屏蔽", + "Block": "屏蔽", + "Block(Account)": "屏蔽(賬戶)", + "Block(Local)": "屏蔽(本地)", + "Add": "添加", + "Submit": "提交", + "Local": "本地", + "Both": "同時", + "This artwork is blocked": "此作品已被屏蔽" } } \ No newline at end of file diff --git a/lib/network/models.dart b/lib/network/models.dart index 9a3023d..8b93179 100644 --- a/lib/network/models.dart +++ b/lib/network/models.dart @@ -527,3 +527,25 @@ class Novel { commentsCount = json["total_comments"], isAi = json["novel_ai_type"] == 2; } + +class MuteList { + List tags; + + List authors; + + int limit; + + MuteList(this.tags, this.authors, this.limit); + + static MuteList? fromJson(Map data) { + return MuteList( + (data['muted_tags'] as List) + .map((e) => Tag(e['tag'], e['tag_translation'])) + .toList(), + (data['muted_users'] as List) + .map((e) => Author(e['user_id'], e['user_name'], e['user_account'], + e['user_profile_image_urls']['medium'], false)) + .toList(), + data['mute_limit_count']); + } +} diff --git a/lib/network/network.dart b/lib/network/network.dart index fd234e8..2293edf 100644 --- a/lib/network/network.dart +++ b/lib/network/network.dart @@ -191,6 +191,21 @@ class Network { } } + String? encodeFormData(Map? data) { + if (data == null) return null; + StringBuffer buffer = StringBuffer(); + data.forEach((key, value) { + if (value is List) { + for (var element in value) { + buffer.write("$key[]=$element&"); + } + } else { + buffer.write("$key=$value&"); + } + }); + return buffer.toString(); + } + Future>> apiPost(String path, {Map? query, Map? data}) async { try { @@ -199,7 +214,7 @@ class Network { } final res = await dio.post>(path, queryParameters: query, - data: data, + data: encodeFormData(data), options: Options( headers: headers, validateStatus: (status) => true, @@ -497,21 +512,24 @@ class Network { } } - Future> getMutedTags() async { + Future> getMuteList() async { var res = await apiGet("/v1/mute/list"); if (res.success) { - return res.data["mute_tags"] - .map((e) => Tag(e["tag"]["name"], e["tag"]["translated_name"])) - .toList(); + return Res(MuteList.fromJson(res.data)); } else { - return []; + return Res.error(res.errorMessage); } } - Future> muteTags( - List muteTags, List unmuteTags) async { + Future> editMute(List addTags, List addUsers, + List deleteTags, List deleteUsers) async { var res = await apiPost("/v1/mute/edit", - data: {"add_tags": muteTags, "delete_tags": unmuteTags}); + data: { + "add_tags": addTags, + "add_user_ids": addUsers, + "delete_tags": deleteTags, + "delete_user_ids": deleteUsers + }..removeWhere((key, value) => value.isEmpty)); if (res.success) { return const Res(true); } else { diff --git a/lib/pages/illust_page.dart b/lib/pages/illust_page.dart index 4397ca6..b4cda2a 100644 --- a/lib/pages/illust_page.dart +++ b/lib/pages/illust_page.dart @@ -5,9 +5,11 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart' show Icons; import 'package:flutter/services.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; +import 'package:pixes/appdata.dart'; import 'package:pixes/components/animated_image.dart'; import 'package:pixes/components/loading.dart'; import 'package:pixes/components/message.dart'; +import 'package:pixes/components/page_route.dart'; import 'package:pixes/components/title_bar.dart'; import 'package:pixes/foundation/app.dart'; import 'package:pixes/foundation/image_provider.dart'; @@ -17,6 +19,7 @@ import 'package:pixes/pages/comments_page.dart'; import 'package:pixes/pages/image_page.dart'; import 'package:pixes/pages/search_page.dart'; import 'package:pixes/pages/user_info_page.dart'; +import 'package:pixes/utils/block.dart'; import 'package:pixes/utils/translation.dart'; import 'package:share_plus/share_plus.dart'; @@ -143,6 +146,7 @@ class IllustPage extends StatefulWidget { class _IllustPageState extends State { @override Widget build(BuildContext context) { + var isBlocked = checkIllusts([widget.illust]).isEmpty; return buildKeyboardListener(ColoredBox( color: FluentTheme.of(context).micaBackgroundColor, child: SizedBox.expand( @@ -151,19 +155,30 @@ class _IllustPageState extends State { child: LayoutBuilder(builder: (context, constrains) { return Stack( children: [ - Positioned( - bottom: 0, - left: 0, - right: 0, - top: 0, - child: buildBody(constrains.maxWidth, constrains.maxHeight), - ), - _BottomBar( - widget.illust, - constrains.maxHeight, - constrains.maxWidth, - favoriteCallback: widget.favoriteCallback, - ), + if (!isBlocked) + Positioned( + bottom: 0, + left: 0, + right: 0, + top: 0, + child: buildBody(constrains.maxWidth, constrains.maxHeight), + ), + if (!isBlocked) + _BottomBar( + widget.illust, + constrains.maxHeight, + constrains.maxWidth, + favoriteCallback: widget.favoriteCallback, + updateCallback: () => setState(() {}), + ), + if (isBlocked) + const Positioned.fill( + child: Center( + child: Center( + child: Text( + "This artwork is blocked", + )), + )) ], ); }), @@ -306,10 +321,12 @@ class _IllustPageState extends State { class _BottomBar extends StatefulWidget { const _BottomBar(this.illust, this.height, this.width, - {this.favoriteCallback}); + {this.favoriteCallback, this.updateCallback}); final void Function(bool)? favoriteCallback; + final void Function()? updateCallback; + final Illust illust; final double height; @@ -378,8 +395,9 @@ class _BottomBarState extends State<_BottomBar> with TickerProviderStateMixin { } void _handlePointerCancel() { - if (animationController.value == 1 || animationController.value == 0) + if (animationController.value == 1 || animationController.value == 0) { return; + } if (animationController.value >= 0.5) { animationController.forward(); } else { @@ -876,7 +894,9 @@ class _BottomBarState extends State<_BottomBar> with TickerProviderStateMixin { } Widget buildMoreActions() { - return Row( + return Wrap( + runSpacing: 4, + spacing: 8, children: [ Button( onPressed: () => favorite("private"), @@ -913,10 +933,7 @@ class _BottomBarState extends State<_BottomBar> with TickerProviderStateMixin { ], ), ), - ), - const SizedBox( - width: 6, - ), + ).fixWidth(96), Button( onPressed: () { Share.share( @@ -937,10 +954,7 @@ class _BottomBarState extends State<_BottomBar> with TickerProviderStateMixin { ], ), ), - ), - const SizedBox( - width: 6, - ), + ).fixWidth(96), Button( onPressed: () { var text = "https://pixiv.net/artworks/${widget.illust.id}"; @@ -959,10 +973,7 @@ class _BottomBarState extends State<_BottomBar> with TickerProviderStateMixin { ], ), ), - ), - const SizedBox( - width: 6, - ), + ).fixWidth(96), Button( onPressed: () { context.to(() => _RelatedIllustsPage(widget.illust.id.toString())); @@ -979,12 +990,189 @@ class _BottomBarState extends State<_BottomBar> with TickerProviderStateMixin { ], ), ), - ), + ).fixWidth(96), + Button( + onPressed: () async { + await Navigator.of(context) + .push(SideBarRoute(_BlockingPage(widget.illust))); + if (mounted) { + widget.updateCallback?.call(); + } + }, + child: SizedBox( + height: 28, + child: Row( + children: [ + const Icon(MdIcons.block, size: 18), + const SizedBox( + width: 8, + ), + Text("Block".tl) + ], + ), + ), + ).fixWidth(96), ], ).paddingHorizontal(2).paddingBottom(4); } } +class _BlockingPage extends StatefulWidget { + const _BlockingPage(this.illust); + + final Illust illust; + + @override + State<_BlockingPage> createState() => __BlockingPageState(); +} + +class __BlockingPageState extends State<_BlockingPage> { + List blockedTags = []; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + TitleBar(title: "Block".tl), + Expanded( + child: ListView.builder( + padding: EdgeInsets.only(bottom: context.padding.bottom), + itemCount: widget.illust.tags.length + 2, + itemBuilder: (context, index) { + if (index == widget.illust.tags.length + 1) { + return buildSubmit(); + } + + var text = index == 0 + ? widget.illust.author.name + : widget.illust.tags[index - 1].name; + + var subTitle = index == 0 + ? "author" + : widget.illust.tags[index - 1].translatedName ?? ""; + + return Card( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + borderColor: blockedTags.contains(index) + ? ColorScheme.of(context).outlineVariant + : ColorScheme.of(context).outlineVariant.withOpacity(0.2), + padding: EdgeInsets.zero, + child: ListTile( + title: Text(text), + subtitle: Text(subTitle), + trailing: Button( + onPressed: () { + if (blockedTags.contains(index)) { + blockedTags.remove(index); + } else { + blockedTags.add(index); + } + setState(() {}); + }, + child: blockedTags.contains(index) + ? Text("Cancel".tl) + : Text("Block".tl)) + .fixWidth(72), + ), + ); + }, + ), + ) + ], + ); + } + + var flyout = FlyoutController(); + + bool isSubmitting = false; + + Widget buildSubmit() { + return FlyoutTarget( + controller: flyout, + child: FilledButton( + onPressed: () async { + if (this.blockedTags.isEmpty) { + return; + } + if (isSubmitting) return; + var blockedTags = []; + var blockedUsers = []; + for (var i in this.blockedTags) { + if (i == 0) { + blockedUsers.add(widget.illust.author.id.toString()); + } else { + blockedTags.add(widget.illust.tags[i - 1].name); + } + } + bool addToAccount = false; + bool addToLocal = false; + if (appdata.account!.user.isPremium) { + await flyout.showFlyout( + navigatorKey: App.rootNavigatorKey.currentState, + builder: (context) { + return MenuFlyout( + items: [ + MenuFlyoutItem( + text: Text("Local".tl), + onPressed: () { + addToLocal = true; + }), + MenuFlyoutItem( + text: Text("Account".tl), + onPressed: () { + addToAccount = true; + }), + MenuFlyoutItem( + text: Text("Both".tl), + onPressed: () { + addToLocal = true; + addToAccount = true; + }), + ], + ); + }); + } else { + addToLocal = true; + } + if (addToAccount) { + setState(() { + isSubmitting = true; + }); + var res = + await Network().editMute(blockedTags, blockedUsers, [], []); + setState(() { + isSubmitting = false; + }); + if (res.error) { + if (mounted) { + context.showToast(message: "Network Error"); + } + return; + } + } + if (addToLocal) { + for (var tag in blockedTags) { + appdata.settings['blockTags'].add(tag); + } + for (var user in blockedUsers) { + appdata.settings['blockTags'].add('user:$user'); + } + appdata.writeSettings(); + } + if (mounted) { + context.pop(); + } + }, + child: isSubmitting + ? const ProgressRing( + strokeWidth: 1.6, + ).fixWidth(18).fixHeight(18).toAlign(Alignment.center) + : Text("Submit".tl), + ).fixWidth(96).fixHeight(28), + ).toAlign(Alignment.center).paddingTop(16); + } +} + class IllustPageWithId extends StatefulWidget { const IllustPageWithId(this.id, {super.key}); diff --git a/lib/pages/search_page.dart b/lib/pages/search_page.dart index 41cae87..0d6b4a7 100644 --- a/lib/pages/search_page.dart +++ b/lib/pages/search_page.dart @@ -2,7 +2,6 @@ import 'package:fluent_ui/fluent_ui.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:pixes/appdata.dart'; import 'package:pixes/components/loading.dart'; -import 'package:pixes/components/message.dart'; import 'package:pixes/components/novel.dart'; import 'package:pixes/components/page_route.dart'; import 'package:pixes/components/user_preview.dart'; @@ -128,7 +127,6 @@ class _SearchPageState extends State { ), onPressed: () { optionController.showFlyout( - navigatorKey: App.rootNavigatorKey.currentState, placementMode: FlyoutPlacementMode.bottomCenter, builder: buildSearchOption, ); diff --git a/lib/pages/settings_page.dart b/lib/pages/settings_page.dart index 69d3edc..f200da2 100644 --- a/lib/pages/settings_page.dart +++ b/lib/pages/settings_page.dart @@ -131,16 +131,16 @@ class _SettingsPageState extends State { return; } context.to(() => _SetSingleFieldPage( - "Download Path".tl, - "downloadPath", - check: (text) { - if(!Directory(text).havePermission()) { - return "No permission".tl; - } else { - return null; - } - }, - )); + "Download Path".tl, + "downloadPath", + check: (text) { + if (!Directory(text).havePermission()) { + return "No permission".tl; + } else { + return null; + } + }, + )); }), ), buildItem( @@ -182,23 +182,30 @@ class _SettingsPageState extends State { buildItem( title: "Github", action: IconButton( - icon: const Icon(MdIcons.open_in_new, size: 18,), + icon: const Icon( + MdIcons.open_in_new, + size: 18, + ), onPressed: () => launchUrlString("https://github.com/wgh136/pixes"), )), buildItem( title: "Telegram", action: IconButton( - icon: const Icon(MdIcons.open_in_new, size: 18,), - onPressed: () => - launchUrlString("https://t.me/pica_group"), + icon: const Icon( + MdIcons.open_in_new, + size: 18, + ), + onPressed: () => launchUrlString("https://t.me/pica_group"), )), buildItem( title: "Logs", action: IconButton( - icon: const Icon(MdIcons.open_in_new, size: 18,), - onPressed: () => context.to(() => const LogsPage()) - )), + icon: const Icon( + MdIcons.open_in_new, + size: 18, + ), + onPressed: () => context.to(() => const LogsPage()))), ], ), ); @@ -214,9 +221,25 @@ class _SettingsPageState extends State { child: Text("Edit".tl).fixWidth(64), onPressed: () { context.to(() => _SetSingleFieldPage( - "Http ${"Proxy".tl}", - "proxy", - )); + "Http ${"Proxy".tl}", + "proxy", + )); + }, + )), + buildItem( + title: "Block(Account)".tl, + action: Button( + child: Text("Edit".tl).fixWidth(64), + onPressed: () { + launchUrlString("https://www.pixiv.net/setting_mute.php"); + }, + )), + buildItem( + title: "Block(Local)".tl, + action: Button( + child: Text("Edit".tl).fixWidth(64), + onPressed: () { + context.to(() => const _BlockTagsPage()); }, )), ], @@ -231,63 +254,77 @@ class _SettingsPageState extends State { buildItem( title: "Theme".tl, action: DropDownButton( - title: Text(appdata.settings["theme"] ?? "System".tl), - items: [ - MenuFlyoutItem(text: Text("System".tl), onPressed: () { - setState(() { - appdata.settings["theme"] = "System"; - }); - appdata.writeData(); - StateController.findOrNull(tag: "MyApp")?.update(); - }), - MenuFlyoutItem(text: Text("light".tl), onPressed: () { - setState(() { - appdata.settings["theme"] = "Light"; - }); - appdata.writeData(); - StateController.findOrNull(tag: "MyApp")?.update(); - }), - MenuFlyoutItem(text: Text("dark".tl), onPressed: () { - setState(() { - appdata.settings["theme"] = "Dark"; - }); - appdata.writeData(); - StateController.findOrNull(tag: "MyApp")?.update(); - }), - ])), + title: Text(appdata.settings["theme"] ?? "System".tl), + items: [ + MenuFlyoutItem( + text: Text("System".tl), + onPressed: () { + setState(() { + appdata.settings["theme"] = "System"; + }); + appdata.writeData(); + StateController.findOrNull(tag: "MyApp")?.update(); + }), + MenuFlyoutItem( + text: Text("light".tl), + onPressed: () { + setState(() { + appdata.settings["theme"] = "Light"; + }); + appdata.writeData(); + StateController.findOrNull(tag: "MyApp")?.update(); + }), + MenuFlyoutItem( + text: Text("dark".tl), + onPressed: () { + setState(() { + appdata.settings["theme"] = "Dark"; + }); + appdata.writeData(); + StateController.findOrNull(tag: "MyApp")?.update(); + }), + ])), buildItem( title: "Language".tl, action: DropDownButton( title: Text(appdata.settings["language"] ?? "System"), items: [ - MenuFlyoutItem(text: const Text("System"), onPressed: () { - setState(() { - appdata.settings["language"] = "System"; - }); - appdata.writeData(); - StateController.findOrNull(tag: "MyApp")?.update(); - }), - MenuFlyoutItem(text: const Text("English"), onPressed: () { - setState(() { - appdata.settings["language"] = "English"; - }); - appdata.writeData(); - StateController.findOrNull(tag: "MyApp")?.update(); - }), - MenuFlyoutItem(text: const Text("简体中文"), onPressed: () { - setState(() { - appdata.settings["language"] = "简体中文"; - }); - appdata.writeData(); - StateController.findOrNull(tag: "MyApp")?.update(); - }), - MenuFlyoutItem(text: const Text("繁體中文"), onPressed: () { - setState(() { - appdata.settings["language"] = "繁體中文"; - }); - appdata.writeData(); - StateController.findOrNull(tag: "MyApp")?.update(); - }), + MenuFlyoutItem( + text: const Text("System"), + onPressed: () { + setState(() { + appdata.settings["language"] = "System"; + }); + appdata.writeData(); + StateController.findOrNull(tag: "MyApp")?.update(); + }), + MenuFlyoutItem( + text: const Text("English"), + onPressed: () { + setState(() { + appdata.settings["language"] = "English"; + }); + appdata.writeData(); + StateController.findOrNull(tag: "MyApp")?.update(); + }), + MenuFlyoutItem( + text: const Text("简体中文"), + onPressed: () { + setState(() { + appdata.settings["language"] = "简体中文"; + }); + appdata.writeData(); + StateController.findOrNull(tag: "MyApp")?.update(); + }), + MenuFlyoutItem( + text: const Text("繁體中文"), + onPressed: () { + setState(() { + appdata.settings["language"] = "繁體中文"; + }); + appdata.writeData(); + StateController.findOrNull(tag: "MyApp")?.update(); + }), ])), ], ), @@ -416,3 +453,88 @@ ${"Some keywords will be replaced by the following rule:".tl} ${"Multiple path separators will be automatically replaced with a single".tl} """; } + +class _BlockTagsPage extends StatefulWidget { + const _BlockTagsPage(); + + @override + State<_BlockTagsPage> createState() => __BlockTagsPageState(); +} + +class __BlockTagsPageState extends State<_BlockTagsPage> { + @override + Widget build(BuildContext context) { + return Column( + children: [ + TitleBar( + title: "Block".tl, + action: FilledButton( + child: Text("Add".tl), + onPressed: () { + var controller = TextEditingController(); + + void finish(BuildContext context) { + var text = controller.text; + if (text.isNotEmpty && + !(appdata.settings["blockTags"] as List).contains(text)) { + setState(() { + appdata.settings["blockTags"].add(text); + }); + appdata.writeSettings(); + } + context.pop(); + } + + showDialog( + context: context, + barrierDismissible: true, + builder: (context) { + return ContentDialog( + title: Text("Add".tl), + content: SizedBox( + width: 300, + height: 32, + child: TextBox( + controller: controller, + onSubmitted: (v) => finish(context), + ), + ), + actions: [ + FilledButton( + child: Text("Submit".tl), + onPressed: () { + finish(context); + }) + ], + ); + }); + }, + ), + ), + Expanded( + child: ListView.builder( + itemCount: appdata.settings["blockTags"].length, + itemBuilder: (context, index) { + return Card( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + padding: EdgeInsets.zero, + child: ListTile( + title: Text(appdata.settings["blockTags"][index]), + trailing: Button( + child: Text("Delete".tl), + onPressed: () { + setState(() { + (appdata.settings["blockTags"] as List).removeAt(index); + }); + appdata.writeSettings(); + }, + ), + ), + ); + }, + ), + ) + ], + ); + } +} diff --git a/lib/utils/block.dart b/lib/utils/block.dart index 606820f..3a5a270 100644 --- a/lib/utils/block.dart +++ b/lib/utils/block.dart @@ -1,7 +1,7 @@ import 'package:pixes/appdata.dart'; import 'package:pixes/network/models.dart'; -void checkIllusts(List illusts) { +List checkIllusts(List illusts) { illusts.removeWhere((illust) { if (illust.isBlocked) { return true; @@ -9,7 +9,7 @@ void checkIllusts(List illusts) { if (appdata.settings["blockTags"] == null) { return false; } - if (appdata.settings["blockTags"].contains(illust.author.name)) { + if (appdata.settings["blockTags"].contains("user:${illust.author.name}")) { return true; } for (var tag in illust.tags) { @@ -19,4 +19,5 @@ void checkIllusts(List illusts) { } return false; }); + return illusts; }