Compare commits

..

16 Commits

Author SHA1 Message Date
e1ddd22e4e 🚀 Launch 1.2.3+2 2024-09-23 23:34:40 +08:00
22b2ae32e9 Featured replies clickable 2024-09-23 23:34:25 +08:00
9d5c452eae 🐛 Fix overflow in content 2024-09-23 23:20:01 +08:00
0fdb1e4ead 💫 Improve loading image animation 2024-09-23 23:19:52 +08:00
724bd6592e 💄 Improvements and optimize UX 2024-09-23 22:43:13 +08:00
2d347e0d41 ♻️ Refactored post item widget 2024-09-23 22:43:02 +08:00
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
31 changed files with 1727 additions and 993 deletions

View File

@ -22,9 +22,9 @@
"explore": "Explore",
"posts": "Posts",
"unlink": "Unlink",
"feedSearch": "Search Feed",
"feedSearchWithTag": "Searching with tag #@key",
"feedSearchWithCategory": "Searching in category @category",
"postSearch": "Search Post",
"postSearchWithTag": "Searching with tag #@key",
"postSearchWithCategory": "Searching in category @category",
"feedUnreadCount": "@count posts you may missed",
"messages": "Messages",
"messagesUnreadCount": "@count messages unread",
@ -428,9 +428,30 @@
"preferencesApplied": "Preferences has been applied.",
"save": "Save",
"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",
"updateCheckStrictly": "Strict mode",
"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

@ -32,9 +32,9 @@
"dashboard": "仪表盘",
"today": "今日",
"yesterday": "昨日",
"feedSearch": "搜索资讯",
"feedSearchWithTag": "检索带有 #@key 标签的资讯",
"feedSearchWithCategory": "检索位于分类 @category 的资讯",
"postSearch": "搜索帖子",
"postSearchWithTag": "检索带有 #@key 标签的资讯",
"postSearchWithCategory": "检索位于分类 @category 的资讯",
"feedUnreadCount": "@count 条你可能错过的帖子",
"messages": "消息",
"messagesUnreadCount": "@count 条未读的消息",
@ -428,5 +428,26 @@
"update": "更新",
"updateCheckStrictly": "严格模式",
"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)
- image_picker_ios (0.0.1):
- Flutter
- livekit_client (2.2.5):
- livekit_client (2.2.6):
- Flutter
- WebRTC-SDK (= 125.6422.04)
- media_kit_libs_ios_video (1.0.4):
@ -482,7 +482,7 @@ SPEC CHECKSUMS:
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
image_cropper: 37d40f62177c101ff4c164906d259ea2c3aa70cf
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
livekit_client: 9c8080879256a0fb16da13c9be4845248209d896
livekit_client: 20e01637431bc108dad451c8a11c1d206e1dd2cd
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
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:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:solian/exceptions/request.dart';
import 'package:solian/exts.dart';
import 'package:solian/platform.dart';
import 'package:solian/providers/auth.dart';
@ -41,38 +42,7 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
final Completer _bootCompleter = Completer();
late final List<({String label, Future<void> Function() action})> _periods = [
(
label: 'bsLoadingTheme',
action: () async {
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) {
void _updateNow(String localVersionString, String remoteVersionString) {
context
.showConfirmDialog(
'updateAvailable'.tr,
@ -92,22 +62,70 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
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 {
setState(() {
_isErrored = true;
_subtitle = 'bsCheckForUpdateDesc'.tr;
});
context.showInfoDialog(
'updateAvailable'.tr,
'bsCheckForUpdateDesc'.tr,
);
}
} else if (remoteVersionString != localVersionString) {
_bootCompleter.future.then((_) {
context.showSnackbar('updateMayAvailable'.trParams({
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 = [
(
label: 'bsLoadingTheme',
action: () async {
await context.read<ThemeSwitcher>().restoreTheme();
},
),
(
@ -207,6 +225,7 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
void initState() {
super.initState();
_runPeriods();
_checkForUpdate();
}
@override

View File

@ -1,6 +1,8 @@
import 'dart:math' as math;
import 'package:action_slider/action_slider.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:get/get.dart';
import 'package:solian/exceptions/request.dart';
import 'package:solian/exceptions/unauthorized.dart';
@ -73,6 +75,47 @@ extension AppExtensions on BuildContext {
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) {
Widget content = Text(exception.toString().capitalize!);
if (exception is UnauthorizedException) {

View File

@ -23,7 +23,7 @@ import 'package:solian/screens/realms.dart';
import 'package:solian/screens/realms/realm_detail.dart';
import 'package:solian/screens/realms/realm_organize.dart';
import 'package:solian/screens/realms/realm_view.dart';
import 'package:solian/screens/feed.dart';
import 'package:solian/screens/explore.dart';
import 'package:solian/screens/posts/post_editor.dart';
import 'package:solian/screens/settings.dart';
import 'package:solian/shells/root_shell.dart';
@ -78,13 +78,18 @@ abstract class AppRouter {
builder: (context, state, child) => child,
routes: [
GoRoute(
path: '/feed',
name: 'feed',
builder: (context, state) => const FeedScreen(),
path: '/explore',
name: 'explore',
builder: (context, state) => const ExploreScreen(),
),
GoRoute(
path: '/feed/search',
name: 'feedSearch',
path: '/drafts',
name: 'draftBox',
builder: (context, state) => const DraftBoxScreen(),
),
GoRoute(
path: '/posts/search',
name: 'postSearch',
builder: (context, state) => TitleShell(
state: state,
child: FeedSearchScreen(
@ -93,11 +98,6 @@ abstract class AppRouter {
),
),
),
GoRoute(
path: '/drafts',
name: 'draftBox',
builder: (context, state) => const DraftBoxScreen(),
),
GoRoute(
path: '/posts/view/:id',
name: 'postDetail',

View File

@ -1,6 +1,8 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:get/get.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';
class AboutScreen extends StatelessWidget {
@ -47,32 +49,52 @@ class AboutScreen extends StatelessWidget {
),
Text('Copyright © ${DateTime.now().year} Solsynth LLC'),
const Gap(16),
CenteredContainer(
maxWidth: 280,
child: Wrap(
spacing: 8,
runSpacing: 8,
alignment: WrapAlignment.center,
children: [
TextButton(
style: denseButtonStyle,
child: const Text('App Details'),
child: Text('appDetails'.tr),
onPressed: () async {
final info = await PackageInfo.fromPlatform();
showAboutDialog(
context: context,
applicationVersion: '${info.version} (${info.buildNumber})',
applicationVersion:
'${info.version} (${info.buildNumber})',
applicationLegalese:
'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.',
'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.',
applicationIcon: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(16)),
child:
Image.asset('assets/logo.png', width: 60, height: 60),
borderRadius:
const BorderRadius.all(Radius.circular(16)),
child: Image.asset('assets/logo.png',
width: 60, height: 60),
),
);
},
),
TextButton(
style: denseButtonStyle,
child: const Text('Project Website'),
child: Text('projectWebsite'.tr),
onPressed: () {
launchUrlString('https://solsynth.dev/products/solar-network');
launchUrlString(
'https://solsynth.dev/products/solar-network');
},
),
TextButton(
style: denseButtonStyle,
child: Text('termRelated'.tr),
onPressed: () {
launchUrlString('https://solsynth.dev/terms');
},
),
],
),
),
const Gap(16),
const Text(
'Open-sourced under AGPLv3',

View File

@ -1,11 +1,16 @@
import 'dart:math';
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:get/get.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/exts.dart';
import 'package:solian/models/account.dart';
import 'package:solian/models/attachment.dart';
import 'package:solian/models/daily_sign.dart';
import 'package:solian/models/pagination.dart';
import 'package:solian/models/post.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/app_bar_leading.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_warped_list.dart';
import 'package:solian/widgets/sized_container.dart';
@ -45,6 +51,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
Account? _userinfo;
Subscription? _subscription;
List<Post> _pinnedPosts = List.empty();
List<DailySignRecord> _dailySignRecords = List.empty();
int _totalUpvote = 0, _totalDownvote = 0;
Future<void> _getSubscription() async {
@ -57,7 +64,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
Future<void> _getUserinfo() async {
setState(() => _isBusy = true);
var client = await ServiceFinder.configureClient('auth');
var client = await ServiceFinder.configureClient('id');
var resp = await client.get('/users/${widget.name}');
if (resp.statusCode != 200) {
context.showErrorDialog(resp.bodyString).then((_) {
@ -67,7 +74,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
_userinfo = Account.fromJson(resp.body);
}
client = await ServiceFinder.configureClient('interactive');
client = await ServiceFinder.configureClient('co');
resp = await client.get('/users/${widget.name}');
if (resp.statusCode != 200) {
context.showErrorDialog(resp.bodyString).then((_) {
@ -82,7 +89,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
}
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');
if (resp.statusCode != 200) {
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 {
return _totalUpvote * 2 - _totalDownvote + _postController.postTotal.value;
}
@ -129,6 +153,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
_getUserinfo().then((_) {
_getSubscription();
_getPinnedPosts();
_getDailySignRecords();
});
}
@ -168,7 +193,9 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
toolbarHeight: AppTheme.toolbarHeight(context),
leadingWidth: 24,
automaticallyImplyLeading: false,
flexibleSpace: Row(
flexibleSpace: SizedBox(
height: 56,
child: Row(
children: [
AppBarLeadingButton.adaptive(context) ?? const Gap(8),
const Gap(8),
@ -258,6 +285,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
),
],
),
).paddingOnly(top: MediaQuery.of(context).padding.top),
bottom: TabBar(
tabs: [
Tab(text: 'profilePage'.tr),
@ -271,10 +299,11 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
body: TabBarView(
physics: const NeverScrollableScrollPhysics(),
children: [
Column(
ListView(
children: [
const Gap(16),
AccountHeadingWidget(
CenteredContainer(
child: AccountHeadingWidget(
name: _userinfo!.name,
nick: _userinfo!.nick,
desc: _userinfo!.description,
@ -285,7 +314,117 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
.getSomeoneStatus(_userinfo!.name),
detail: _userinfo,
profile: _userinfo!.profile,
extraWidgets: const [],
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(
child: RefreshIndicator(
onRefresh: () =>
Future.sync(() => _albumPagingController.refresh()),
onRefresh: () => Future.sync(
() => _albumPagingController.refresh(),
),
child: PagedGridView<int, Attachment>(
padding: EdgeInsets.zero,
pagingController: _albumPagingController,

View File

@ -13,6 +13,7 @@ import 'package:solian/providers/relation.dart';
import 'package:solian/providers/websocket.dart';
import 'package:solian/services.dart';
import 'package:solian/widgets/sized_container.dart';
import 'package:url_launcher/url_launcher_string.dart';
class SignInScreen extends StatefulWidget {
const SignInScreen({super.key});
@ -167,7 +168,6 @@ class _SignInScreenState extends State<SignInScreen> {
final result = AuthResult.fromJson(resp.body);
_currentTicket = result.ticket;
_passwordController.clear();
// Finish sign in if possible
if (result.isFinished) {
@ -185,11 +185,13 @@ class _SignInScreenState extends State<SignInScreen> {
autoStartBackgroundNotificationService();
Navigator.pop(context, true);
_passwordController.clear();
});
} else {
// Skip the first step
_factorPicked = null;
_factorPickedType = null;
_passwordController.clear();
setState(() => _period += 2);
}
} catch (e) {
@ -210,9 +212,8 @@ class _SignInScreenState extends State<SignInScreen> {
case 2:
_passwordController.clear();
_factorPickedType = null;
default:
setState(() => _period--);
}
setState(() => _period--);
}
@override
@ -235,16 +236,18 @@ class _SignInScreenState extends State<SignInScreen> {
);
},
child: switch (_period % 3) {
1 => Column(
1 => ListView(
shrinkWrap: true,
key: const ValueKey<int>(1),
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipRRect(
Align(
alignment: Alignment.centerLeft,
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child:
Image.asset('assets/logo.png', width: 64, height: 64),
).paddingOnly(bottom: 8, left: 4),
),
Text(
'signinPickFactor'.tr,
style: const TextStyle(
@ -323,16 +326,18 @@ class _SignInScreenState extends State<SignInScreen> {
),
],
),
2 => Column(
2 => ListView(
key: const ValueKey<int>(2),
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
shrinkWrap: true,
children: [
ClipRRect(
Align(
alignment: Alignment.centerLeft,
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child:
Image.asset('assets/logo.png', width: 64, height: 64),
).paddingOnly(bottom: 8, left: 4),
),
Text(
'signinEnterPassword'.tr,
style: const TextStyle(
@ -396,16 +401,18 @@ class _SignInScreenState extends State<SignInScreen> {
),
],
),
_ => Column(
_ => ListView(
key: const ValueKey<int>(0),
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
shrinkWrap: true,
children: [
ClipRRect(
Align(
alignment: Alignment.centerLeft,
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child:
Image.asset('assets/logo.png', width: 64, height: 64),
).paddingOnly(bottom: 8, left: 4),
),
Text(
'signinGreeting'.tr,
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/services.dart';
import 'package:solian/widgets/sized_container.dart';
import 'package:url_launcher/url_launcher_string.dart';
class SignUpScreen extends StatefulWidget {
const SignUpScreen({super.key});
@ -18,7 +19,7 @@ class _SignUpScreenState extends State<SignUpScreen> {
final _nicknameController = TextEditingController();
final _passwordController = TextEditingController();
void performAction(BuildContext context) async {
void _performAction(BuildContext context) async {
final email = _emailController.value.text;
final username = _usernameController.value.text;
final nickname = _nicknameController.value.text;
@ -60,20 +61,24 @@ class _SignUpScreenState extends State<SignUpScreen> {
}
}
bool _isTermAccepted = false;
@override
Widget build(BuildContext context) {
return Material(
color: Theme.of(context).colorScheme.surface,
child: CenteredContainer(
maxWidth: 360,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
child: ListView(
shrinkWrap: true,
children: [
ClipRRect(
Align(
alignment: Alignment.centerLeft,
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: Image.asset('assets/logo.png', width: 64, height: 64),
).paddingOnly(bottom: 8, left: 4),
),
Text(
'signupGreeting'.tr,
style: const TextStyle(
@ -136,12 +141,61 @@ class _SignUpScreenState extends State<SignUpScreen> {
),
onTapOutside: (_) =>
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),
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed:
!_isTermAccepted ? null : () => _performAction(context),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
@ -149,12 +203,11 @@ class _SignUpScreenState extends State<SignUpScreen> {
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 {
try {
_signRecord = await _dailySign.getToday();
_dailySign.listLastRecord(30).then((value) {
_dailySign.listLastRecord(14).then((value) {
setState(() => _signRecordHistory = value);
});
} catch (e) {
@ -103,7 +103,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
try {
_signRecord = await _dailySign.signToday();
_dailySign.listLastRecord(30).then((value) {
_dailySign.listLastRecord(14).then((value) {
setState(() => _signRecordHistory = value);
});
} catch (e) {

View File

@ -16,14 +16,14 @@ import 'package:solian/widgets/app_bar_leading.dart';
import 'package:solian/widgets/posts/post_shuffle_swiper.dart';
import 'package:solian/widgets/posts/post_warped_list.dart';
class FeedScreen extends StatefulWidget {
const FeedScreen({super.key});
class ExploreScreen extends StatefulWidget {
const ExploreScreen({super.key});
@override
State<FeedScreen> createState() => _FeedScreenState();
State<ExploreScreen> createState() => _ExploreScreenState();
}
class _FeedScreenState extends State<FeedScreen>
class _ExploreScreenState extends State<ExploreScreen>
with SingleTickerProviderStateMixin {
late final PostListController _postController;
late final TabController _tabController;
@ -82,7 +82,7 @@ class _FeedScreenState extends State<FeedScreen>
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return [
SliverAppBar(
title: AppBarTitle('feed'.tr),
title: AppBarTitle('explore'.tr),
centerTitle: false,
floating: true,
toolbarHeight: AppTheme.toolbarHeight(context),

View File

@ -63,13 +63,13 @@ class _FeedSearchScreenState extends State<FeedSearchScreen> {
ListTile(
leading: const Icon(Icons.label),
tileColor: Theme.of(context).colorScheme.surfaceContainer,
title: Text('feedSearchWithTag'.trParams({'key': widget.tag!})),
title: Text('postSearchWithTag'.trParams({'key': widget.tag!})),
),
if (widget.category != null)
ListTile(
leading: const Icon(Icons.category),
tileColor: Theme.of(context).colorScheme.surfaceContainer,
title: Text('feedSearchWithCategory'
title: Text('postSearchWithCategory'
.trParams({'key': widget.category!})),
),
Expanded(

View File

@ -2,12 +2,15 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:solian/exceptions/request.dart';
import 'package:solian/exts.dart';
import 'package:solian/platform.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/providers/database/database.dart';
import 'package:solian/providers/theme_switcher.dart';
import 'package:solian/router.dart';
import 'package:solian/theme.dart';
import 'package:solian/widgets/reports/abuse_report.dart';
class SettingScreen extends StatefulWidget {
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),
ListTile(
leading: const Icon(Icons.delete_sweep),

View File

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

View File

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

View File

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

View File

@ -34,8 +34,17 @@ class AutoCacheImage extends StatelessWidget {
progressIndicatorBuilder: noProgressIndicator
? null
: (context, url, downloadProgress) => Center(
child: CircularProgressIndicator(
value: downloadProgress.progress,
child: TweenAnimationBuilder(
tween: Tween(
begin: 0,
end: downloadProgress.progress ?? 0,
),
duration: const Duration(milliseconds: 300),
builder: (context, value, _) => CircularProgressIndicator(
value: downloadProgress.progress != null
? value.toDouble()
: null,
),
),
),
errorWidget: noErrorWidget
@ -74,12 +83,21 @@ class AutoCacheImage extends StatelessWidget {
ImageChunkEvent? loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
child: TweenAnimationBuilder(
tween: Tween(
begin: 0,
end: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: 0,
),
duration: const Duration(milliseconds: 300),
builder: (context, value, _) => CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? value.toDouble()
: null,
),
),
);
},
errorBuilder: noErrorWidget

View File

@ -49,6 +49,7 @@ class ChatEventMessage extends StatelessWidget {
return MarkdownTextContent(
parentId: 'm${item.id}',
isSelectable: true,
isAutoWarp: true,
content: body.text,
);
}

View File

@ -12,7 +12,7 @@ class DailySignHistoryChartDialog extends StatelessWidget {
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(
(a, b) => DateTime.fromMillisecondsSinceEpoch(
@ -42,9 +42,10 @@ class DailySignHistoryChartDialog extends StatelessWidget {
child: CircularProgressIndicator(),
),
)
: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
: SizedBox(
width: double.maxFinite,
child: ListView(
shrinkWrap: true,
children: [
Text(
'dailySignHistoryRecent'.tr,
@ -98,14 +99,17 @@ class DailySignHistoryChartDialog extends StatelessWidget {
.map((spot) => LineTooltipItem(
'${signSymbols[spot.y.toInt()]}\n${DateFormat('MM/dd').format(DateTime.fromMillisecondsSinceEpoch(spot.x.toInt()))}',
TextStyle(
color:
Theme.of(context).colorScheme.onSurface,
color: Theme.of(context)
.colorScheme
.onSurface,
),
))
.toList(),
getTooltipColor: (_) =>
Theme.of(context).colorScheme.surfaceContainerHigh,
)),
getTooltipColor: (_) => Theme.of(context)
.colorScheme
.surfaceContainerHigh,
),
),
titlesData: FlTitlesData(
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
@ -201,13 +205,15 @@ class DailySignHistoryChartDialog extends StatelessWidget {
.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,
color: Theme.of(context)
.colorScheme
.onSurface,
),
))
.toList(),
getTooltipColor: (_) =>
Theme.of(context).colorScheme.surfaceContainerHigh,
getTooltipColor: (_) => Theme.of(context)
.colorScheme
.surfaceContainerHigh,
)),
titlesData: FlTitlesData(
topTitles: const AxisTitles(
@ -252,6 +258,7 @@ class DailySignHistoryChartDialog extends StatelessWidget {
).marginOnly(right: 24, bottom: 8, top: 8),
],
),
),
);
}
}

View File

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

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_markdown_selectionarea/flutter_markdown.dart';
import 'package:gap/gap.dart';
import 'package:get/get.dart';
import 'package:markdown/markdown.dart' as markdown;
import 'package:markdown/markdown.dart';
@ -15,6 +16,7 @@ class MarkdownTextContent extends StatelessWidget {
final String parentId;
final bool isSelectable;
final bool isLargeText;
final bool isAutoWarp;
const MarkdownTextContent({
super.key,
@ -22,17 +24,36 @@ class MarkdownTextContent extends StatelessWidget {
required this.parentId,
this.isSelectable = false,
this.isLargeText = false,
this.isAutoWarp = false,
});
Widget _buildContent(BuildContext context) {
final emojiRegex = RegExp(r':([-\w]+):');
final emojiMatch = emojiRegex.allMatches(content);
final isOnlyEmoji = content.replaceAll(emojiRegex, '').trim().isEmpty;
final stickerRegex = RegExp(r':([-\w]+):');
return Markdown(
// Split the content into paragraphs
final paragraphs = content.split(RegExp(r'\n\s*\n'));
// Iterate over each paragraph to process stickers individually
List<Widget> contentWidgets = [];
for (var idx = 0; idx < paragraphs.length; idx++) {
// Getting paragraph
var paragraph = paragraphs[idx];
// Auto adding new-lines
if (isAutoWarp) {
paragraph = paragraph.replaceAll('\n', '\\\n');
}
// Matching stickers
final stickerMatch = stickerRegex.allMatches(paragraph);
final isOnlySticker =
paragraph.replaceAll(stickerRegex, '').trim().isEmpty;
contentWidgets.add(
Markdown(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
data: content,
data: paragraph,
padding: EdgeInsets.zero,
styleSheet: MarkdownStyleSheet.fromTheme(
Theme.of(context),
@ -99,10 +120,12 @@ class MarkdownTextContent extends StatelessWidget {
case 'stickers':
double radius = 8;
final StickerProvider sticker = Get.find();
if (emojiMatch.length <= 1 && isOnlyEmoji) {
// Adjust sticker size based on the sticker count in this paragraph
if (stickerMatch.length <= 1 && isOnlySticker) {
width = 128;
height = 128;
} else if (emojiMatch.length <= 3 && isOnlyEmoji) {
} else if (stickerMatch.length <= 3 && isOnlySticker) {
width = 32;
height = 32;
} else {
@ -114,12 +137,15 @@ class MarkdownTextContent extends StatelessWidget {
return ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(radius)),
child: Container(
width: width,
height: height,
color: Theme.of(context).colorScheme.surfaceContainer,
child: FutureBuilder(
future: sticker.getStickerByAlias(segments[1]),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
return const Center(
child: CircularProgressIndicator());
}
return AutoCacheImage(
snapshot.data!.imageUrl,
@ -147,7 +173,6 @@ class MarkdownTextContent extends StatelessWidget {
).paddingSymmetric(vertical: 4);
}
}
return AutoCacheImage(
url,
width: width,
@ -155,6 +180,19 @@ class MarkdownTextContent extends StatelessWidget {
fit: fit,
);
},
),
);
if (idx < paragraphs.length - 1) {
contentWidgets.add(const Gap(4));
}
}
// Return the list of widgets for the paragraphs
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: contentWidgets,
);
}

View File

@ -9,9 +9,9 @@ abstract class AppNavigation {
page: 'dashboard',
),
AppNavigationDestination(
icon: Icons.newspaper,
label: 'feed'.tr,
page: 'feed',
icon: Icons.explore,
label: 'explore'.tr,
page: 'explore',
),
AppNavigationDestination(
icon: Icons.workspaces,

View File

@ -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<PostAction> {
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)
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24),

View File

@ -18,6 +18,7 @@ import 'package:solian/widgets/link_expansion.dart';
import 'package:solian/widgets/markdown_text_content.dart';
import 'package:solian/widgets/posts/post_tags.dart';
import 'package:solian/widgets/posts/post_quick_action.dart';
import 'package:solian/widgets/relative_date.dart';
import 'package:solian/widgets/sized_container.dart';
import 'package:timeago/timeago.dart' show format;
@ -69,360 +70,6 @@ class _PostItemState extends State<PostItem> {
super.initState();
}
Widget _buildDate() {
if (widget.isFullDate) {
return Text(DateFormat('y/M/d HH:mm')
.format(item.publishedAt?.toLocal() ?? DateTime.now()));
} else {
return Text(
format(
item.publishedAt?.toLocal() ?? DateTime.now(),
locale: 'en_short',
),
);
}
}
Widget _buildThumbnail() {
if (widget.item.body['thumbnail'] == null) return const SizedBox.shrink();
final border = BorderSide(
color: Theme.of(context).dividerColor,
width: 0.3,
);
return Container(
decoration: BoxDecoration(border: Border(top: border, bottom: border)),
child: AspectRatio(
aspectRatio: 16 / 9,
child: AttachmentSelfContainedEntry(
rid: widget.item.body['thumbnail'],
parentId: 'p${item.id}-thumbnail',
),
),
);
}
Widget _buildHeader() {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.isCompact)
AccountAvatar(
content: item.author.avatar,
radius: 10,
).paddingOnly(left: 2, top: 1),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
item.author.nick,
style: const TextStyle(fontWeight: FontWeight.bold),
),
_buildDate().paddingOnly(left: 4),
],
),
if (item.body['title'] != null)
Text(
item.body['title'],
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(fontSize: 15),
),
if (item.body['description'] != null)
Text(
item.body['description'],
style: Theme.of(context).textTheme.bodySmall,
),
],
).paddingOnly(left: widget.isCompact ? 6 : 12),
),
if (widget.item.type == 'article')
Badge(
label: Text('article'.tr),
).paddingOnly(top: 3),
],
);
}
Widget _buildHeaderDivider() {
if (item.body['description'] != null || item.body['title'] != null) {
return const Divider(thickness: 0.3, height: 1).paddingSymmetric(
vertical: 8,
);
}
return const SizedBox.shrink();
}
Widget _buildFooter() {
List<String> labels = List.empty(growable: true);
if (widget.item.editedAt != null) {
labels.add('postEdited'.trParams({
'date': DateFormat('yy/M/d HH:mm').format(item.editedAt!.toLocal()),
}));
}
if (widget.item.realm != null) {
labels.add('postInRealm'.trParams({
'realm': widget.item.realm!.alias,
}));
}
List<Widget> widgets = List.empty(growable: true);
if (widget.item.tags?.isNotEmpty ?? false) {
widgets.add(PostTagsList(tags: widget.item.tags!));
}
if (labels.isNotEmpty) {
widgets.add(Text(
labels.join(' · '),
textAlign: TextAlign.left,
style: TextStyle(
fontSize: 12,
color: _unFocusColor,
),
));
}
if (widget.item.pinnedAt != null) {
widgets.add(Text(
'postPinned'.tr,
style: TextStyle(fontSize: 12, color: _unFocusColor),
));
}
if (widgets.isEmpty) {
return const SizedBox.shrink();
} else {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: widgets,
).paddingOnly(top: 4);
}
}
Widget _buildReply(BuildContext context) {
return OpenContainer(
tappable: widget.isClickable || widget.isOverrideEmbedClickable,
closedBuilder: (_, openContainer) => Column(
children: [
Row(
children: [
FaIcon(
FontAwesomeIcons.reply,
size: 16,
color: _unFocusColor,
),
Expanded(
child: Text(
'postRepliedNotify'.trParams(
{'username': '@${widget.item.replyTo!.author.name}'},
),
style: TextStyle(color: _unFocusColor),
).paddingOnly(left: 6),
),
],
).paddingOnly(left: 12),
Card(
elevation: 1,
child: PostItem(
item: widget.item.replyTo!,
isCompact: true,
attachmentParent: widget.item.id.toString(),
).paddingSymmetric(vertical: 8),
),
],
),
openBuilder: (_, __) => TitleShell(
title: 'postDetail'.tr,
child: PostDetailScreen(
id: widget.item.replyTo!.id.toString(),
post: widget.item.replyTo!,
),
),
closedElevation: 0,
openElevation: 0,
closedColor:
widget.backgroundColor ?? Theme.of(context).colorScheme.surface,
openColor: Theme.of(context).colorScheme.surface,
);
}
Widget _buildRepost(BuildContext context) {
return OpenContainer(
tappable: widget.isClickable || widget.isOverrideEmbedClickable,
closedBuilder: (_, openContainer) => Column(
children: [
Row(
children: [
FaIcon(
FontAwesomeIcons.retweet,
size: 16,
color: _unFocusColor,
),
Expanded(
child: Text(
'postRepostedNotify'.trParams(
{'username': '@${widget.item.repostTo!.author.name}'},
),
style: TextStyle(color: _unFocusColor),
).paddingOnly(left: 6),
),
],
).paddingOnly(left: 12),
Card(
elevation: 1,
child: PostItem(
item: widget.item.repostTo!,
isCompact: true,
attachmentParent: widget.item.id.toString(),
).paddingSymmetric(vertical: 8),
),
],
),
openBuilder: (_, __) => TitleShell(
title: 'postDetail'.tr,
child: PostDetailScreen(
id: widget.item.repostTo!.id.toString(),
post: widget.item.repostTo!,
),
),
closedElevation: 0,
openElevation: 0,
closedColor:
widget.backgroundColor ?? Theme.of(context).colorScheme.surface,
openColor: Theme.of(context).colorScheme.surface,
);
}
Widget _buildAttachments() {
final List<String> attachments = item.body['attachments'] is List
? List.from(item.body['attachments']?.whereType<String>())
: List.empty();
if (attachments.length > 3) {
return AttachmentList(
parentId: widget.item.id.toString(),
attachmentsId: attachments,
autoload: false,
isGrid: true,
).paddingOnly(left: 36, top: 4, bottom: 4);
} else if (attachments.length > 1 || AppTheme.isLargeScreen(context)) {
return AttachmentList(
parentId: widget.item.id.toString(),
attachmentsId: attachments,
autoload: false,
isColumn: true,
).paddingOnly(left: 60, right: 24);
} else {
return AttachmentList(
flatMaxHeight: MediaQuery.of(context).size.width,
parentId: widget.item.id.toString(),
attachmentsId: attachments,
autoload: false,
);
}
}
Widget _buildFeaturedReply() {
if ((widget.item.metric?.replyCount ?? 0) == 0) {
return const SizedBox.shrink();
}
final List<String> attachments = item.body['attachments'] is List
? List.from(item.body['attachments']?.whereType<String>())
: List.empty();
final unFocusColor =
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
return FutureBuilder(
future: Get.find<PostProvider>().listPostFeaturedReply(
widget.item.id.toString(),
),
builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data!.isEmpty) {
return const SizedBox.shrink();
}
return Container(
constraints: const BoxConstraints(maxWidth: 480),
child: Card(
margin: EdgeInsets.zero,
child: Column(
children: snapshot.data!
.map(
(x) => Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AccountAvatar(content: x.author.avatar, radius: 10),
const Gap(6),
Text(
x.author.nick,
style: const TextStyle(fontWeight: FontWeight.bold),
),
const Gap(6),
Text(
format(
x.publishedAt?.toLocal() ?? DateTime.now(),
locale: 'en_short',
),
).paddingOnly(top: 0.5),
const Gap(8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MarkdownTextContent(
content: x.body['content'],
parentId: 'p${item.id}-featured-reply${x.id}',
),
if (x.body['attachments'] is List &&
x.body['attachments'].length > 0)
Row(
children: [
Icon(
Icons.file_copy,
size: 15,
color: unFocusColor,
).paddingOnly(right: 5),
Text(
'attachmentHint'.trParams(
{
'count': x.body['attachments'].length
.toString()
},
),
style: TextStyle(color: unFocusColor),
)
],
),
],
),
),
],
).paddingSymmetric(horizontal: 12, vertical: 8),
)
.toList(),
),
),
)
.animate()
.fadeIn(
duration: 300.ms,
curve: Curves.easeIn,
)
.paddingOnly(
top: (attachments.length == 1 && !AppTheme.isLargeScreen(context))
? 10
: 6,
left:
(attachments.length == 1 && !AppTheme.isLargeScreen(context))
? 24
: 60,
right: 16,
);
},
);
}
double _contentHeight = 0;
@override
@ -436,9 +83,15 @@ class _PostItemState extends State<PostItem> {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildThumbnail().paddingOnly(bottom: 8),
_buildHeader().paddingSymmetric(horizontal: 12),
_buildHeaderDivider().paddingSymmetric(horizontal: 12),
_PostThumbnail(
rid: item.body['thumbnail'],
parentId: widget.item.id.toString(),
).paddingOnly(bottom: 8),
_PostHeaderWidget(
isCompact: widget.isCompact,
item: item,
).paddingSymmetric(horizontal: 12),
_PostHeaderDividerWidget(item: item).paddingSymmetric(horizontal: 12),
Stack(
children: [
SizedContainer(
@ -448,10 +101,14 @@ class _PostItemState extends State<PostItem> {
onChange: (size) {
setState(() => _contentHeight = size.height);
},
child: SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(),
child: MarkdownTextContent(
parentId: 'p${item.id}',
content: item.body['content'],
isAutoWarp: item.type == 'story',
isSelectable: widget.isContentSelectable,
),
).paddingOnly(
left: 16,
right: 12,
@ -489,7 +146,7 @@ class _PostItemState extends State<PostItem> {
right: 8,
top: 4,
),
_buildFooter().paddingOnly(left: 16),
_PostFooterWidget(item: item).paddingOnly(left: 16),
if (attachments.isNotEmpty)
Row(
children: [
@ -515,7 +172,10 @@ class _PostItemState extends State<PostItem> {
closedBuilder: (_, openContainer) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildThumbnail().paddingOnly(bottom: 4),
_PostThumbnail(
rid: item.body['thumbnail'],
parentId: widget.item.id.toString(),
).paddingOnly(bottom: 4),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -537,8 +197,11 @@ class _PostItemState extends State<PostItem> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
_buildHeaderDivider(),
_PostHeaderWidget(
isCompact: widget.isCompact,
item: item,
),
_PostHeaderDividerWidget(item: item),
Stack(
children: [
SizedContainer(
@ -549,15 +212,19 @@ class _PostItemState extends State<PostItem> {
onChange: (size) {
setState(() => _contentHeight = size.height);
},
child: SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(),
child: MarkdownTextContent(
parentId: 'p${item.id}-embed',
content: item.body['content'],
isAutoWarp: item.type == 'story',
isSelectable: widget.isContentSelectable,
isLargeText: item.type == 'article' &&
widget.isFullContent,
).paddingOnly(left: 12, right: 8),
),
),
),
if (_contentHeight >= 320 && !widget.isFullContent)
Align(
alignment: Alignment.bottomCenter,
@ -569,10 +236,14 @@ class _PostItemState extends State<PostItem> {
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Theme.of(context).colorScheme.surface,
(widget.backgroundColor ??
Theme.of(context)
.colorScheme
.surface
.surface),
(widget.backgroundColor ??
Theme.of(context)
.colorScheme
.surface)
.withOpacity(0),
],
),
@ -586,15 +257,33 @@ class _PostItemState extends State<PostItem> {
Container(
constraints: const BoxConstraints(maxWidth: 480),
padding: const EdgeInsets.only(top: 4),
child: _buildReply(context),
child: _PostEmbedWidget(
isClickable: widget.isClickable,
isOverrideEmbedClickable:
widget.isOverrideEmbedClickable,
item: widget.item.replyTo!,
username: widget.item.replyTo!.author.name,
hintText: 'postRepliedNotify',
icon: FontAwesomeIcons.reply,
id: widget.item.replyTo!.id.toString(),
),
),
if (widget.item.repostTo != null && widget.isShowEmbed)
Container(
constraints: const BoxConstraints(maxWidth: 480),
padding: const EdgeInsets.only(top: 4),
child: _buildRepost(context),
child: _PostEmbedWidget(
isClickable: widget.isClickable,
isOverrideEmbedClickable:
widget.isOverrideEmbedClickable,
item: widget.item.repostTo!,
username: widget.item.repostTo!.author.name,
hintText: 'postRepostedNotify',
icon: FontAwesomeIcons.retweet,
id: widget.item.repostTo!.id.toString(),
),
_buildFooter().paddingOnly(left: 12),
),
_PostFooterWidget(item: item).paddingOnly(left: 12),
LinkExpansion(content: item.body['content'])
.paddingOnly(top: 4),
],
@ -610,8 +299,8 @@ class _PostItemState extends State<PostItem> {
right: 16,
left: 16,
),
_buildAttachments(),
if (widget.showFeaturedReply) _buildFeaturedReply(),
_PostAttachmentWidget(item: item),
if (widget.showFeaturedReply) _PostFeaturedReplyWidget(item: item),
if (widget.isShowReply || widget.isReactable)
PostQuickAction(
isShowReply: widget.isShowReply,
@ -654,6 +343,400 @@ class _PostItemState extends State<PostItem> {
}
}
class _PostFeaturedReplyWidget extends StatelessWidget {
final Post item;
const _PostFeaturedReplyWidget({required this.item});
@override
Widget build(BuildContext context) {
final isLargeScreen = AppTheme.isLargeScreen(context);
final unFocusColor =
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
if ((item.metric?.replyCount ?? 0) == 0) {
return const SizedBox.shrink();
}
final List<String> attachments = item.body['attachments'] is List
? List.from(item.body['attachments']?.whereType<String>())
: List.empty();
return FutureBuilder(
future:
Get.find<PostProvider>().listPostFeaturedReply(item.id.toString()),
builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data!.isEmpty) {
return const SizedBox.shrink();
}
return Container(
constraints: const BoxConstraints(maxWidth: 480),
child: Card(
margin: EdgeInsets.zero,
child: Column(
children: snapshot.data!
.map(
(reply) => ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: OpenContainer(
closedBuilder: (_, openContainer) => Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AccountAvatar(
content: reply.author.avatar,
radius: 10,
),
const Gap(6),
Text(
reply.author.nick,
style:
const TextStyle(fontWeight: FontWeight.bold),
),
const Gap(6),
Text(
format(
reply.publishedAt?.toLocal() ?? DateTime.now(),
locale: 'en_short',
),
).paddingOnly(top: 0.5),
const Gap(8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MarkdownTextContent(
isAutoWarp: reply.type == 'story',
content: reply.body['content'],
parentId:
'p${item.id}-featured-reply${reply.id}',
),
if (reply.body['attachments'] is List &&
reply.body['attachments'].isNotEmpty)
Row(
children: [
Icon(
Icons.file_copy,
size: 15,
color: unFocusColor,
).paddingOnly(right: 5),
Text(
'attachmentHint'.trParams(
{
'count': reply
.body['attachments'].length
.toString(),
},
),
style: TextStyle(color: unFocusColor),
),
],
),
],
),
),
],
).paddingSymmetric(horizontal: 12, vertical: 8),
openBuilder: (_, __) => TitleShell(
title: 'postDetail'.tr,
child: PostDetailScreen(
id: reply.id.toString(),
post: reply,
),
),
closedElevation: 0,
openElevation: 0,
closedColor:
Theme.of(context).colorScheme.surfaceContainer,
openColor: Theme.of(context).colorScheme.surface,
),
),
)
.toList(),
),
),
)
.animate()
.fadeIn(
duration: 300.ms,
curve: Curves.easeIn,
)
.paddingOnly(
top: (attachments.length == 1 && !isLargeScreen) ? 10 : 6,
left: (attachments.length == 1 && !isLargeScreen) ? 24 : 60,
right: 16,
);
},
);
}
}
class _PostAttachmentWidget extends StatelessWidget {
final Post item;
const _PostAttachmentWidget({required this.item});
@override
Widget build(BuildContext context) {
final isLargeScreen = AppTheme.isLargeScreen(context);
final List<String> attachments = item.body['attachments'] is List
? List.from(item.body['attachments']?.whereType<String>())
: List.empty();
if (attachments.length > 3) {
return AttachmentList(
parentId: item.id.toString(),
attachmentsId: attachments,
autoload: false,
isGrid: true,
).paddingOnly(left: 36, top: 4, bottom: 4);
} else if (attachments.length > 1 || isLargeScreen) {
return AttachmentList(
parentId: item.id.toString(),
attachmentsId: attachments,
autoload: false,
isColumn: true,
).paddingOnly(left: 60, right: 24, top: 4, bottom: 4);
} else {
return AttachmentList(
flatMaxHeight: MediaQuery.of(context).size.width,
parentId: item.id.toString(),
attachmentsId: attachments,
autoload: false,
);
}
}
}
class _PostEmbedWidget extends StatelessWidget {
final bool isClickable;
final bool isOverrideEmbedClickable;
final Post item;
final String username;
final String hintText;
final IconData icon;
final String id;
const _PostEmbedWidget({
required this.isClickable,
required this.isOverrideEmbedClickable,
required this.item,
required this.username,
required this.hintText,
required this.icon,
required this.id,
});
@override
Widget build(BuildContext context) {
final unFocusColor =
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
return OpenContainer(
tappable: isClickable || isOverrideEmbedClickable,
closedBuilder: (_, openContainer) => Column(
children: [
Row(
children: [
FaIcon(
icon,
size: 16,
color: unFocusColor,
),
Expanded(
child: Text(
hintText.trParams(
{'username': '@$username'},
),
style: TextStyle(color: unFocusColor),
).paddingOnly(left: 6),
),
],
).paddingOnly(left: 12),
Card(
elevation: 1,
child: PostItem(
item: item,
isCompact: true,
attachmentParent: id,
).paddingSymmetric(vertical: 8),
),
],
),
openBuilder: (_, __) => TitleShell(
title: 'postDetail'.tr,
child: PostDetailScreen(
id: id,
post: item,
),
),
closedElevation: 0,
openElevation: 0,
closedColor: Theme.of(context).colorScheme.surface,
openColor: Theme.of(context).colorScheme.surface,
);
}
}
class _PostHeaderDividerWidget extends StatelessWidget {
final Post item;
const _PostHeaderDividerWidget({
required this.item,
});
@override
Widget build(BuildContext context) {
if (item.body['description'] != null || item.body['title'] != null) {
return const Divider(thickness: 0.3, height: 1).paddingSymmetric(
vertical: 8,
);
}
return const SizedBox.shrink();
}
}
class _PostFooterWidget extends StatelessWidget {
final Post item;
const _PostFooterWidget({required this.item});
@override
Widget build(BuildContext context) {
final unFocusColor =
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
List<String> labels = List.empty(growable: true);
if (item.editedAt != null) {
labels.add('postEdited'.trParams({
'date': DateFormat('yy/M/d HH:mm').format(item.editedAt!.toLocal()),
}));
}
if (item.realm != null) {
labels.add('postInRealm'.trParams({
'realm': item.realm!.alias,
}));
}
List<Widget> widgets = List.empty(growable: true);
if (item.tags?.isNotEmpty ?? false) {
widgets.add(PostTagsList(tags: item.tags!));
}
if (labels.isNotEmpty) {
widgets.add(Text(
labels.join(' · '),
textAlign: TextAlign.left,
style: TextStyle(
fontSize: 12,
color: unFocusColor,
),
));
}
if (item.pinnedAt != null) {
widgets.add(Text(
'postPinned'.tr,
style: TextStyle(fontSize: 12, color: unFocusColor),
));
}
if (widgets.isEmpty) {
return const SizedBox.shrink();
} else {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: widgets,
).paddingOnly(top: 4);
}
}
}
class _PostHeaderWidget extends StatelessWidget {
final bool isCompact;
final Post item;
const _PostHeaderWidget({
required this.isCompact,
required this.item,
});
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (isCompact)
AccountAvatar(
content: item.author.avatar,
radius: 10,
).paddingOnly(left: 2, top: 1),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
item.author.nick,
style: const TextStyle(fontWeight: FontWeight.bold),
),
RelativeDate(item.publishedAt?.toLocal() ?? DateTime.now())
.paddingOnly(left: 4),
],
),
if (item.body['title'] != null)
Text(
item.body['title'],
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(fontSize: 15),
),
if (item.body['description'] != null)
Text(
item.body['description'],
style: Theme.of(context).textTheme.bodySmall,
),
],
).paddingOnly(left: isCompact ? 6 : 12),
),
if (item.type == 'article')
Badge(
label: Text('article'.tr),
).paddingOnly(top: 3),
],
);
}
}
class _PostThumbnail extends StatelessWidget {
final String parentId;
final String? rid;
const _PostThumbnail({required this.parentId, required this.rid});
@override
Widget build(BuildContext context) {
if (rid?.isEmpty ?? true) return const SizedBox.shrink();
final border = BorderSide(
color: Theme.of(context).dividerColor,
width: 0.3,
);
return Container(
decoration: BoxDecoration(border: Border(top: border, bottom: border)),
child: AspectRatio(
aspectRatio: 16 / 9,
child: AttachmentSelfContainedEntry(
rid: rid!,
parentId: 'p$parentId-thumbnail',
),
),
);
}
}
typedef _OnWidgetSizeChange = void Function(Size size);
class _MeasureSizeRenderObject extends RenderProxyBox {

View File

@ -27,7 +27,7 @@ class PostTagsList extends StatelessWidget {
),
),
onTap: () {
AppRouter.instance.pushNamed('feedSearch', queryParameters: {
AppRouter.instance.pushNamed('postSearch', queryParameters: {
'tag': x.alias,
});
},

View File

@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:timeago/timeago.dart';
class RelativeDate extends StatelessWidget {
final DateTime date;
final bool isFull;
const RelativeDate(this.date, {super.key, this.isFull = false});
@override
Widget build(BuildContext context) {
if (isFull) {
return Text(DateFormat('y/M/d HH:mm').format(date));
}
return Text(
format(
date,
locale: 'en_short',
),
);
}
}

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

View File

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

View File

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