From 91a32e67360de5932172a1e8736a733891c1fd86 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sat, 21 Sep 2024 22:10:59 +0800 Subject: [PATCH] :sparkles: Report abuse --- assets/locales/en_us.json | 7 +- assets/locales/zh_cn.json | 7 +- lib/bootstrapper.dart | 132 ++++++++++++-------------- lib/screens/auth/signup.dart | 5 +- lib/widgets/link_expansion.dart | 10 +- lib/widgets/posts/post_action.dart | 18 ++++ lib/widgets/reports/abuse_report.dart | 107 +++++++++++++++++++++ 7 files changed, 211 insertions(+), 75 deletions(-) create mode 100644 lib/widgets/reports/abuse_report.dart diff --git a/assets/locales/en_us.json b/assets/locales/en_us.json index 61a13d8..84973e0 100644 --- a/assets/locales/en_us.json +++ b/assets/locales/en_us.json @@ -440,5 +440,10 @@ "termRelated": "Related Terms", "appDetails": "App Details", "projectWebsite": "Project Website", - "iAmNotRobot": "I'm not a Robot" + "iAmNotRobot": "I'm not a Robot", + "report": "Report", + "reportAbuse": "Report abuse", + "reportAbuseResource": "Resource identifier", + "reportAbuseReason": "Report reason", + "reportSubmitted": "Report submitted, thank you for your contribution. We will send a notification about the result of the report within 24 hours for you." } diff --git a/assets/locales/zh_cn.json b/assets/locales/zh_cn.json index 2065bed..7655aba 100644 --- a/assets/locales/zh_cn.json +++ b/assets/locales/zh_cn.json @@ -436,5 +436,10 @@ "termRelated": "相关条款", "projectWebsite": "项目网站", "appDetails": "应用详情", - "iAmNotRobot": "我不是机器人" + "iAmNotRobot": "我不是机器人", + "report": "举报", + "reportAbuse": "举报滥用", + "reportAbuseResource": "举报的资源", + "reportAbuseReason": "举报的原因", + "reportSubmitted": "举报已提交,感谢你的贡献。我们将通过通知在 24 小时内通知该举报的处理结果。" } diff --git a/lib/bootstrapper.dart b/lib/bootstrapper.dart index e2ec24b..d01dd00 100644 --- a/lib/bootstrapper.dart +++ b/lib/bootstrapper.dart @@ -41,6 +41,68 @@ class _BootstrapperShellState extends State { final Completer _bootCompleter = Completer(); + Future _checkForUpdate() async { + if (PlatformInfo.isWeb) return; + try { + final prefs = await SharedPreferences.getInstance(); + final info = await PackageInfo.fromPlatform(); + final localVersionString = '${info.version}+${info.buildNumber}'; + final resp = await GetConnect( + timeout: const Duration(seconds: 60), + ).get( + 'https://git.solsynth.dev/api/v1/repos/hydrogen/solian/tags?page=1&limit=1', + ); + final remoteVersionString = + (resp.body as List).firstOrNull?['name'] ?? '0.0.0+0'; + final remoteVersion = Version.parse(remoteVersionString.split('+').first); + final localVersion = Version.parse(localVersionString.split('+').first); + final remoteBuildNumber = + int.tryParse(remoteVersionString.split('+').last) ?? 0; + final localBuildNumber = + int.tryParse(localVersionString.split('+').last) ?? 0; + final strictUpdate = prefs.getBool('check_update_strictly') ?? false; + if (remoteVersion > localVersion || + (remoteVersion == localVersion && + remoteBuildNumber > localBuildNumber) || + (remoteVersionString != localVersionString && strictUpdate)) { + if (PlatformInfo.isAndroid) { + context + .showConfirmDialog( + 'updateAvailable'.tr, + 'updateAvailableDesc'.trParams({ + 'from': localVersionString, + 'to': remoteVersionString, + }), + ) + .then((result) { + if (result) { + final model = UpdateModel( + 'https://files.solsynth.dev/d/production01/solian/app-arm64-v8a-release.apk', + 'solian-app-arm64-v8a-release.apk', + 'ic_launcher', + 'https://testflight.apple.com/join/YJ0lmN6O', + ); + AzhonAppUpdate.update(model); + } + }); + } else { + context.showInfoDialog( + 'updateAvailable'.tr, + 'bsCheckForUpdateDesc'.tr, + ); + } + } else if (remoteVersionString != localVersionString) { + _bootCompleter.future.then((_) { + context.showSnackbar('updateMayAvailable'.trParams({ + 'version': remoteVersionString, + })); + }); + } + } catch (e) { + context.showErrorDialog('Unable to check update: $e'); + } + } + late final List<({String label, Future Function() action})> _periods = [ ( label: 'bsLoadingTheme', @@ -48,75 +110,6 @@ class _BootstrapperShellState extends State { await context.read().restoreTheme(); }, ), - ( - label: 'bsCheckForUpdate', - action: () async { - if (PlatformInfo.isWeb) return; - try { - final prefs = await SharedPreferences.getInstance(); - final info = await PackageInfo.fromPlatform(); - final localVersionString = '${info.version}+${info.buildNumber}'; - final resp = await GetConnect().get( - 'https://git.solsynth.dev/api/v1/repos/hydrogen/solian/tags?page=1&limit=1', - ); - final remoteVersionString = - (resp.body as List).firstOrNull?['name'] ?? '0.0.0+0'; - final remoteVersion = - Version.parse(remoteVersionString.split('+').first); - final localVersion = - Version.parse(localVersionString.split('+').first); - final remoteBuildNumber = - int.tryParse(remoteVersionString.split('+').last) ?? 0; - final localBuildNumber = - int.tryParse(localVersionString.split('+').last) ?? 0; - final strictUpdate = prefs.getBool('check_update_strictly') ?? false; - if (remoteVersion > localVersion || - (remoteVersion == localVersion && - remoteBuildNumber > localBuildNumber) || - (remoteVersionString != localVersionString && strictUpdate)) { - setState(() { - _isErrored = true; - _subtitle = 'bsCheckForUpdateDesc'.tr; - }); - - if (PlatformInfo.isAndroid) { - context - .showConfirmDialog( - 'updateAvailable'.tr, - 'updateAvailableDesc'.trParams({ - 'from': localVersionString, - 'to': remoteVersionString, - }), - ) - .then((result) { - if (result) { - final model = UpdateModel( - 'https://files.solsynth.dev/d/production01/solian/app-arm64-v8a-release.apk', - 'solian-app-arm64-v8a-release.apk', - 'ic_launcher', - 'https://testflight.apple.com/join/YJ0lmN6O', - ); - AzhonAppUpdate.update(model); - } - }); - } else { - setState(() { - _isErrored = true; - _subtitle = 'bsCheckForUpdateDesc'.tr; - }); - } - } else if (remoteVersionString != localVersionString) { - _bootCompleter.future.then((_) { - context.showSnackbar('updateMayAvailable'.trParams({ - 'version': remoteVersionString, - })); - }); - } - } catch (e) { - context.showErrorDialog('Unable to check update: $e'); - } - }, - ), ( label: 'bsCheckingServer', action: () async { @@ -214,6 +207,7 @@ class _BootstrapperShellState extends State { void initState() { super.initState(); _runPeriods(); + _checkForUpdate(); } @override diff --git a/lib/screens/auth/signup.dart b/lib/screens/auth/signup.dart index 9c65b2a..961a2f1 100644 --- a/lib/screens/auth/signup.dart +++ b/lib/screens/auth/signup.dart @@ -146,7 +146,10 @@ class _SignUpScreenState extends State { const Gap(8), CheckboxListTile( value: _isTermAccepted, - title: Text('termAccept'.tr), + title: Text( + 'termAccept'.tr, + style: const TextStyle(height: 1.2), + ).paddingOnly(bottom: 4), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all( Radius.circular(8), diff --git a/lib/widgets/link_expansion.dart b/lib/widgets/link_expansion.dart index e3e10f5..ec4ab21 100644 --- a/lib/widgets/link_expansion.dart +++ b/lib/widgets/link_expansion.dart @@ -74,9 +74,13 @@ class LinkExpansion extends StatelessWidget { ), ).paddingOnly(right: 8), if (snapshot.data!.siteName != null) - Text( - snapshot.data!.siteName!, - style: Theme.of(context).textTheme.labelLarge, + Expanded( + child: Text( + snapshot.data!.siteName!, + style: Theme.of(context).textTheme.labelLarge, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), ), ], ).paddingOnly( diff --git a/lib/widgets/posts/post_action.dart b/lib/widgets/posts/post_action.dart index 5384a86..c71013a 100644 --- a/lib/widgets/posts/post_action.dart +++ b/lib/widgets/posts/post_action.dart @@ -12,6 +12,7 @@ import 'package:solian/platform.dart'; import 'package:solian/providers/auth.dart'; import 'package:solian/router.dart'; import 'package:solian/screens/posts/post_editor.dart'; +import 'package:solian/widgets/reports/abuse_report.dart'; class PostAction extends StatefulWidget { final Post item; @@ -149,6 +150,23 @@ class _PostActionState extends State { Navigator.pop(context); }, ), + ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 24), + leading: const Icon(Icons.report), + title: Text('report'.tr), + onTap: () { + showDialog( + context: context, + builder: (context) => AbuseReportDialog( + resourceId: 'post:${widget.item.id}', + ), + ).then((status) { + if (status == true) { + Navigator.pop(context); + } + }); + }, + ), if (!widget.noReact) ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 24), diff --git a/lib/widgets/reports/abuse_report.dart b/lib/widgets/reports/abuse_report.dart new file mode 100644 index 0000000..9094aca --- /dev/null +++ b/lib/widgets/reports/abuse_report.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; +import 'package:get/get.dart'; +import 'package:solian/exceptions/request.dart'; +import 'package:solian/exts.dart'; +import 'package:solian/providers/auth.dart'; + +class AbuseReportDialog extends StatefulWidget { + final String? resourceId; + + const AbuseReportDialog({super.key, this.resourceId}); + + @override + State createState() => _AbuseReportDialogState(); +} + +class _AbuseReportDialogState extends State { + final TextEditingController _resourceController = TextEditingController(); + final TextEditingController _reasonController = TextEditingController(); + + bool _isBusy = false; + + Future _submit() async { + final auth = Get.find(); + if (!auth.isAuthorized.value) return; + final client = await auth.configureClient('id'); + + setState(() => _isBusy = true); + + final resp = await client.post('/reports/abuse', { + 'resource': _resourceController.text, + 'reason': _reasonController.text, + }); + + setState(() => _isBusy = false); + + if (resp.statusCode != 200) { + context.showErrorDialog(RequestException(resp)); + } else { + context.showSnackbar('reportSubmitted'.tr); + Navigator.pop(context, true); + } + } + + @override + void initState() { + if (widget.resourceId != null) { + _resourceController.text = widget.resourceId!; + } + super.initState(); + } + + @override + void dispose() { + _resourceController.dispose(); + _reasonController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text('reportAbuse'.tr), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Gap(4), + TextField( + controller: _resourceController, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: 'reportAbuseResource'.tr, + enabled: widget.resourceId == null, + isDense: true, + ), + ), + const Gap(12), + TextField( + controller: _reasonController, + textInputAction: TextInputAction.newline, + minLines: 1, + maxLines: 3, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: 'reportAbuseReason'.tr, + isDense: true, + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: _isBusy + ? null + : () { + Navigator.pop(context); + }, + child: Text('cancel'.tr), + ), + TextButton( + onPressed: _isBusy ? null : () => _submit(), + child: Text('okay'.tr), + ), + ], + ); + } +}