Compare commits

...

27 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
153c15e5c9 🚀 Launch 1.2.2+2 2024-09-18 13:05:08 +08:00
6a0f42cdc9 🐛 Fix realm view won't show channels 2024-09-18 13:03:40 +08:00
01aaa5455e 💄 Fix content padding mis-match 2024-09-18 00:14:16 +08:00
f3ceb5f967 🚀 Launch 1.2.2+1 2024-09-17 23:50:49 +08:00
b5e2fa4c25 🐛 Fix post editor alias overflow 2024-09-17 23:08:00 +08:00
8378024490 🚀 Launch 1.2.1+41 2024-09-17 22:31:37 +08:00
6d40d6bba3 💄 Optimize content 2024-09-17 21:48:20 +08:00
77075c8dab Optimize updater 2024-09-17 21:37:20 +08:00
dec34e297d 🐛 Bug fixes on attachments and related things 2024-09-17 20:59:01 +08:00
358677ade0 Android self-update 2024-09-17 20:40:44 +08:00
d2f37ae45d 🐛 Fix fileType render error 2024-09-17 18:28:53 +08:00
41 changed files with 1922 additions and 1024 deletions

View File

@ -1,4 +1,4 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="dev.solsynth.solian">
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
<uses-permission android:name="android.permission.WAKE_LOCK" />

View File

@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip

View File

@ -18,7 +18,7 @@ pluginManagement {
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version '8.4.0' apply false
id "com.android.application" version '8.6.0' apply false
id "com.google.gms.google-services" version "4.3.15" apply false
id "com.google.firebase.crashlytics" version "2.8.1" apply false
id "org.jetbrains.kotlin.android" version '2.0.0' apply false

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",
@ -55,7 +55,7 @@
"edit": "Edit",
"delete": "Delete",
"settings": "Settings",
"settingsNotificationBgService": "Background Notification Service",
"settingsNotificationBgService": "Background notification service",
"settingsNotificationBgServiceDesc": "A notification service is always installed on the device, so that some devices that do not support push notifications can receive notifications in the background. When this feature is enabled, push notifications will not be registered with the server, and you will always appear to be online in the eyes of others (except for invisible). You may need to turn off power and traffic optimization in the settings.",
"search": "Search",
"post": "Post",
@ -68,6 +68,11 @@
"notificationUnreadCount": "@count unread notifications",
"errorHappened": "An error occurred",
"errorHappenedUnauthorized": "Unauthorized request, please sign in or try resign in.",
"errorHappenedRequestBad": "Request error, the server refused to process the request. Please check your request data.",
"errorHappenedRequestForbidden": "Request error, insufficient permissions.",
"errorHappenedRequestNotFound": "Request error, the requested data does not exist.",
"errorHappenedRequestConnection": "Network request failed. Please check the connection status and service status, then try again.",
"errorHappenedRequestUnknown": "Request error, unknown type. Please take a full screenshot of this message and submit feedback.",
"forgotPassword": "Forgot password",
"email": "Email",
"username": "Username",
@ -350,8 +355,7 @@
"bsCheckForUpdate": "Checking For Updates",
"bsCheckForUpdateFailed": "Unable to Check Updates",
"bsCheckForUpdateNew": "Found New Version",
"bsCheckForUpdateDescApple": "Please head to TestFlight and update your app to latest version to prevent error happens and get latest functions.",
"bsCheckForUpdateDescCommon": "Please head to our website download and install latest version of application to prevent error happens and get latest functions.",
"bsCheckForUpdateDesc": "Please head to app store and update your app to latest version to prevent error happens and get latest functions.",
"bsCheckingServer": "Checking Server Status",
"bsCheckingServerFail": "Unable connect to server, check your network connection",
"bsCheckingServerDown": "Server currently unavailable, please retry later",
@ -422,5 +426,32 @@
"notificationTopicPostFeedback": "Post feedbacks",
"notificationTopicPostSubscription": "Post subscriptions",
"preferencesApplied": "Preferences has been applied.",
"save": "Save"
"save": "Save",
"updateAvailable": "Update available",
"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.",
"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 条未读的消息",
@ -351,8 +351,7 @@
"bsCheckForUpdate": "正在检查更新",
"bsCheckForUpdateFailed": "无法检查更新",
"bsCheckForUpdateNew": "发现新版本",
"bsCheckForUpdateDescApple": "请前往 TestFlight 并将您的应用程序更新到最新版本,以防止出现错误并获取最新功能。",
"bsCheckForUpdateDescCommon": "请前往我们的网站下载并安装最新版本的应用程序,以防止出现错误并获取最新功能。",
"bsCheckForUpdateDesc": "请前往应用商店并将您的应用程序更新到最新版本,以防止出现错误并获取最新功能。",
"bsCheckingServer": "检查服务器状态中",
"bsCheckingServerFail": "无法连接至服务器,请检查你的网络连接状态",
"bsCheckingServerDown": "当前服务器不可用,请稍后重试",
@ -423,5 +422,32 @@
"notificationTopicPostFeedback": "帖子反馈",
"notificationTopicPostSubscription": "订阅源",
"preferencesApplied": "偏好设置已应用",
"save": "保存"
"save": "保存",
"updateAvailable": "有可用更新",
"updateAvailableDesc": "有可用更新 (@from 到 @to) 你想现在下载安装吗?在等待下载期间你仍可以正常使用。",
"update": "更新",
"updateCheckStrictly": "严格模式",
"updateCheckStrictlyDesc": "如果启用,应用程序将会在本地版本与远程版本不同时询问更新,而不会检查版本号大小。",
"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

@ -54,22 +54,22 @@ PODS:
- Firebase/Performance (11.0.0):
- Firebase/CoreOnly
- FirebasePerformance (~> 11.0.0)
- firebase_analytics (11.3.1):
- firebase_analytics (11.3.2):
- Firebase/Analytics (= 11.0.0)
- firebase_core
- Flutter
- firebase_core (3.4.1):
- firebase_core (3.5.0):
- Firebase/CoreOnly (= 11.0.0)
- Flutter
- firebase_crashlytics (4.1.1):
- firebase_crashlytics (4.1.2):
- Firebase/Crashlytics (= 11.0.0)
- firebase_core
- Flutter
- firebase_messaging (15.1.1):
- firebase_messaging (15.1.2):
- Firebase/Messaging (= 11.0.0)
- firebase_core
- Flutter
- firebase_performance (0.10.0-6):
- firebase_performance (0.10.0-7):
- Firebase/Performance (= 11.0.0)
- firebase_core
- Flutter
@ -154,6 +154,8 @@ PODS:
- PromisesSwift (~> 2.1)
- FirebaseSharedSwift (11.2.0)
- Flutter (1.0.0)
- flutter_app_update (0.0.1):
- Flutter
- flutter_background_service_ios (0.0.3):
- Flutter
- flutter_keyboard_visibility (0.0.1):
@ -225,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):
@ -306,6 +308,7 @@ DEPENDENCIES:
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
- firebase_performance (from `.symlinks/plugins/firebase_performance/ios`)
- Flutter (from `Flutter`)
- flutter_app_update (from `.symlinks/plugins/flutter_app_update/ios`)
- flutter_background_service_ios (from `.symlinks/plugins/flutter_background_service_ios/ios`)
- flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
@ -383,6 +386,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/firebase_performance/ios"
Flutter:
:path: Flutter
flutter_app_update:
:path: ".symlinks/plugins/flutter_app_update/ios"
flutter_background_service_ios:
:path: ".symlinks/plugins/flutter_background_service_ios/ios"
flutter_keyboard_visibility:
@ -445,11 +450,11 @@ SPEC CHECKSUMS:
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
Firebase: 9f574c08c2396885b5e7e100ed4293d956218af9
firebase_analytics: b8ce6c2c4b245d3c3bb3a147965d09da0f455959
firebase_core: ba84e940cf5cbbc601095f86556560937419195c
firebase_crashlytics: 4111f8198b78c99471c955af488cecd8224967e6
firebase_messaging: c40f84e7a98da956d5262fada373b5c458edcf13
firebase_performance: 8b7b9ca5adf3a9b3afa12b4eb96b9cabefc2c248
firebase_analytics: 4fd10182fd08bb8358f26ac8aca8dad7b6d0f592
firebase_core: 2ec6b789859c7c24766344ec71fdf78639402d56
firebase_crashlytics: 60630a0f91ee432275fa1660fd8593079761448a
firebase_messaging: a18e1e02b2e8e69097c8173e0c851be223b21c50
firebase_performance: 12d45fdf120992fa879d990929bf73d4a5ced053
FirebaseABTesting: 2104d957ce33888a3d6f3bde298cdee376dde8f1
FirebaseAnalytics: 27eb78b97880ea4a004839b9bac0b58880f5a92a
FirebaseCore: 3cf438f431f18c12cdf2aaf64434648b63f7e383
@ -464,6 +469,7 @@ SPEC CHECKSUMS:
FirebaseSessions: adcec8b72d0066a385e3affcd1bcb1ebb3908ce6
FirebaseSharedSwift: 7a0d78d155ede78407f0fdc89fbc914014c7c540
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_app_update: 65f61da626cb111d1b24674abc4b01728d7723bc
flutter_background_service_ios: e30e0d3ee69e4cee66272d0c78eacd48c2e94aac
flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069
flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086
@ -476,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

@ -1,8 +1,12 @@
import 'dart:async';
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: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';
@ -13,6 +17,8 @@ import 'package:solian/providers/theme_switcher.dart';
import 'package:solian/providers/websocket.dart';
import 'package:solian/services.dart';
import 'package:solian/widgets/sized_container.dart';
import 'package:flutter_app_update/flutter_app_update.dart';
import 'package:version/version.dart';
class BootstrapperShell extends StatefulWidget {
final Widget child;
@ -34,6 +40,87 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
int _periodCursor = 0;
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 = [
(
label: 'bsLoadingTheme',
@ -41,32 +128,6 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
await context.read<ThemeSwitcher>().restoreTheme();
},
),
(
label: 'bsCheckForUpdate',
action: () async {
if (PlatformInfo.isWeb) return;
try {
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?limit=1',
);
if (resp.body[0]['name'] != localVersionString) {
setState(() {
_isErrored = true;
_subtitle = PlatformInfo.isIOS || PlatformInfo.isMacOS
? 'bsCheckForUpdateDescApple'.tr
: 'bsCheckForUpdateDescCommon'.tr;
});
}
} catch (e) {
setState(() {
_isErrored = true;
_subtitle = 'bsCheckForUpdateFailed'.tr;
});
}
},
),
(
label: 'bsCheckingServer',
action: () async {
@ -154,6 +215,9 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
}
} finally {
setState(() => _isBusy = false);
Future.delayed(const Duration(milliseconds: 100), () {
_bootCompleter.complete();
});
}
}
@ -161,6 +225,7 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
void initState() {
super.initState();
_runPeriods();
_checkForUpdate();
}
@override
@ -251,6 +316,9 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
_isBusy = false;
_isErrored = false;
});
Future.delayed(const Duration(milliseconds: 100), () {
_bootCompleter.complete();
});
} else {
setState(() {
_isBusy = true;

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';
@ -51,6 +53,69 @@ extension AppExtensions on BuildContext {
);
}
Future<bool> showConfirmDialog(String title, body) async {
return await showDialog<bool>(
useRootNavigator: true,
context: this,
builder: (ctx) => AlertDialog(
title: Text(title),
content: Text(body),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: Text('cancel'.tr),
),
TextButton(
onPressed: () => Navigator.pop(ctx, true),
child: Text('okay'.tr),
)
],
),
) ??
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

@ -9,7 +9,6 @@ import 'package:go_router/go_router.dart';
import 'package:protocol_handler/protocol_handler.dart';
import 'package:provider/provider.dart';
import 'package:solian/background.dart';
import 'package:solian/bootstrapper.dart';
import 'package:solian/firebase_options.dart';
import 'package:solian/platform.dart';
import 'package:solian/providers/attachment_uploader.dart';
@ -123,9 +122,7 @@ class SolianApp extends StatelessWidget {
builder: (context, child) {
return SystemShell(
child: ScaffoldMessenger(
child: BootstrapperShell(
child: child ?? const SizedBox.shrink(),
),
child: child ?? const SizedBox.shrink(),
),
);
},

View File

@ -89,13 +89,13 @@ class ChannelProvider extends GetxController {
return resp;
}
Future<Response> listAvailableChannel({String realm = 'global'}) async {
Future<Response> listAvailableChannel({String scope = 'global'}) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
final client = await auth.configureClient('messaging');
final resp = await client.get('/channels/$realm/me/available');
final resp = await client.get('/channels/$scope/me/available');
if (resp.statusCode != 200) {
throw RequestException(resp);
}

View File

@ -1,6 +1,7 @@
import 'package:animations/animations.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:solian/bootstrapper.dart';
import 'package:solian/models/realm.dart';
import 'package:solian/screens/about.dart';
import 'package:solian/screens/account.dart';
@ -22,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';
@ -32,9 +33,12 @@ abstract class AppRouter {
static GoRouter instance = GoRouter(
routes: [
ShellRoute(
builder: (context, state, child) => RootShell(
state: state,
child: child,
builder: (context, state, child) => BootstrapperShell(
key: const Key('global-bootstrapper'),
child: RootShell(
state: state,
child: child,
),
),
routes: [
GoRoute(
@ -74,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(
@ -89,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,31 +49,51 @@ class AboutScreen extends StatelessWidget {
),
Text('Copyright © ${DateTime.now().year} Solsynth LLC'),
const Gap(16),
TextButton(
style: denseButtonStyle,
child: const Text('App Details'),
onPressed: () async {
final info = await PackageInfo.fromPlatform();
CenteredContainer(
maxWidth: 280,
child: Wrap(
spacing: 8,
runSpacing: 8,
alignment: WrapAlignment.center,
children: [
TextButton(
style: denseButtonStyle,
child: Text('appDetails'.tr),
onPressed: () async {
final info = await PackageInfo.fromPlatform();
showAboutDialog(
context: context,
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.',
applicationIcon: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(16)),
child:
Image.asset('assets/logo.png', width: 60, height: 60),
showAboutDialog(
context: context,
applicationVersion:
'${info.version} (${info.buildNumber})',
applicationLegalese:
'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),
),
);
},
),
);
},
),
TextButton(
style: denseButtonStyle,
child: const Text('Project Website'),
onPressed: () {
launchUrlString('https://solsynth.dev/products/solar-network');
},
TextButton(
style: denseButtonStyle,
child: Text('projectWebsite'.tr),
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 Text(

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,96 +193,99 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
toolbarHeight: AppTheme.toolbarHeight(context),
leadingWidth: 24,
automaticallyImplyLeading: false,
flexibleSpace: Row(
children: [
AppBarLeadingButton.adaptive(context) ?? const Gap(8),
const Gap(8),
if (_userinfo != null)
AccountAvatar(content: _userinfo!.avatar, radius: 16),
const Gap(12),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (_userinfo != null)
Text(
_userinfo!.nick,
style: Theme.of(context).textTheme.bodyLarge,
),
if (_userinfo != null)
Text(
'@${_userinfo!.name}',
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
if (_userinfo != null && _subscription == null)
OutlinedButton(
style: const ButtonStyle(
visualDensity:
VisualDensity(horizontal: -4, vertical: -2),
flexibleSpace: SizedBox(
height: 56,
child: Row(
children: [
AppBarLeadingButton.adaptive(context) ?? const Gap(8),
const Gap(8),
if (_userinfo != null)
AccountAvatar(content: _userinfo!.avatar, radius: 16),
const Gap(12),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (_userinfo != null)
Text(
_userinfo!.nick,
style: Theme.of(context).textTheme.bodyLarge,
),
if (_userinfo != null)
Text(
'@${_userinfo!.name}',
style: Theme.of(context).textTheme.bodySmall,
),
],
),
onPressed: _isSubscribing
? null
: () async {
setState(() => _isSubscribing = true);
_subscription =
await Get.find<SubscriptionProvider>()
.subscribeToUser(_userinfo!.id);
setState(() => _isSubscribing = false);
},
child: Text('subscribe'.tr),
)
else if (_userinfo != null)
OutlinedButton(
style: const ButtonStyle(
visualDensity:
VisualDensity(horizontal: -4, vertical: -2),
),
if (_userinfo != null && _subscription == null)
OutlinedButton(
style: const ButtonStyle(
visualDensity:
VisualDensity(horizontal: -4, vertical: -2),
),
onPressed: _isSubscribing
? null
: () async {
setState(() => _isSubscribing = true);
_subscription =
await Get.find<SubscriptionProvider>()
.subscribeToUser(_userinfo!.id);
setState(() => _isSubscribing = false);
},
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
? null
: () async {
setState(() => _isSubscribing = true);
await Get.find<SubscriptionProvider>()
.unsubscribeFromUser(_userinfo!.id);
_subscription = null;
setState(() => _isSubscribing = false);
},
child: Text('unsubscribe'.tr),
if (_userinfo != null &&
!_relationshipProvider.hasFriend(_userinfo!))
IconButton(
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,
),
if (_userinfo != null &&
!_relationshipProvider.hasFriend(_userinfo!))
IconButton(
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,
),
],
),
],
),
).paddingOnly(top: MediaQuery.of(context).padding.top),
bottom: TabBar(
tabs: [
Tab(text: 'profilePage'.tr),
@ -271,21 +299,132 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
body: TabBarView(
physics: const NeverScrollableScrollPhysics(),
children: [
Column(
ListView(
children: [
const Gap(16),
AccountHeadingWidget(
name: _userinfo!.name,
nick: _userinfo!.nick,
desc: _userinfo!.description,
badges: _userinfo!.badges,
banner: _userinfo!.banner,
avatar: _userinfo!.avatar,
status: Get.find<StatusProvider>()
.getSomeoneStatus(_userinfo!.name),
detail: _userinfo,
profile: _userinfo!.profile,
extraWidgets: const [],
CenteredContainer(
child: AccountHeadingWidget(
name: _userinfo!.name,
nick: _userinfo!.nick,
desc: _userinfo!.description,
badges: _userinfo!.badges,
banner: _userinfo!.banner,
avatar: _userinfo!.avatar,
status: Get.find<StatusProvider>()
.getSomeoneStatus(_userinfo!.name),
detail: _userinfo,
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(
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(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child:
Image.asset('assets/logo.png', width: 64, height: 64),
).paddingOnly(bottom: 8, left: 4),
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(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child:
Image.asset('assets/logo.png', width: 64, height: 64),
).paddingOnly(bottom: 8, left: 4),
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(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child:
Image.asset('assets/logo.png', width: 64, height: 64),
).paddingOnly(bottom: 8, left: 4),
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(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: Image.asset('assets/logo.png', width: 64, height: 64),
).paddingOnly(bottom: 8, left: 4),
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

@ -115,6 +115,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
leading: const Icon(Icons.settings),
trailing: const Icon(Icons.chevron_right),
title: Text('channelSettings'.tr),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
onTap: () async {
AppRouter.instance
.pushNamed(
@ -174,6 +175,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
ListTile(
leading: const Icon(Icons.notifications_active),
title: Text('channelNotifyLevel'.tr),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
trailing: DropdownButtonHideUnderline(
child: DropdownButton2<int>(
isExpanded: true,
@ -206,6 +208,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
),
),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Icons.supervisor_account),
trailing: const Icon(Icons.chevron_right),
title: Text('channelMembers'.tr),
@ -214,6 +217,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
...(_isOwned ? ownerActions : List.empty()),
const Divider(thickness: 0.3),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: _isOwned
? const Icon(Icons.delete)
: const Icon(Icons.exit_to_app),

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

@ -183,18 +183,18 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
children: [
ListTile(
tileColor: Theme.of(context).colorScheme.surfaceContainerLow,
title: Row(
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_editorController.title ?? 'title'.tr,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const Gap(6),
if (_editorController.aliasController.text.isNotEmpty)
Badge(
label: Text('#${_editorController.aliasController.text}'),
),
).paddingOnly(bottom: 2),
],
),
subtitle: Text(

View File

@ -34,7 +34,7 @@ class _RealmViewScreenState extends State<RealmViewScreen> {
final List<Channel> _channels = List.empty(growable: true);
getRealm({String? overrideAlias}) async {
final RealmProvider provider = Get.find();
final RealmProvider realm = Get.find();
setState(() => _isBusy = true);
@ -43,7 +43,7 @@ class _RealmViewScreenState extends State<RealmViewScreen> {
}
try {
final resp = await provider.getRealm(_overrideAlias ?? widget.alias);
final resp = await realm.getRealm(_overrideAlias ?? widget.alias);
setState(() => _realm = Realm.fromJson(resp.body));
} catch (e) {
context.showErrorDialog(e);
@ -55,14 +55,26 @@ class _RealmViewScreenState extends State<RealmViewScreen> {
getChannels() async {
setState(() => _isBusy = true);
final ChannelProvider provider = Get.find();
final resp = await provider.listChannel(scope: _realm!.alias);
final ChannelProvider channel = Get.find();
final resp = await channel.listChannel(scope: _realm!.alias);
final availableResp = await channel.listAvailableChannel(
scope: _realm!.alias,
);
final Set<int> channelIdx = {};
setState(() {
_channels.clear();
_channels.addAll(
resp.body.map((e) => Channel.fromJson(e)).toList().cast<Channel>(),
);
_channels.addAll(
availableResp.body
.map((e) => Channel.fromJson(e))
.toList()
.cast<Channel>(),
);
_channels.retainWhere((x) => channelIdx.add(x.id));
});
setState(() => _isBusy = false);

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});
@ -114,6 +117,69 @@ class _SettingScreenState extends State<SettingScreen> {
},
),
),
_buildCaptionHeader('update'.tr),
CheckboxListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
secondary: const Icon(Icons.sync_alt),
title: Text('updateCheckStrictly'.tr),
subtitle: Text('updateCheckStrictlyDesc'.tr),
value: _prefs?.getBool('check_update_strictly') ?? false,
onChanged: (value) {
_prefs
?.setBool('check_update_strictly', value ?? false)
.then((_) {
setState(() {});
});
},
),
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

@ -484,7 +484,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
),
),
Text(
'${fileType[0].toUpperCase()}${fileType.substring(1)} · ${element.size.formatBytes()}',
'${fileType.isNotEmpty ? fileType.capitalize : 'unknown'.tr} · ${element.size.formatBytes()}',
style: const TextStyle(fontSize: 12),
),
],

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(
@ -233,6 +239,7 @@ class _AttachmentItemVideoState extends State<_AttachmentItemVideo> {
final ratio = widget.item.metadata?['ratio'] ?? 16 / 9;
if (!_showContent) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
child: Stack(
children: [
if (widget.item.metadata?['thumbnail'] != null)
@ -400,6 +407,7 @@ class _AttachmentItemAudioState extends State<_AttachmentItemAudio> {
const ratio = 16 / 9;
if (!_showContent) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
child: Stack(
children: [
if (widget.item.metadata?['thumbnail'] != null)

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

@ -10,6 +10,7 @@ class AutoCacheImage extends StatelessWidget {
final BoxFit? fit;
final bool noProgressIndicator;
final bool noErrorWidget;
final bool isDense;
const AutoCacheImage(
this.url, {
@ -19,6 +20,7 @@ class AutoCacheImage extends StatelessWidget {
this.fit,
this.noProgressIndicator = false,
this.noErrorWidget = false,
this.isDense = false,
});
@override
@ -32,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
@ -46,13 +57,14 @@ class AutoCacheImage extends StatelessWidget {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.close, size: 32)
Icon(Icons.close, size: isDense ? 24 : 32)
.animate(onPlay: (e) => e.repeat(reverse: true))
.fade(duration: 500.ms),
Text(
error.toString(),
textAlign: TextAlign.center,
),
if (!isDense)
Text(
error.toString(),
textAlign: TextAlign.center,
),
],
),
),
@ -71,11 +83,20 @@ class AutoCacheImage extends StatelessWidget {
ImageChunkEvent? loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
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,
),
),
);
},
@ -89,13 +110,14 @@ class AutoCacheImage extends StatelessWidget {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.close, size: 32)
Icon(Icons.close, size: isDense ? 24 : 32)
.animate(onPlay: (e) => e.repeat(reverse: true))
.fade(duration: 500.ms),
Text(
error.toString(),
textAlign: TextAlign.center,
),
if (!isDense)
Text(
error.toString(),
textAlign: TextAlign.center,
),
],
),
),

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

@ -415,6 +415,7 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
x.imageUrl,
width: 28,
height: 28,
isDense: true,
),
display: x.name,
content: x.textWarpedPlaceholder,

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,215 +42,222 @@ class DailySignHistoryChartDialog extends StatelessWidget {
child: CircularProgressIndicator(),
),
)
: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'dailySignHistoryRecent'.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!
.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(),
),
: SizedBox(
width: double.maxFinite,
child: ListView(
shrinkWrap: true,
children: [
Text(
'dailySignHistoryRecent'.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(),
),
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!
.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,
spots: data!
.map(
(x) => FlSpot(
x.createdAt
.copyWith(
hour: 0,
minute: 0,
second: 0,
millisecond: 0,
microsecond: 0,
)
.millisecondsSinceEpoch
.toDouble(),
x.resultTier.toDouble(),
),
))
.toList(),
getTooltipColor: (_) =>
Theme.of(context).colorScheme.surfaceContainerHigh,
)),
titlesData: FlTitlesData(
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
)
.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,
),
),
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),
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),
),
),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 28,
interval: 86400000,
getTitlesWidget: (value, _) => Text(
DateFormat('dd').format(
DateTime.fromMillisecondsSinceEpoch(
value.toInt(),
),
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(),
),
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),
if (snapshot.data!.siteName != null)
Text(
snapshot.data!.siteName!,
style: Theme.of(context).textTheme.labelLarge,
Expanded(
child: Text(
snapshot.data!.siteName!,
style: Theme.of(context).textTheme.labelLarge,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
).paddingOnly(

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';
@ -14,137 +15,184 @@ class MarkdownTextContent extends StatelessWidget {
final String content;
final String parentId;
final bool isSelectable;
final bool isLargeText;
final bool isAutoWarp;
const MarkdownTextContent({
super.key,
required this.content,
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(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
data: content,
padding: EdgeInsets.zero,
styleSheet: MarkdownStyleSheet.fromTheme(
Theme.of(context),
).copyWith(
horizontalRuleDecoration: BoxDecoration(
border: Border(
top: BorderSide(
width: 1.0,
color: Theme.of(context).dividerColor,
// 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: paragraph,
padding: EdgeInsets.zero,
styleSheet: MarkdownStyleSheet.fromTheme(
Theme.of(context),
).copyWith(
textScaleFactor: isLargeText ? 1.1 : 1,
blockquote: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
blockquoteDecoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
borderRadius: const BorderRadius.all(Radius.circular(4)),
),
horizontalRuleDecoration: BoxDecoration(
border: Border(
top: BorderSide(
width: 1.0,
color: Theme.of(context).dividerColor,
),
),
),
),
),
),
extensionSet: markdown.ExtensionSet(
markdown.ExtensionSet.gitHubFlavored.blockSyntaxes,
<markdown.InlineSyntax>[
_UserNameCardInlineSyntax(),
_CustomEmoteInlineSyntax(),
markdown.EmojiSyntax(),
markdown.AutolinkSyntax(),
markdown.AutolinkExtensionSyntax(),
...markdown.ExtensionSet.gitHubFlavored.inlineSyntaxes
],
),
onTapLink: (text, href, title) async {
if (href == null) return;
if (href.startsWith('solink://')) {
final segments = href.replaceFirst('solink://', '').split('/');
switch (segments[0]) {
case 'users':
showModalBottomSheet(
useRootNavigator: true,
isScrollControlled: true,
backgroundColor: Theme.of(context).colorScheme.surface,
context: context,
builder: (context) => AccountProfilePopup(
name: segments[1],
),
);
}
return;
}
await launchUrlString(
href,
mode: LaunchMode.externalApplication,
);
},
imageBuilder: (uri, title, alt) {
var url = uri.toString();
double? width, height;
BoxFit? fit;
if (url.startsWith('solink://')) {
final segments = url.replaceFirst('solink://', '').split('/');
switch (segments[0]) {
case 'stickers':
double radius = 8;
final StickerProvider sticker = Get.find();
if (emojiMatch.length <= 1 && isOnlyEmoji) {
width = 128;
height = 128;
} else if (emojiMatch.length <= 3 && isOnlyEmoji) {
width = 32;
height = 32;
} else {
radius = 4;
width = 16;
height = 16;
extensionSet: markdown.ExtensionSet(
markdown.ExtensionSet.gitHubFlavored.blockSyntaxes,
<markdown.InlineSyntax>[
_UserNameCardInlineSyntax(),
_CustomEmoteInlineSyntax(),
markdown.EmojiSyntax(),
markdown.AutolinkSyntax(),
markdown.AutolinkExtensionSyntax(),
...markdown.ExtensionSet.gitHubFlavored.inlineSyntaxes
],
),
onTapLink: (text, href, title) async {
if (href == null) return;
if (href.startsWith('solink://')) {
final segments = href.replaceFirst('solink://', '').split('/');
switch (segments[0]) {
case 'users':
showModalBottomSheet(
useRootNavigator: true,
isScrollControlled: true,
backgroundColor: Theme.of(context).colorScheme.surface,
context: context,
builder: (context) => AccountProfilePopup(
name: segments[1],
),
);
}
fit = BoxFit.contain;
return ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(radius)),
child: Container(
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 AutoCacheImage(
snapshot.data!.imageUrl,
width: width,
height: height,
fit: fit,
noErrorWidget: true,
);
},
),
),
).paddingSymmetric(vertical: 4);
case 'attachments':
const radius = BorderRadius.all(Radius.circular(8));
return LimitedBox(
maxHeight: MediaQuery.of(context).size.width,
child: ClipRRect(
borderRadius: radius,
child: AttachmentSelfContainedEntry(
isDense: true,
parentId: parentId,
rid: segments[1],
),
),
).paddingSymmetric(vertical: 4);
}
}
return;
}
return AutoCacheImage(
url,
width: width,
height: height,
fit: fit,
);
},
await launchUrlString(
href,
mode: LaunchMode.externalApplication,
);
},
imageBuilder: (uri, title, alt) {
var url = uri.toString();
double? width, height;
BoxFit? fit;
if (url.startsWith('solink://')) {
final segments = url.replaceFirst('solink://', '').split('/');
switch (segments[0]) {
case 'stickers':
double radius = 8;
final StickerProvider sticker = Get.find();
// Adjust sticker size based on the sticker count in this paragraph
if (stickerMatch.length <= 1 && isOnlySticker) {
width = 128;
height = 128;
} else if (stickerMatch.length <= 3 && isOnlySticker) {
width = 32;
height = 32;
} else {
radius = 4;
width = 16;
height = 16;
}
fit = BoxFit.contain;
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 AutoCacheImage(
snapshot.data!.imageUrl,
width: width,
height: height,
fit: fit,
noErrorWidget: true,
);
},
),
),
).paddingSymmetric(vertical: 4);
case 'attachments':
const radius = BorderRadius.all(Radius.circular(8));
return LimitedBox(
maxHeight: MediaQuery.of(context).size.width,
child: ClipRRect(
borderRadius: radius,
child: AttachmentSelfContainedEntry(
isDense: true,
parentId: parentId,
rid: segments[1],
),
),
).paddingSymmetric(vertical: 4);
}
}
return AutoCacheImage(
url,
width: width,
height: height,
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,357 +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 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
@ -433,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(
@ -445,10 +101,14 @@ class _PostItemState extends State<PostItem> {
onChange: (size) {
setState(() => _contentHeight = size.height);
},
child: MarkdownTextContent(
parentId: 'p${item.id}',
content: item.body['content'],
isSelectable: widget.isContentSelectable,
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,
@ -486,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: [
@ -512,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: [
@ -534,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(
@ -546,11 +212,17 @@ class _PostItemState extends State<PostItem> {
onChange: (size) {
setState(() => _contentHeight = size.height);
},
child: MarkdownTextContent(
parentId: 'p${item.id}-embed',
content: item.body['content'],
isSelectable: widget.isContentSelectable,
).paddingOnly(left: 12, right: 8),
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)
@ -564,10 +236,14 @@ class _PostItemState extends State<PostItem> {
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Theme.of(context).colorScheme.surface,
Theme.of(context)
.colorScheme
.surface
(widget.backgroundColor ??
Theme.of(context)
.colorScheme
.surface),
(widget.backgroundColor ??
Theme.of(context)
.colorScheme
.surface)
.withOpacity(0),
],
),
@ -581,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),
],
@ -605,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,
@ -649,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

@ -21,19 +21,19 @@ PODS:
- Firebase/Messaging (11.0.0):
- Firebase/CoreOnly
- FirebaseMessaging (~> 11.0.0)
- firebase_analytics (11.3.1):
- firebase_analytics (11.3.2):
- Firebase/Analytics (= 11.0.0)
- firebase_core
- FlutterMacOS
- firebase_core (3.4.1):
- firebase_core (3.5.0):
- Firebase/CoreOnly (~> 11.0.0)
- FlutterMacOS
- firebase_crashlytics (4.1.1):
- firebase_crashlytics (4.1.2):
- Firebase/CoreOnly (~> 11.0.0)
- Firebase/Crashlytics (~> 11.0.0)
- firebase_core
- FlutterMacOS
- firebase_messaging (15.1.1):
- firebase_messaging (15.1.2):
- Firebase/CoreOnly (~> 11.0.0)
- Firebase/Messaging (~> 11.0.0)
- firebase_core
@ -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):
@ -338,10 +338,10 @@ SPEC CHECKSUMS:
device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720
file_selector_macos: 54fdab7caa3ac3fc43c9fac4d7d8d231277f8cf2
Firebase: 9f574c08c2396885b5e7e100ed4293d956218af9
firebase_analytics: 2169e28bb3ee1f765efe0fd4f5b5f625d92fda13
firebase_core: 3f80bec72646b26618f0497e74ce8bcd608f03ca
firebase_crashlytics: 6b1e511297406a6efd4405dc6301da8843b9dfe1
firebase_messaging: ce70e6615f0cd906d80b7a651b960d76dad6de56
firebase_analytics: a2d0d907566e4a48e27745317f05b4b7db85edd9
firebase_core: c55630cdb8a01cf49eae741dd4bc8c93bdd546b8
firebase_crashlytics: a359f1f75a23e560c8c97b743ab684ba795d7688
firebase_messaging: 12726b352752420d073ff075e328cc2f0ca14c47
FirebaseAnalytics: 27eb78b97880ea4a004839b9bac0b58880f5a92a
FirebaseCore: 3cf438f431f18c12cdf2aaf64434648b63f7e383
FirebaseCoreExtension: cda74ddfb001224bd8fd1d6e74698b4ed07803de
@ -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

@ -13,15 +13,23 @@ packages:
dependency: transitive
description:
name: _flutterfire_internals
sha256: ddc6f775260b89176d329dee26f88b9469ef46aa3228ff6a0b91caf2b2989692
sha256: "5fdcea390499dd26c808a3c662df5f4208d6bbc0643072eee94f1476249e2818"
url: "https://pub.dev"
source: hosted
version: "1.3.42"
version: "1.3.43"
_macros:
dependency: transitive
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:
@ -490,114 +498,114 @@ packages:
dependency: "direct main"
description:
name: firebase_analytics
sha256: "7b5ae39d853ead76f9d030dc23389bfec4ea826d7cccb4eea4873dcb0cdd172b"
sha256: "9c52c099e9cbb852c7f1d2302c7eb34a15758834eca1877f7a779e6082f9882d"
url: "https://pub.dev"
source: hosted
version: "11.3.1"
version: "11.3.2"
firebase_analytics_platform_interface:
dependency: transitive
description:
name: firebase_analytics_platform_interface
sha256: "0205e05bb37abd29d5dec5cd89aeb04f3f58bf849aad21dd938be0507d52a40c"
sha256: "4ec57aee951832fdbf10ca722bbb83fe0001d6168d6c4cfea9ccee0df6afb1e0"
url: "https://pub.dev"
source: hosted
version: "4.2.3"
version: "4.2.4"
firebase_analytics_web:
dependency: transitive
description:
name: firebase_analytics_web
sha256: "434807f8b30526e21cc062410c28ee5c6680a13626c4443b5ffede29f84b0c74"
sha256: "95c594fb1e8960992a607b135459e2f9ea3683dd8d01e6b845cace7c6665ec7e"
url: "https://pub.dev"
source: hosted
version: "0.5.10"
version: "0.5.10+1"
firebase_core:
dependency: "direct main"
description:
name: firebase_core
sha256: "40921de9795fbf5887ed5c0adfdf4972d5a8d7ae7e1b2bb98dea39bc02626a88"
sha256: c7de9354eb2cd8bfe8059e1112174c9a58beda7051807207306bc48283277cfb
url: "https://pub.dev"
source: hosted
version: "3.4.1"
version: "3.5.0"
firebase_core_platform_interface:
dependency: transitive
description:
name: firebase_core_platform_interface
sha256: f7d7180c7f99babd4b4c517754d41a09a4943a0f7a69b65c894ca5c68ba66315
sha256: e30da58198a6d4b49d5bce4e852f985c32cb10db329ebef9473db2b9f09ce810
url: "https://pub.dev"
source: hosted
version: "5.2.1"
version: "5.3.0"
firebase_core_web:
dependency: transitive
description:
name: firebase_core_web
sha256: f4ee170441ca141c5f9ee5ad8737daba3ee9c8e7efb6902aee90b4fbd178ce25
sha256: f967a7138f5d2ffb1ce15950e2a382924239eaa521150a8f144af34e68b3b3e5
url: "https://pub.dev"
source: hosted
version: "2.18.0"
version: "2.18.1"
firebase_crashlytics:
dependency: "direct main"
description:
name: firebase_crashlytics
sha256: c4fdbb14ba6f36794f89dc27fb5c759c9cc67ecbaeb079edc4dba515bbf9f555
sha256: "7821f9d8373b91f2a5ca8214226891d5870e196a7376f66350f65204387e9c15"
url: "https://pub.dev"
source: hosted
version: "4.1.1"
version: "4.1.2"
firebase_crashlytics_platform_interface:
dependency: transitive
description:
name: firebase_crashlytics_platform_interface
sha256: "891d6f7ba4b93672d0e1265f27b6a9dccd56ba2cc30ce6496586b32d1d8770ac"
sha256: "8ed539fd9e9b6c07905f9f44c5f6d4785ac841a5a8195bd35586c8b1d54ec26d"
url: "https://pub.dev"
source: hosted
version: "3.6.42"
version: "3.6.43"
firebase_messaging:
dependency: "direct main"
description:
name: firebase_messaging
sha256: cc02c4afd6510cd84586020670140c4a23fbe52af16cd260ccf8ede101bb8d1b
sha256: "32ce60b747e755b48d7112d728d4f736ba82acd98ec825626558d444d385fa3a"
url: "https://pub.dev"
source: hosted
version: "15.1.1"
version: "15.1.2"
firebase_messaging_platform_interface:
dependency: transitive
description:
name: firebase_messaging_platform_interface
sha256: d8a4984635f09213302243ea670fe5c42f3261d7d8c7c0a5f7dcd5d6c84be459
sha256: "69671a0f1a40c7b7c46ad0283e6f34ca2a59a0362ca14a240a4ea01c46e8a521"
url: "https://pub.dev"
source: hosted
version: "4.5.44"
version: "4.5.45"
firebase_messaging_web:
dependency: transitive
description:
name: firebase_messaging_web
sha256: "258b9d637965db7855299b123533609ed95e52350746a723dfd1d8d6f3fac678"
sha256: "6890111a9d01d7b13d0f6fe74850812c334e903d2c80a2d9356a3abb8c3a9e9a"
url: "https://pub.dev"
source: hosted
version: "3.9.0"
version: "3.9.1"
firebase_performance:
dependency: "direct main"
description:
name: firebase_performance
sha256: "879ce4d83242cb7d1ec67a8b45daa351f230211778e34eeea979894839c4832a"
sha256: ed9a408b6d1f221fc0e2890dcf0733b604d1aea6cd3b897f97dd3f889f01ddfc
url: "https://pub.dev"
source: hosted
version: "0.10.0+6"
version: "0.10.0+7"
firebase_performance_platform_interface:
dependency: transitive
description:
name: firebase_performance_platform_interface
sha256: ac68eba644f593903a931ba7f26f0677b725d5a60f8f7bc0fed01d88a11d1463
sha256: bfcfbfcefeaf3853a72602675b786e13a609d49ac70fc325d302b5794b8b0c06
url: "https://pub.dev"
source: hosted
version: "0.1.4+42"
version: "0.1.4+43"
firebase_performance_web:
dependency: transitive
description:
name: firebase_performance_web
sha256: ff53b9c5d8601fc983d0173b88fdfd8abcc74948f0a3753f3c1ec276b680f23c
sha256: "2ac9e44a1be7b20f1a7a3912d84bf2e1ec76398f2dadc07b6b7c3173d590e329"
url: "https://pub.dev"
source: hosted
version: "0.1.7"
version: "0.1.7+1"
fixnum:
dependency: transitive
description:
@ -635,6 +643,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.5.0"
flutter_app_update:
dependency: "direct main"
description:
name: flutter_app_update
sha256: "3650f57571e9f05d51f008f3fc9d556351910348f8011de7734b56fa74ccfee6"
url: "https://pub.dev"
source: hosted
version: "3.1.1"
flutter_background_service:
dependency: "direct main"
description:
@ -735,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:
@ -751,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:
@ -1193,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:
@ -2130,6 +2146,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.4"
version:
dependency: "direct main"
description:
name: version
sha256: "3d4140128e6ea10d83da32fef2fa4003fccbf6852217bb854845802f04191f94"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
very_good_infinite_list:
dependency: "direct main"
description:

View File

@ -2,7 +2,7 @@ name: solian
description: "The Solar Network App"
publish_to: "none"
version: 1.2.1+40
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
@ -80,13 +80,16 @@ dependencies:
path_provider: ^2.1.4
flutter_background_service: ^5.0.10
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