Compare commits

...

10 Commits

Author SHA1 Message Date
de39799301 🚀 Launch 1.2.3 2024-09-22 22:57:00 +08:00
4b921602a2 🐛 Bug fixes 2024-09-22 22:56:28 +08:00
6cde218393 💄 Optimization of post item style 2024-09-21 23:28:14 +08:00
c896185af0 See other user recent fortune 2024-09-21 23:10:20 +08:00
4cbeafd447 Account deletion 2024-09-21 22:44:08 +08:00
91a32e6736 Report abuse 2024-09-21 22:10:59 +08:00
befc647b03 💄 Improved about page 2024-09-19 20:39:09 +08:00
16b2e3a0c7 Terms that show up let user accept 2024-09-19 20:34:04 +08:00
0cc842c030 🐛 Fix upgrade detection method 2024-09-18 20:27:13 +08:00
fb370a484d 🐛 Fix english localization update message placeholder issue 2024-09-18 19:55:42 +08:00
22 changed files with 1023 additions and 453 deletions

View File

@ -428,9 +428,30 @@
"preferencesApplied": "Preferences has been applied.", "preferencesApplied": "Preferences has been applied.",
"save": "Save", "save": "Save",
"updateAvailable": "Update available", "updateAvailable": "Update available",
"updateAvailableDesc": "There is an update available (@version). Do you want to download and install it now? You can still use the app normally while waiting for the download to complete.", "updateAvailableDesc": "There is an update available (@from to @to). Do you want to download and install it now? You can still use the app normally while waiting for the download to complete.",
"update": "Update", "update": "Update",
"updateCheckStrictly": "Strict mode", "updateCheckStrictly": "Strict mode",
"updateCheckStrictlyDesc": "If enabled, the app will ask for updating once the local version is different from remote one.", "updateCheckStrictlyDesc": "If enabled, the app will ask for updating once the local version is different from remote one.",
"updateMayAvailable": "App version @version is available, you can update from app store or our website." "updateMayAvailable": "App version @version is available, you can update from app store or our website.",
"updateNow": "Update now",
"termAccept": "I've read and agree to Solar Network's Terms",
"termAcceptDesc": "Including but not limited to \"User Agreement\" and \"Privacy Policy\"",
"termAcceptLink": "View terms",
"termAcceptNextWithAgree": "By clicking the \"Next\", it means you agree to our terms and its updates. You should already agreed with them while you sign up.",
"termRelated": "Related Terms",
"appDetails": "App Details",
"projectWebsite": "Project Website",
"iAmNotRobot": "I'm not a Robot",
"report": "Report",
"reportAbuse": "Report abuse",
"reportAbuseDesc": "Report any violation of service terms",
"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.",
"accountDeletion": "Request account deletion",
"accountDeletionDesc": "Delete the current account and all its data. Note that this action is irreversible!",
"accountDeletionConfirm": "Confirm request account deletion",
"accountDeletionConfirmDesc": "Are you sure to delete account @account? You will receive a confirmation email with a link to confirm the deletion of the account within 24 hours. Note that this action is irreversible, and all data associated with the account will be deleted, and you should be careful about it.",
"accountDeletionRequested": "Account deletion requested, check your inbox to confirm the request.",
"slideToConfirm": "Slide to confirm"
} }

View File

@ -428,5 +428,26 @@
"update": "更新", "update": "更新",
"updateCheckStrictly": "严格模式", "updateCheckStrictly": "严格模式",
"updateCheckStrictlyDesc": "如果启用,应用程序将会在本地版本与远程版本不同时询问更新,而不会检查版本号大小。", "updateCheckStrictlyDesc": "如果启用,应用程序将会在本地版本与远程版本不同时询问更新,而不会检查版本号大小。",
"updateMayAvailable": "版本 @version 现已可用,你可以前往应用商店或是我们的官网下载更新" "updateNow": "立即更新",
"updateMayAvailable": "版本 @version 现已可用,你可以前往应用商店或是我们的官网下载更新。",
"termAccept": "我已阅读并同意 Solar Network 各项条款",
"termAcceptDesc": "包括但不限于《用户守则》和《隐私政策》",
"termAcceptLink": "浏览条款",
"termAcceptNextWithAgree": "点击 “下一步”,即表示你同意我们的各项条款,包括其之后的更新。你应该在注册时已经同意过了。",
"termRelated": "相关条款",
"projectWebsite": "项目网站",
"appDetails": "应用详情",
"iAmNotRobot": "我不是机器人",
"report": "举报",
"reportAbuse": "举报滥用",
"reportAbuseDesc": "举报任何违反服务条款的行为",
"reportAbuseResource": "举报的资源",
"reportAbuseReason": "举报的原因",
"reportSubmitted": "举报已提交,感谢你的贡献。我们将通过通知在 24 小时内通知该举报的处理结果。",
"accountDeletion": "请求删除账号",
"accountDeletionDesc": "删除目前登陆的账号,及其所有的数据。注意,该操作不可撤销!",
"accountDeletionConfirm": "确认账号删除请求",
"accountDeletionConfirmDesc": "你确定要删除账号 @account 吗?你将会在其绑定的主要邮件地址收到一封包含着确认删除账号连接的邮件,在二十四小时内使用该连接即可完成删除账号。注意,本操作不可撤销,并且账号创建或关联的所有数据都将被删除,请三思而后行。",
"accountDeletionRequested": "已请求删除账号,检查你的收件箱来确认请求。",
"slideToConfirm": "滑动来确认"
} }

View File

@ -227,7 +227,7 @@ PODS:
- TOCropViewController (~> 2.7.4) - TOCropViewController (~> 2.7.4)
- image_picker_ios (0.0.1): - image_picker_ios (0.0.1):
- Flutter - Flutter
- livekit_client (2.2.5): - livekit_client (2.2.6):
- Flutter - Flutter
- WebRTC-SDK (= 125.6422.04) - WebRTC-SDK (= 125.6422.04)
- media_kit_libs_ios_video (1.0.4): - media_kit_libs_ios_video (1.0.4):
@ -482,7 +482,7 @@ SPEC CHECKSUMS:
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
image_cropper: 37d40f62177c101ff4c164906d259ea2c3aa70cf image_cropper: 37d40f62177c101ff4c164906d259ea2c3aa70cf
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
livekit_client: 9c8080879256a0fb16da13c9be4845248209d896 livekit_client: 20e01637431bc108dad451c8a11c1d206e1dd2cd
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1 media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e

View File

@ -6,6 +6,7 @@ import 'package:get/get.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:solian/exceptions/request.dart';
import 'package:solian/exts.dart'; import 'package:solian/exts.dart';
import 'package:solian/platform.dart'; import 'package:solian/platform.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
@ -41,6 +42,85 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
final Completer _bootCompleter = Completer(); final Completer _bootCompleter = Completer();
void _updateNow(String localVersionString, String remoteVersionString) {
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);
}
});
}
Future<void> _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',
);
if (resp.statusCode != 200) {
throw RequestException(resp);
}
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) {
_updateNow(localVersionString, remoteVersionString);
} else {
context.showInfoDialog(
'updateAvailable'.tr,
'bsCheckForUpdateDesc'.tr,
);
}
} else if (remoteVersionString != localVersionString) {
_bootCompleter.future.then((_) {
context.showSnackbar(
'updateMayAvailable'.trParams({
'version': remoteVersionString,
}),
action: PlatformInfo.isAndroid
? SnackBarAction(
label: 'updateNow'.tr,
onPressed: () {
_updateNow(localVersionString, remoteVersionString);
},
)
: null,
);
});
}
} catch (e) {
context.showErrorDialog('Unable to check update: $e');
}
}
late final List<({String label, Future<void> Function() action})> _periods = [ late final List<({String label, Future<void> Function() action})> _periods = [
( (
label: 'bsLoadingTheme', label: 'bsLoadingTheme',
@ -48,68 +128,6 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
await context.read<ThemeSwitcher>().restoreTheme(); await context.read<ThemeSwitcher>().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';
final remoteVersion = Version.parse(remoteVersionString ?? '0.0.0');
final localVersion =
Version.parse(localVersionString.split('+').first);
final strictUpdate = prefs.getBool('check_update_strictly') ?? false;
if (remoteVersion > localVersion ||
(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', label: 'bsCheckingServer',
action: () async { action: () async {
@ -207,6 +225,7 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
void initState() { void initState() {
super.initState(); super.initState();
_runPeriods(); _runPeriods();
_checkForUpdate();
} }
@override @override

View File

@ -1,6 +1,8 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'package:action_slider/action_slider.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:solian/exceptions/request.dart'; import 'package:solian/exceptions/request.dart';
import 'package:solian/exceptions/unauthorized.dart'; import 'package:solian/exceptions/unauthorized.dart';
@ -73,6 +75,47 @@ extension AppExtensions on BuildContext {
false; false;
} }
Future<bool> showSlideToConfirmDialog(String title, body) async {
return await showDialog<bool>(
useRootNavigator: true,
context: this,
builder: (ctx) => AlertDialog(
title: Text(title, textAlign: TextAlign.center),
content: SizedBox(
width: double.maxFinite,
child: ListView(
shrinkWrap: true,
children: [
Text(body, textAlign: TextAlign.center),
const Gap(28),
ActionSlider.standard(
icon: const Icon(Icons.send),
iconAlignment: Alignment.center,
sliderBehavior: SliderBehavior.move,
actionThresholdType: ThresholdType.release,
toggleColor: Colors.red,
action: (controller) async {
controller.success();
await Future.delayed(const Duration(milliseconds: 500));
Navigator.pop(ctx, true);
},
child: Text('slideToConfirm'.tr),
),
],
),
),
actionsAlignment: MainAxisAlignment.center,
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: Text('cancel'.tr),
)
],
),
) ??
false;
}
Future<void> showErrorDialog(dynamic exception) { Future<void> showErrorDialog(dynamic exception) {
Widget content = Text(exception.toString().capitalize!); Widget content = Text(exception.toString().capitalize!);
if (exception is UnauthorizedException) { if (exception is UnauthorizedException) {

View File

@ -1,6 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:get/get.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:solian/widgets/sized_container.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
class AboutScreen extends StatelessWidget { class AboutScreen extends StatelessWidget {
@ -47,31 +49,50 @@ class AboutScreen extends StatelessWidget {
), ),
Text('Copyright © ${DateTime.now().year} Solsynth LLC'), Text('Copyright © ${DateTime.now().year} Solsynth LLC'),
const Gap(16), const Gap(16),
TextButton( CenteredContainer(
style: denseButtonStyle, maxWidth: 280,
child: const Text('App Details'), child: Wrap(
onPressed: () async { spacing: 8,
final info = await PackageInfo.fromPlatform(); runSpacing: 8,
children: [
TextButton(
style: denseButtonStyle,
child: Text('appDetails'.tr),
onPressed: () async {
final info = await PackageInfo.fromPlatform();
showAboutDialog( showAboutDialog(
context: context, context: context,
applicationVersion: '${info.version} (${info.buildNumber})', applicationVersion:
applicationLegalese: '${info.version} (${info.buildNumber})',
'The Solar Network App is an intuitive and self-hostable social network and computing platform. Experience the freedom of a user-friendly design that empowers you to create and connect with communities on your own terms. Embrace the future of social networking with a platform that prioritizes your independence and privacy.', applicationLegalese:
applicationIcon: ClipRRect( 'The Solar Network App is an intuitive and open-source social network and computing platform. Experience the freedom of a user-friendly design that empowers you to create and connect with communities on your own terms. Embrace the future of social networking with a platform that prioritizes your independence and privacy.',
borderRadius: const BorderRadius.all(Radius.circular(16)), applicationIcon: ClipRRect(
child: borderRadius:
Image.asset('assets/logo.png', width: 60, height: 60), const BorderRadius.all(Radius.circular(16)),
child: Image.asset('assets/logo.png',
width: 60, height: 60),
),
);
},
), ),
); TextButton(
}, style: denseButtonStyle,
), child: Text('projectWebsite'.tr),
TextButton( onPressed: () {
style: denseButtonStyle, launchUrlString(
child: const Text('Project Website'), 'https://solsynth.dev/products/solar-network');
onPressed: () { },
launchUrlString('https://solsynth.dev/products/solar-network'); ),
}, TextButton(
style: denseButtonStyle,
child: Text('termRelated'.tr),
onPressed: () {
launchUrlString('https://solsynth.dev/terms');
},
),
],
),
), ),
const Gap(16), const Gap(16),
const Text( const Text(

View File

@ -1,11 +1,16 @@
import 'dart:math';
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:intl/intl.dart';
import 'package:solian/controllers/post_list_controller.dart'; import 'package:solian/controllers/post_list_controller.dart';
import 'package:solian/exts.dart'; import 'package:solian/exts.dart';
import 'package:solian/models/account.dart'; import 'package:solian/models/account.dart';
import 'package:solian/models/attachment.dart'; import 'package:solian/models/attachment.dart';
import 'package:solian/models/daily_sign.dart';
import 'package:solian/models/pagination.dart'; import 'package:solian/models/pagination.dart';
import 'package:solian/models/post.dart'; import 'package:solian/models/post.dart';
import 'package:solian/models/subscription.dart'; import 'package:solian/models/subscription.dart';
@ -18,6 +23,7 @@ import 'package:solian/widgets/account/account_avatar.dart';
import 'package:solian/widgets/account/account_heading.dart'; import 'package:solian/widgets/account/account_heading.dart';
import 'package:solian/widgets/app_bar_leading.dart'; import 'package:solian/widgets/app_bar_leading.dart';
import 'package:solian/widgets/attachments/attachment_list.dart'; import 'package:solian/widgets/attachments/attachment_list.dart';
import 'package:solian/widgets/daily_sign/history_chart.dart';
import 'package:solian/widgets/posts/post_list.dart'; import 'package:solian/widgets/posts/post_list.dart';
import 'package:solian/widgets/posts/post_warped_list.dart'; import 'package:solian/widgets/posts/post_warped_list.dart';
import 'package:solian/widgets/sized_container.dart'; import 'package:solian/widgets/sized_container.dart';
@ -45,6 +51,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
Account? _userinfo; Account? _userinfo;
Subscription? _subscription; Subscription? _subscription;
List<Post> _pinnedPosts = List.empty(); List<Post> _pinnedPosts = List.empty();
List<DailySignRecord> _dailySignRecords = List.empty();
int _totalUpvote = 0, _totalDownvote = 0; int _totalUpvote = 0, _totalDownvote = 0;
Future<void> _getSubscription() async { Future<void> _getSubscription() async {
@ -57,7 +64,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
Future<void> _getUserinfo() async { Future<void> _getUserinfo() async {
setState(() => _isBusy = true); setState(() => _isBusy = true);
var client = await ServiceFinder.configureClient('auth'); var client = await ServiceFinder.configureClient('id');
var resp = await client.get('/users/${widget.name}'); var resp = await client.get('/users/${widget.name}');
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
context.showErrorDialog(resp.bodyString).then((_) { context.showErrorDialog(resp.bodyString).then((_) {
@ -67,7 +74,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
_userinfo = Account.fromJson(resp.body); _userinfo = Account.fromJson(resp.body);
} }
client = await ServiceFinder.configureClient('interactive'); client = await ServiceFinder.configureClient('co');
resp = await client.get('/users/${widget.name}'); resp = await client.get('/users/${widget.name}');
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
context.showErrorDialog(resp.bodyString).then((_) { context.showErrorDialog(resp.bodyString).then((_) {
@ -82,7 +89,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
} }
Future<void> _getPinnedPosts() async { Future<void> _getPinnedPosts() async {
final client = await ServiceFinder.configureClient('interactive'); final client = await ServiceFinder.configureClient('co');
final resp = await client.get('/users/${widget.name}/pin'); final resp = await client.get('/users/${widget.name}/pin');
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
context.showErrorDialog(resp.bodyString).then((_) { context.showErrorDialog(resp.bodyString).then((_) {
@ -96,6 +103,23 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
} }
} }
Future<void> _getDailySignRecords() async {
final client = await ServiceFinder.configureClient('id');
final resp = await client.get('/users/${widget.name}/daily?take=14');
if (resp.statusCode != 200) {
context.showErrorDialog(resp.bodyString).then((_) {
Navigator.pop(context);
});
} else {
final result = PaginationResult.fromJson(resp.body);
setState(() {
_dailySignRecords = List.from(
result.data?.map((x) => DailySignRecord.fromJson(x)) ?? [],
);
});
}
}
int get _userSocialCreditPoints { int get _userSocialCreditPoints {
return _totalUpvote * 2 - _totalDownvote + _postController.postTotal.value; return _totalUpvote * 2 - _totalDownvote + _postController.postTotal.value;
} }
@ -129,6 +153,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
_getUserinfo().then((_) { _getUserinfo().then((_) {
_getSubscription(); _getSubscription();
_getPinnedPosts(); _getPinnedPosts();
_getDailySignRecords();
}); });
} }
@ -168,96 +193,99 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
toolbarHeight: AppTheme.toolbarHeight(context), toolbarHeight: AppTheme.toolbarHeight(context),
leadingWidth: 24, leadingWidth: 24,
automaticallyImplyLeading: false, automaticallyImplyLeading: false,
flexibleSpace: Row( flexibleSpace: SizedBox(
children: [ height: 56,
AppBarLeadingButton.adaptive(context) ?? const Gap(8), child: Row(
const Gap(8), children: [
if (_userinfo != null) AppBarLeadingButton.adaptive(context) ?? const Gap(8),
AccountAvatar(content: _userinfo!.avatar, radius: 16), const Gap(8),
const Gap(12), if (_userinfo != null)
Expanded( AccountAvatar(content: _userinfo!.avatar, radius: 16),
child: Column( const Gap(12),
mainAxisAlignment: MainAxisAlignment.center, Expanded(
crossAxisAlignment: CrossAxisAlignment.start, child: Column(
children: [ mainAxisAlignment: MainAxisAlignment.center,
if (_userinfo != null) crossAxisAlignment: CrossAxisAlignment.start,
Text( children: [
_userinfo!.nick, if (_userinfo != null)
style: Theme.of(context).textTheme.bodyLarge, Text(
), _userinfo!.nick,
if (_userinfo != null) style: Theme.of(context).textTheme.bodyLarge,
Text( ),
'@${_userinfo!.name}', if (_userinfo != null)
style: Theme.of(context).textTheme.bodySmall, Text(
), '@${_userinfo!.name}',
], style: Theme.of(context).textTheme.bodySmall,
), ),
), ],
if (_userinfo != null && _subscription == null)
OutlinedButton(
style: const ButtonStyle(
visualDensity:
VisualDensity(horizontal: -4, vertical: -2),
), ),
onPressed: _isSubscribing ),
? null if (_userinfo != null && _subscription == null)
: () async { OutlinedButton(
setState(() => _isSubscribing = true); style: const ButtonStyle(
_subscription = visualDensity:
await Get.find<SubscriptionProvider>() VisualDensity(horizontal: -4, vertical: -2),
.subscribeToUser(_userinfo!.id); ),
setState(() => _isSubscribing = false); onPressed: _isSubscribing
}, ? null
child: Text('subscribe'.tr), : () async {
) setState(() => _isSubscribing = true);
else if (_userinfo != null) _subscription =
OutlinedButton( await Get.find<SubscriptionProvider>()
style: const ButtonStyle( .subscribeToUser(_userinfo!.id);
visualDensity: setState(() => _isSubscribing = false);
VisualDensity(horizontal: -4, vertical: -2), },
child: Text('subscribe'.tr),
)
else if (_userinfo != null)
OutlinedButton(
style: const ButtonStyle(
visualDensity:
VisualDensity(horizontal: -4, vertical: -2),
),
onPressed: _isSubscribing
? null
: () async {
setState(() => _isSubscribing = true);
await Get.find<SubscriptionProvider>()
.unsubscribeFromUser(_userinfo!.id);
_subscription = null;
setState(() => _isSubscribing = false);
},
child: Text('unsubscribe'.tr),
), ),
onPressed: _isSubscribing if (_userinfo != null &&
? null !_relationshipProvider.hasFriend(_userinfo!))
: () async { IconButton(
setState(() => _isSubscribing = true); icon: const Icon(Icons.person_add),
await Get.find<SubscriptionProvider>() onPressed: _isMakingFriend
.unsubscribeFromUser(_userinfo!.id); ? null
_subscription = null; : () async {
setState(() => _isSubscribing = false); setState(() => _isMakingFriend = true);
}, try {
child: Text('unsubscribe'.tr), await _relationshipProvider
.makeFriend(widget.name);
context.showSnackbar(
'accountFriendRequestSent'.tr,
);
} catch (e) {
context.showErrorDialog(e);
} finally {
setState(() => _isMakingFriend = false);
}
},
)
else
const IconButton(
icon: Icon(Icons.handshake),
onPressed: null,
),
SizedBox(
width: AppTheme.isLargeScreen(context) ? 8 : 16,
), ),
if (_userinfo != null && ],
!_relationshipProvider.hasFriend(_userinfo!)) ),
IconButton( ).paddingOnly(top: MediaQuery.of(context).padding.top),
icon: const Icon(Icons.person_add),
onPressed: _isMakingFriend
? null
: () async {
setState(() => _isMakingFriend = true);
try {
await _relationshipProvider
.makeFriend(widget.name);
context.showSnackbar(
'accountFriendRequestSent'.tr,
);
} catch (e) {
context.showErrorDialog(e);
} finally {
setState(() => _isMakingFriend = false);
}
},
)
else
const IconButton(
icon: Icon(Icons.handshake),
onPressed: null,
),
SizedBox(
width: AppTheme.isLargeScreen(context) ? 8 : 16,
),
],
),
bottom: TabBar( bottom: TabBar(
tabs: [ tabs: [
Tab(text: 'profilePage'.tr), Tab(text: 'profilePage'.tr),
@ -271,21 +299,132 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
body: TabBarView( body: TabBarView(
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
children: [ children: [
Column( ListView(
children: [ children: [
const Gap(16), const Gap(16),
AccountHeadingWidget( CenteredContainer(
name: _userinfo!.name, child: AccountHeadingWidget(
nick: _userinfo!.nick, name: _userinfo!.name,
desc: _userinfo!.description, nick: _userinfo!.nick,
badges: _userinfo!.badges, desc: _userinfo!.description,
banner: _userinfo!.banner, badges: _userinfo!.badges,
avatar: _userinfo!.avatar, banner: _userinfo!.banner,
status: Get.find<StatusProvider>() avatar: _userinfo!.avatar,
.getSomeoneStatus(_userinfo!.name), status: Get.find<StatusProvider>()
detail: _userinfo, .getSomeoneStatus(_userinfo!.name),
profile: _userinfo!.profile, detail: _userinfo,
extraWidgets: const [], profile: _userinfo!.profile,
extraWidgets: [
if (_dailySignRecords.isNotEmpty)
Card(
child: SizedBox(
height: 180,
width:
max(640, MediaQuery.of(context).size.width),
child: LineChart(
LineChartData(
lineBarsData: [
LineChartBarData(
isCurved: true,
isStrokeCapRound: true,
isStrokeJoinRound: true,
color:
Theme.of(context).colorScheme.primary,
belowBarData: BarAreaData(
show: true,
gradient: LinearGradient(
colors: List.filled(
_dailySignRecords.length,
Theme.of(context)
.colorScheme
.primary
.withOpacity(0.3),
).toList(),
),
),
spots: _dailySignRecords
.map(
(x) => FlSpot(
x.createdAt
.copyWith(
hour: 0,
minute: 0,
second: 0,
millisecond: 0,
microsecond: 0,
)
.millisecondsSinceEpoch
.toDouble(),
x.resultTier.toDouble(),
),
)
.toList(),
)
],
lineTouchData: LineTouchData(
touchTooltipData: LineTouchTooltipData(
getTooltipItems: (spots) => spots
.map((spot) => LineTooltipItem(
'${DailySignHistoryChartDialog.signSymbols[spot.y.toInt()]}\n${DateFormat('MM/dd').format(DateTime.fromMillisecondsSinceEpoch(spot.x.toInt()))}',
TextStyle(
color: Theme.of(context)
.colorScheme
.onSurface,
),
))
.toList(),
getTooltipColor: (_) => Theme.of(context)
.colorScheme
.surfaceContainerHigh,
),
),
titlesData: FlTitlesData(
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 40,
interval: 1,
getTitlesWidget: (value, _) => Align(
alignment: Alignment.centerRight,
child: Text(
DailySignHistoryChartDialog
.signSymbols[value.toInt()],
textAlign: TextAlign.right,
).paddingOnly(right: 8),
),
),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 28,
interval: 86400000,
getTitlesWidget: (value, _) => Text(
DateFormat('dd').format(
DateTime.fromMillisecondsSinceEpoch(
value.toInt(),
),
),
textAlign: TextAlign.center,
).paddingOnly(top: 8),
),
),
),
gridData: const FlGridData(show: false),
borderData: FlBorderData(show: false),
),
),
).marginOnly(
right: 24, left: 12, bottom: 8, top: 24),
)
],
),
), ),
], ],
), ),
@ -373,8 +512,9 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
), ),
CenteredContainer( CenteredContainer(
child: RefreshIndicator( child: RefreshIndicator(
onRefresh: () => onRefresh: () => Future.sync(
Future.sync(() => _albumPagingController.refresh()), () => _albumPagingController.refresh(),
),
child: PagedGridView<int, Attachment>( child: PagedGridView<int, Attachment>(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
pagingController: _albumPagingController, pagingController: _albumPagingController,

View File

@ -13,6 +13,7 @@ import 'package:solian/providers/relation.dart';
import 'package:solian/providers/websocket.dart'; import 'package:solian/providers/websocket.dart';
import 'package:solian/services.dart'; import 'package:solian/services.dart';
import 'package:solian/widgets/sized_container.dart'; import 'package:solian/widgets/sized_container.dart';
import 'package:url_launcher/url_launcher_string.dart';
class SignInScreen extends StatefulWidget { class SignInScreen extends StatefulWidget {
const SignInScreen({super.key}); const SignInScreen({super.key});
@ -167,7 +168,6 @@ class _SignInScreenState extends State<SignInScreen> {
final result = AuthResult.fromJson(resp.body); final result = AuthResult.fromJson(resp.body);
_currentTicket = result.ticket; _currentTicket = result.ticket;
_passwordController.clear();
// Finish sign in if possible // Finish sign in if possible
if (result.isFinished) { if (result.isFinished) {
@ -185,11 +185,13 @@ class _SignInScreenState extends State<SignInScreen> {
autoStartBackgroundNotificationService(); autoStartBackgroundNotificationService();
Navigator.pop(context, true); Navigator.pop(context, true);
_passwordController.clear();
}); });
} else { } else {
// Skip the first step // Skip the first step
_factorPicked = null; _factorPicked = null;
_factorPickedType = null; _factorPickedType = null;
_passwordController.clear();
setState(() => _period += 2); setState(() => _period += 2);
} }
} catch (e) { } catch (e) {
@ -210,9 +212,8 @@ class _SignInScreenState extends State<SignInScreen> {
case 2: case 2:
_passwordController.clear(); _passwordController.clear();
_factorPickedType = null; _factorPickedType = null;
default:
setState(() => _period--);
} }
setState(() => _period--);
} }
@override @override
@ -235,16 +236,18 @@ class _SignInScreenState extends State<SignInScreen> {
); );
}, },
child: switch (_period % 3) { child: switch (_period % 3) {
1 => Column( 1 => ListView(
shrinkWrap: true,
key: const ValueKey<int>(1), key: const ValueKey<int>(1),
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
ClipRRect( Align(
borderRadius: const BorderRadius.all(Radius.circular(8)), alignment: Alignment.centerLeft,
child: child: ClipRRect(
Image.asset('assets/logo.png', width: 64, height: 64), borderRadius: const BorderRadius.all(Radius.circular(8)),
).paddingOnly(bottom: 8, left: 4), child:
Image.asset('assets/logo.png', width: 64, height: 64),
).paddingOnly(bottom: 8, left: 4),
),
Text( Text(
'signinPickFactor'.tr, 'signinPickFactor'.tr,
style: const TextStyle( style: const TextStyle(
@ -323,16 +326,18 @@ class _SignInScreenState extends State<SignInScreen> {
), ),
], ],
), ),
2 => Column( 2 => ListView(
key: const ValueKey<int>(2), key: const ValueKey<int>(2),
mainAxisAlignment: MainAxisAlignment.center, shrinkWrap: true,
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
ClipRRect( Align(
borderRadius: const BorderRadius.all(Radius.circular(8)), alignment: Alignment.centerLeft,
child: child: ClipRRect(
Image.asset('assets/logo.png', width: 64, height: 64), borderRadius: const BorderRadius.all(Radius.circular(8)),
).paddingOnly(bottom: 8, left: 4), child:
Image.asset('assets/logo.png', width: 64, height: 64),
).paddingOnly(bottom: 8, left: 4),
),
Text( Text(
'signinEnterPassword'.tr, 'signinEnterPassword'.tr,
style: const TextStyle( style: const TextStyle(
@ -396,16 +401,18 @@ class _SignInScreenState extends State<SignInScreen> {
), ),
], ],
), ),
_ => Column( _ => ListView(
key: const ValueKey<int>(0), key: const ValueKey<int>(0),
mainAxisAlignment: MainAxisAlignment.center, shrinkWrap: true,
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
ClipRRect( Align(
borderRadius: const BorderRadius.all(Radius.circular(8)), alignment: Alignment.centerLeft,
child: child: ClipRRect(
Image.asset('assets/logo.png', width: 64, height: 64), borderRadius: const BorderRadius.all(Radius.circular(8)),
).paddingOnly(bottom: 8, left: 4), child:
Image.asset('assets/logo.png', width: 64, height: 64),
).paddingOnly(bottom: 8, left: 4),
),
Text( Text(
'signinGreeting'.tr, 'signinGreeting'.tr,
style: const TextStyle( style: const TextStyle(
@ -451,11 +458,50 @@ class _SignInScreenState extends State<SignInScreen> {
), ),
], ],
), ),
const Gap(12),
Align(
alignment: Alignment.centerRight,
child: Container(
constraints: const BoxConstraints(maxWidth: 290),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'termAcceptNextWithAgree'.tr,
textAlign: TextAlign.end,
style:
Theme.of(context).textTheme.bodySmall!.copyWith(
color: Theme.of(context)
.colorScheme
.onSurface
.withOpacity(0.75),
),
),
Material(
color: Colors.transparent,
child: InkWell(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text('termAcceptLink'.tr),
const Gap(4),
const Icon(Icons.launch, size: 14),
],
),
onTap: () {
launchUrlString('https://solsynth.dev/terms');
},
),
),
],
),
).paddingSymmetric(horizontal: 16),
),
], ],
), ),
}, },
), ),
), ).paddingAll(24),
); );
} }
} }

View File

@ -4,6 +4,7 @@ import 'package:get/get.dart';
import 'package:solian/exts.dart'; import 'package:solian/exts.dart';
import 'package:solian/services.dart'; import 'package:solian/services.dart';
import 'package:solian/widgets/sized_container.dart'; import 'package:solian/widgets/sized_container.dart';
import 'package:url_launcher/url_launcher_string.dart';
class SignUpScreen extends StatefulWidget { class SignUpScreen extends StatefulWidget {
const SignUpScreen({super.key}); const SignUpScreen({super.key});
@ -18,7 +19,7 @@ class _SignUpScreenState extends State<SignUpScreen> {
final _nicknameController = TextEditingController(); final _nicknameController = TextEditingController();
final _passwordController = TextEditingController(); final _passwordController = TextEditingController();
void performAction(BuildContext context) async { void _performAction(BuildContext context) async {
final email = _emailController.value.text; final email = _emailController.value.text;
final username = _usernameController.value.text; final username = _usernameController.value.text;
final nickname = _nicknameController.value.text; final nickname = _nicknameController.value.text;
@ -60,20 +61,24 @@ class _SignUpScreenState extends State<SignUpScreen> {
} }
} }
bool _isTermAccepted = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Material( return Material(
color: Theme.of(context).colorScheme.surface, color: Theme.of(context).colorScheme.surface,
child: CenteredContainer( child: CenteredContainer(
maxWidth: 360, maxWidth: 360,
child: Column( child: ListView(
mainAxisSize: MainAxisSize.min, shrinkWrap: true,
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
ClipRRect( Align(
borderRadius: const BorderRadius.all(Radius.circular(8)), alignment: Alignment.centerLeft,
child: Image.asset('assets/logo.png', width: 64, height: 64), child: ClipRRect(
).paddingOnly(bottom: 8, left: 4), borderRadius: const BorderRadius.all(Radius.circular(8)),
child: Image.asset('assets/logo.png', width: 64, height: 64),
).paddingOnly(bottom: 8, left: 4),
),
Text( Text(
'signupGreeting'.tr, 'signupGreeting'.tr,
style: const TextStyle( style: const TextStyle(
@ -136,12 +141,61 @@ class _SignUpScreenState extends State<SignUpScreen> {
), ),
onTapOutside: (_) => onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(), FocusManager.instance.primaryFocus?.unfocus(),
onSubmitted: (_) => performAction(context), onSubmitted: (_) => _performAction(context),
),
const Gap(8),
CheckboxListTile(
value: _isTermAccepted,
title: Text(
'termAccept'.tr,
style: const TextStyle(height: 1.2),
).paddingOnly(bottom: 4),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(8),
),
),
subtitle: RichText(
text: TextSpan(
style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: Theme.of(context)
.colorScheme
.onSurface
.withOpacity(0.75),
),
children: [
TextSpan(text: 'termAcceptDesc'.tr),
WidgetSpan(
child: Material(
color: Colors.transparent,
child: InkWell(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text('termAcceptLink'.tr),
const Gap(4),
const Icon(Icons.launch, size: 14),
],
),
onTap: () {
launchUrlString('https://solsynth.dev/terms');
},
),
),
),
],
),
),
onChanged: (value) {
setState(() => _isTermAccepted = value ?? false);
},
), ),
const Gap(16), const Gap(16),
Align( Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: TextButton( child: TextButton(
onPressed:
!_isTermAccepted ? null : () => _performAction(context),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@ -149,12 +203,11 @@ class _SignUpScreenState extends State<SignUpScreen> {
const Icon(Icons.chevron_right), const Icon(Icons.chevron_right),
], ],
), ),
onPressed: () => performAction(context),
), ),
) )
], ],
), ),
), ).paddingAll(24),
); );
} }
} }

View File

@ -88,7 +88,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
Future<void> _pullDaily() async { Future<void> _pullDaily() async {
try { try {
_signRecord = await _dailySign.getToday(); _signRecord = await _dailySign.getToday();
_dailySign.listLastRecord(30).then((value) { _dailySign.listLastRecord(14).then((value) {
setState(() => _signRecordHistory = value); setState(() => _signRecordHistory = value);
}); });
} catch (e) { } catch (e) {
@ -103,7 +103,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
try { try {
_signRecord = await _dailySign.signToday(); _signRecord = await _dailySign.signToday();
_dailySign.listLastRecord(30).then((value) { _dailySign.listLastRecord(14).then((value) {
setState(() => _signRecordHistory = value); setState(() => _signRecordHistory = value);
}); });
} catch (e) { } catch (e) {

View File

@ -2,12 +2,15 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:solian/exceptions/request.dart';
import 'package:solian/exts.dart'; import 'package:solian/exts.dart';
import 'package:solian/platform.dart'; import 'package:solian/platform.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/providers/database/database.dart'; import 'package:solian/providers/database/database.dart';
import 'package:solian/providers/theme_switcher.dart'; import 'package:solian/providers/theme_switcher.dart';
import 'package:solian/router.dart'; import 'package:solian/router.dart';
import 'package:solian/theme.dart'; import 'package:solian/theme.dart';
import 'package:solian/widgets/reports/abuse_report.dart';
class SettingScreen extends StatefulWidget { class SettingScreen extends StatefulWidget {
const SettingScreen({super.key}); const SettingScreen({super.key});
@ -129,6 +132,54 @@ class _SettingScreenState extends State<SettingScreen> {
}); });
}, },
), ),
Obx(() {
final AuthProvider auth = Get.find<AuthProvider>();
if (!auth.isAuthorized.value) return const SizedBox.shrink();
return Column(
children: [
_buildCaptionHeader('account'.tr),
ListTile(
leading: const Icon(Icons.flag),
trailing: const Icon(Icons.chevron_right),
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
title: Text('reportAbuse'.tr),
subtitle: Text('reportAbuseDesc'.tr),
onTap: () {
showDialog(
context: context,
builder: (context) => const AbuseReportDialog(),
);
},
),
ListTile(
leading: const Icon(Icons.person_remove),
trailing: const Icon(Icons.chevron_right),
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
title: Text('accountDeletion'.tr),
subtitle: Text('accountDeletionDesc'.tr),
onTap: () {
context
.showSlideToConfirmDialog(
'accountDeletionConfirm'.tr,
'accountDeletionConfirmDesc'.trParams({
'account': '@${auth.userProfile.value!['name']}',
}),
)
.then((value) async {
if (value != true) return;
final client = await auth.configureClient('id');
final resp = await client.post('/users/me/deletion', {});
if (resp.statusCode != 200) {
context.showErrorDialog(RequestException(resp));
} else {
context.showSnackbar('accountDeletionRequested'.tr);
}
});
},
),
],
);
}),
_buildCaptionHeader('more'.tr), _buildCaptionHeader('more'.tr),
ListTile( ListTile(
leading: const Icon(Icons.delete_sweep), leading: const Icon(Icons.delete_sweep),

View File

@ -35,6 +35,9 @@ abstract class AppTheme {
brightness: brightness, brightness: brightness,
seedColor: seedColor ?? const Color.fromRGBO(154, 98, 91, 1), seedColor: seedColor ?? const Color.fromRGBO(154, 98, 91, 1),
), ),
snackBarTheme: const SnackBarThemeData(
behavior: SnackBarBehavior.floating,
),
fontFamily: 'Comfortaa', fontFamily: 'Comfortaa',
fontFamilyFallback: [ fontFamilyFallback: [
'NotoSansSC', 'NotoSansSC',

View File

@ -21,6 +21,7 @@ class AttachmentItem extends StatefulWidget {
final bool showBadge; final bool showBadge;
final bool showHideButton; final bool showHideButton;
final bool autoload; final bool autoload;
final bool isDense;
final BoxFit fit; final BoxFit fit;
final String? badge; final String? badge;
final Function? onHide; final Function? onHide;
@ -34,6 +35,7 @@ class AttachmentItem extends StatefulWidget {
this.showBadge = true, this.showBadge = true,
this.showHideButton = true, this.showHideButton = true,
this.autoload = false, this.autoload = false,
this.isDense = false,
this.onHide, this.onHide,
}); });
@ -53,6 +55,7 @@ class _AttachmentItemState extends State<AttachmentItem> {
fit: widget.fit, fit: widget.fit,
showBadge: widget.showBadge, showBadge: widget.showBadge,
showHideButton: widget.showHideButton, showHideButton: widget.showHideButton,
isDense: widget.isDense,
onHide: widget.onHide, onHide: widget.onHide,
); );
case 'video': case 'video':
@ -120,6 +123,7 @@ class _AttachmentItemImage extends StatelessWidget {
final bool showBadge; final bool showBadge;
final bool showHideButton; final bool showHideButton;
final BoxFit fit; final BoxFit fit;
final bool isDense;
final String? badge; final String? badge;
final Function? onHide; final Function? onHide;
@ -128,6 +132,7 @@ class _AttachmentItemImage extends StatelessWidget {
required this.item, required this.item,
required this.showBadge, required this.showBadge,
required this.showHideButton, required this.showHideButton,
required this.isDense,
required this.fit, required this.fit,
this.badge, this.badge,
this.onHide, this.onHide,
@ -146,6 +151,7 @@ class _AttachmentItemImage extends StatelessWidget {
'/attachments/${item.rid}', '/attachments/${item.rid}',
), ),
fit: fit, fit: fit,
isDense: isDense,
), ),
if (showBadge && badge != null) if (showBadge && badge != null)
Positioned( Positioned(

View File

@ -338,6 +338,7 @@ class AttachmentListEntry extends StatelessWidget {
badge: showBadge ? badgeContent : null, badge: showBadge ? badgeContent : null,
showHideButton: !item!.isMature || showMature, showHideButton: !item!.isMature || showMature,
autoload: autoload, autoload: autoload,
isDense: isDense,
onHide: () { onHide: () {
onReveal(false); onReveal(false);
}, },

View File

@ -12,7 +12,7 @@ class DailySignHistoryChartDialog extends StatelessWidget {
const DailySignHistoryChartDialog({super.key, required this.data}); const DailySignHistoryChartDialog({super.key, required this.data});
static List<String> signSymbols = ['大凶', '', '中平', '', '大吉']; static final List<String> signSymbols = ['大凶', '', '中平', '', '大吉'];
DateTime? get _firstRecordDate => data?.map((x) => x.createdAt).reduce( DateTime? get _firstRecordDate => data?.map((x) => x.createdAt).reduce(
(a, b) => DateTime.fromMillisecondsSinceEpoch( (a, b) => DateTime.fromMillisecondsSinceEpoch(
@ -42,215 +42,222 @@ class DailySignHistoryChartDialog extends StatelessWidget {
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
), ),
) )
: Column( : SizedBox(
mainAxisSize: MainAxisSize.min, width: double.maxFinite,
crossAxisAlignment: CrossAxisAlignment.start, child: ListView(
children: [ shrinkWrap: true,
Text( children: [
'dailySignHistoryRecent'.tr, Text(
style: Theme.of(context).textTheme.titleMedium, 'dailySignHistoryRecent'.tr,
).paddingOnly(bottom: 18), style: Theme.of(context).textTheme.titleMedium,
SizedBox( ).paddingOnly(bottom: 18),
height: 180, SizedBox(
width: max(640, MediaQuery.of(context).size.width), height: 180,
child: LineChart( width: max(640, MediaQuery.of(context).size.width),
LineChartData( child: LineChart(
lineBarsData: [ LineChartData(
LineChartBarData( lineBarsData: [
isCurved: true, LineChartBarData(
isStrokeCapRound: true, isCurved: true,
isStrokeJoinRound: true, isStrokeCapRound: true,
color: Theme.of(context).colorScheme.primary, isStrokeJoinRound: true,
belowBarData: BarAreaData( color: Theme.of(context).colorScheme.primary,
show: true, belowBarData: BarAreaData(
gradient: LinearGradient( show: true,
colors: List.filled( gradient: LinearGradient(
data!.length, colors: List.filled(
Theme.of(context) data!.length,
.colorScheme Theme.of(context)
.primary .colorScheme
.withOpacity(0.3), .primary
).toList(), .withOpacity(0.3),
), ).toList(),
),
spots: data!
.map(
(x) => FlSpot(
x.createdAt
.copyWith(
hour: 0,
minute: 0,
second: 0,
millisecond: 0,
microsecond: 0,
)
.millisecondsSinceEpoch
.toDouble(),
x.resultTier.toDouble(),
),
)
.toList(),
)
],
lineTouchData: LineTouchData(
touchTooltipData: LineTouchTooltipData(
getTooltipItems: (spots) => spots
.map((spot) => LineTooltipItem(
'${signSymbols[spot.y.toInt()]}\n${DateFormat('MM/dd').format(DateTime.fromMillisecondsSinceEpoch(spot.x.toInt()))}',
TextStyle(
color:
Theme.of(context).colorScheme.onSurface,
),
))
.toList(),
getTooltipColor: (_) =>
Theme.of(context).colorScheme.surfaceContainerHigh,
)),
titlesData: FlTitlesData(
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 40,
interval: 1,
getTitlesWidget: (value, _) => Align(
alignment: Alignment.centerRight,
child: Text(
signSymbols[value.toInt()],
textAlign: TextAlign.right,
).paddingOnly(right: 8),
),
),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 28,
interval: 86400000,
getTitlesWidget: (value, _) => Text(
DateFormat('dd').format(
DateTime.fromMillisecondsSinceEpoch(
value.toInt(),
),
), ),
textAlign: TextAlign.center,
).paddingOnly(top: 8),
),
),
),
gridData: const FlGridData(show: false),
borderData: FlBorderData(show: false),
),
),
).marginOnly(right: 24, bottom: 8, top: 8),
const Gap(16),
Text(
'dailySignHistoryReward'.tr,
style: Theme.of(context).textTheme.titleMedium,
).paddingOnly(bottom: 18),
SizedBox(
height: 180,
width: max(640, MediaQuery.of(context).size.width),
child: LineChart(
LineChartData(
lineBarsData: [
LineChartBarData(
isCurved: true,
isStrokeCapRound: true,
isStrokeJoinRound: true,
color: Theme.of(context).colorScheme.primary,
belowBarData: BarAreaData(
show: true,
gradient: LinearGradient(
colors: List.filled(
data!.length,
Theme.of(context)
.colorScheme
.primary
.withOpacity(0.3),
).toList(),
), ),
), spots: data!
spots: data! .map(
.map( (x) => FlSpot(
(x) => FlSpot( x.createdAt
x.createdAt .copyWith(
.copyWith( hour: 0,
hour: 0, minute: 0,
minute: 0, second: 0,
second: 0, millisecond: 0,
millisecond: 0, microsecond: 0,
microsecond: 0, )
) .millisecondsSinceEpoch
.millisecondsSinceEpoch .toDouble(),
.toDouble(), x.resultTier.toDouble(),
x.resultExperience.toDouble(),
),
)
.toList(),
)
],
lineTouchData: LineTouchData(
touchTooltipData: LineTouchTooltipData(
getTooltipItems: (spots) => spots
.map((spot) => LineTooltipItem(
'+${spot.y.toStringAsFixed(0)} EXP\n${DateFormat('MM/dd').format(DateTime.fromMillisecondsSinceEpoch(spot.x.toInt()))}',
TextStyle(
color:
Theme.of(context).colorScheme.onSurface,
), ),
)) )
.toList(), .toList(),
getTooltipColor: (_) => )
Theme.of(context).colorScheme.surfaceContainerHigh, ],
)), lineTouchData: LineTouchData(
titlesData: FlTitlesData( touchTooltipData: LineTouchTooltipData(
topTitles: const AxisTitles( getTooltipItems: (spots) => spots
sideTitles: SideTitles(showTitles: false), .map((spot) => LineTooltipItem(
'${signSymbols[spot.y.toInt()]}\n${DateFormat('MM/dd').format(DateTime.fromMillisecondsSinceEpoch(spot.x.toInt()))}',
TextStyle(
color: Theme.of(context)
.colorScheme
.onSurface,
),
))
.toList(),
getTooltipColor: (_) => Theme.of(context)
.colorScheme
.surfaceContainerHigh,
),
), ),
rightTitles: const AxisTitles( titlesData: FlTitlesData(
sideTitles: SideTitles(showTitles: false), topTitles: const AxisTitles(
), sideTitles: SideTitles(showTitles: false),
leftTitles: AxisTitles( ),
sideTitles: SideTitles( rightTitles: const AxisTitles(
showTitles: true, sideTitles: SideTitles(showTitles: false),
reservedSize: 40, ),
getTitlesWidget: (value, _) => Align( leftTitles: AxisTitles(
alignment: Alignment.centerRight, sideTitles: SideTitles(
child: Text( showTitles: true,
value.toStringAsFixed(0), reservedSize: 40,
textAlign: TextAlign.right, interval: 1,
).paddingOnly(right: 8), getTitlesWidget: (value, _) => Align(
alignment: Alignment.centerRight,
child: Text(
signSymbols[value.toInt()],
textAlign: TextAlign.right,
).paddingOnly(right: 8),
),
),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 28,
interval: 86400000,
getTitlesWidget: (value, _) => Text(
DateFormat('dd').format(
DateTime.fromMillisecondsSinceEpoch(
value.toInt(),
),
),
textAlign: TextAlign.center,
).paddingOnly(top: 8),
), ),
), ),
), ),
bottomTitles: AxisTitles( gridData: const FlGridData(show: false),
sideTitles: SideTitles( borderData: FlBorderData(show: false),
showTitles: true, ),
reservedSize: 28, ),
interval: 86400000, ).marginOnly(right: 24, bottom: 8, top: 8),
getTitlesWidget: (value, _) => Text( const Gap(16),
DateFormat('dd').format( Text(
DateTime.fromMillisecondsSinceEpoch( 'dailySignHistoryReward'.tr,
value.toInt(), style: Theme.of(context).textTheme.titleMedium,
), ).paddingOnly(bottom: 18),
SizedBox(
height: 180,
width: max(640, MediaQuery.of(context).size.width),
child: LineChart(
LineChartData(
lineBarsData: [
LineChartBarData(
isCurved: true,
isStrokeCapRound: true,
isStrokeJoinRound: true,
color: Theme.of(context).colorScheme.primary,
belowBarData: BarAreaData(
show: true,
gradient: LinearGradient(
colors: List.filled(
data!.length,
Theme.of(context)
.colorScheme
.primary
.withOpacity(0.3),
).toList(),
), ),
textAlign: TextAlign.center, ),
).paddingOnly(top: 8), spots: data!
.map(
(x) => FlSpot(
x.createdAt
.copyWith(
hour: 0,
minute: 0,
second: 0,
millisecond: 0,
microsecond: 0,
)
.millisecondsSinceEpoch
.toDouble(),
x.resultExperience.toDouble(),
),
)
.toList(),
)
],
lineTouchData: LineTouchData(
touchTooltipData: LineTouchTooltipData(
getTooltipItems: (spots) => spots
.map((spot) => LineTooltipItem(
'+${spot.y.toStringAsFixed(0)} EXP\n${DateFormat('MM/dd').format(DateTime.fromMillisecondsSinceEpoch(spot.x.toInt()))}',
TextStyle(
color: Theme.of(context)
.colorScheme
.onSurface,
),
))
.toList(),
getTooltipColor: (_) => Theme.of(context)
.colorScheme
.surfaceContainerHigh,
)),
titlesData: FlTitlesData(
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 40,
getTitlesWidget: (value, _) => Align(
alignment: Alignment.centerRight,
child: Text(
value.toStringAsFixed(0),
textAlign: TextAlign.right,
).paddingOnly(right: 8),
),
),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 28,
interval: 86400000,
getTitlesWidget: (value, _) => Text(
DateFormat('dd').format(
DateTime.fromMillisecondsSinceEpoch(
value.toInt(),
),
),
textAlign: TextAlign.center,
).paddingOnly(top: 8),
),
), ),
), ),
gridData: const FlGridData(show: false),
borderData: FlBorderData(show: false),
), ),
gridData: const FlGridData(show: false),
borderData: FlBorderData(show: false),
), ),
), ).marginOnly(right: 24, bottom: 8, top: 8),
).marginOnly(right: 24, bottom: 8, top: 8), ],
], ),
), ),
); );
} }

View File

@ -74,9 +74,13 @@ class LinkExpansion extends StatelessWidget {
), ),
).paddingOnly(right: 8), ).paddingOnly(right: 8),
if (snapshot.data!.siteName != null) if (snapshot.data!.siteName != null)
Text( Expanded(
snapshot.data!.siteName!, child: Text(
style: Theme.of(context).textTheme.labelLarge, snapshot.data!.siteName!,
style: Theme.of(context).textTheme.labelLarge,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
), ),
], ],
).paddingOnly( ).paddingOnly(

View File

@ -12,6 +12,7 @@ import 'package:solian/platform.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/router.dart'; import 'package:solian/router.dart';
import 'package:solian/screens/posts/post_editor.dart'; import 'package:solian/screens/posts/post_editor.dart';
import 'package:solian/widgets/reports/abuse_report.dart';
class PostAction extends StatefulWidget { class PostAction extends StatefulWidget {
final Post item; final Post item;
@ -149,6 +150,23 @@ class _PostActionState extends State<PostAction> {
Navigator.pop(context); Navigator.pop(context);
}, },
), ),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Icons.flag),
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) if (!widget.noReact)
ListTile( ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24), contentPadding: const EdgeInsets.symmetric(horizontal: 24),

View File

@ -313,7 +313,7 @@ class _PostItemState extends State<PostItem> {
attachmentsId: attachments, attachmentsId: attachments,
autoload: false, autoload: false,
isColumn: true, isColumn: true,
).paddingOnly(left: 60, right: 24); ).paddingOnly(left: 60, right: 24, top: 4, bottom: 4);
} else { } else {
return AttachmentList( return AttachmentList(
flatMaxHeight: MediaQuery.of(context).size.width, flatMaxHeight: MediaQuery.of(context).size.width,

View File

@ -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<AbuseReportDialog> createState() => _AbuseReportDialogState();
}
class _AbuseReportDialogState extends State<AbuseReportDialog> {
final TextEditingController _resourceController = TextEditingController();
final TextEditingController _reasonController = TextEditingController();
bool _isBusy = false;
Future<void> _submit() async {
final auth = Get.find<AuthProvider>();
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),
),
],
);
}
}

View File

@ -158,7 +158,7 @@ PODS:
- GoogleUtilities/UserDefaults (8.0.2): - GoogleUtilities/UserDefaults (8.0.2):
- GoogleUtilities/Logger - GoogleUtilities/Logger
- GoogleUtilities/Privacy - GoogleUtilities/Privacy
- livekit_client (2.2.5): - livekit_client (2.2.6):
- FlutterMacOS - FlutterMacOS
- WebRTC-SDK (= 125.6422.04) - WebRTC-SDK (= 125.6422.04)
- macos_window_utils (1.0.0): - macos_window_utils (1.0.0):
@ -359,7 +359,7 @@ SPEC CHECKSUMS:
GoogleAppMeasurement: 6e49ffac7d3f2c3ded9cc663f912a13b67bbd0de GoogleAppMeasurement: 6e49ffac7d3f2c3ded9cc663f912a13b67bbd0de
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
livekit_client: be04a950a4b84b9dbc87507ffad5154fe75fa067 livekit_client: 98d09566e3a936b3402be8091ec3845556d36800
macos_window_utils: 933f91f64805e2eb91a5bd057cf97cd097276663 macos_window_utils: 933f91f64805e2eb91a5bd057cf97cd097276663
media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82 media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82
media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5 media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5

View File

@ -22,6 +22,14 @@ packages:
description: dart description: dart
source: sdk source: sdk
version: "0.3.2" version: "0.3.2"
action_slider:
dependency: "direct main"
description:
name: action_slider
sha256: fad0720cde9bf06c12594c15da17dba087556a3285875a91aee3d3a64a3072e2
url: "https://pub.dev"
source: hosted
version: "0.7.0"
analyzer: analyzer:
dependency: transitive dependency: transitive
description: description:
@ -330,10 +338,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: desktop_drop name: desktop_drop
sha256: d55a010fe46c8e8fcff4ea4b451a9ff84a162217bdb3b2a0aa1479776205e15d sha256: "03abf1c0443afdd1d65cf8fa589a2f01c67a11da56bbb06f6ea1de79d5628e94"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.4.4" version: "0.5.0"
device_info_plus: device_info_plus:
dependency: "direct main" dependency: "direct main"
description: description:
@ -639,10 +647,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_app_update name: flutter_app_update
sha256: "2b83278d5cc807f543e623d5b466216316104335a4918d9cc4556f39985fe84a" sha256: "3650f57571e9f05d51f008f3fc9d556351910348f8011de7734b56fa74ccfee6"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.0" version: "3.1.1"
flutter_background_service: flutter_background_service:
dependency: "direct main" dependency: "direct main"
description: description:
@ -743,10 +751,10 @@ packages:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: flutter_launcher_icons name: flutter_launcher_icons
sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" sha256: a38f2f1b3c373d42bf08bd17d60e20d3c73abce7727607b4d085ec7d5acaa294
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.13.1" version: "0.14.0"
flutter_lints: flutter_lints:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -759,10 +767,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_local_notifications name: flutter_local_notifications
sha256: c500d5d9e7e553f06b61877ca6b9c8b92c570a4c8db371038702e8ce57f8a50f sha256: "49eeef364fddb71515bc78d5a8c51435a68bccd6e4d68e25a942c5e47761ae71"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "17.2.2" version: "17.2.3"
flutter_local_notifications_linux: flutter_local_notifications_linux:
dependency: transitive dependency: transitive
description: description:
@ -1201,10 +1209,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: livekit_client name: livekit_client
sha256: "5df9b6f153b5f2c59fbf116b41e54597dfe8b2340b6630f7d8869887a9e58f44" sha256: "449f1f4f7688cc0d27a466d5b78c8973ec4bf2bbe93f79441f4fd118ecea61d7"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.5" version: "2.2.6"
logging: logging:
dependency: transitive dependency: transitive
description: description:

View File

@ -2,7 +2,7 @@ name: solian
description: "The Solar Network App" description: "The Solar Network App"
publish_to: "none" publish_to: "none"
version: 1.2.2+2 version: 1.2.3+1
environment: environment:
sdk: ">=3.3.4 <4.0.0" sdk: ">=3.3.4 <4.0.0"
@ -43,7 +43,7 @@ dependencies:
protocol_handler: ^0.2.0 protocol_handler: ^0.2.0
markdown: ^7.2.2 markdown: ^7.2.2
pasteboard: ^0.3.0 pasteboard: ^0.3.0
desktop_drop: ^0.4.4 desktop_drop: ^0.5.0
badges: ^3.1.2 badges: ^3.1.2
flutter_card_swiper: ^7.0.1 flutter_card_swiper: ^7.0.1
dismissible_page: ^1.0.2 dismissible_page: ^1.0.2
@ -82,13 +82,14 @@ dependencies:
flutter_local_notifications: ^17.2.2 flutter_local_notifications: ^17.2.2
flutter_app_update: ^3.1.0 flutter_app_update: ^3.1.0
version: ^3.0.2 version: ^3.0.2
action_slider: ^0.7.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter_lints: ^4.0.0 flutter_lints: ^4.0.0
flutter_launcher_icons: ^0.13.1 flutter_launcher_icons: ^0.14.0
build_runner: ^2.4.12 build_runner: ^2.4.12
flutter_native_splash: ^2.4.1 flutter_native_splash: ^2.4.1