Compare commits
36 Commits
Author | SHA1 | Date | |
---|---|---|---|
11c913af60 | |||
db8f0d63e1 | |||
4036a79995 | |||
859bbd09e0 | |||
60033fdef3 | |||
9c3d181deb | |||
9e6829bd5a | |||
f50461a7f7 | |||
147879e4d8 | |||
f353c05cb5 | |||
ac60043ca7 | |||
8d79274b0c | |||
ad4e4071fa | |||
c59f77c877 | |||
16047a7d57 | |||
fdc68fc5e1 | |||
bbee825cf4 | |||
2673c11046 | |||
3ac6822ab6 | |||
7a5fd2e468 | |||
e1ddd22e4e | |||
22b2ae32e9 | |||
9d5c452eae | |||
0fdb1e4ead | |||
724bd6592e | |||
2d347e0d41 | |||
de39799301 | |||
4b921602a2 | |||
6cde218393 | |||
c896185af0 | |||
4cbeafd447 | |||
91a32e6736 | |||
befc647b03 | |||
16b2e3a0c7 | |||
0cc842c030 | |||
fb370a484d |
@ -4,3 +4,4 @@ android.enableJetifier=true
|
|||||||
android.defaults.buildfeatures.buildconfig=true
|
android.defaults.buildfeatures.buildconfig=true
|
||||||
android.nonTransitiveRClass=false
|
android.nonTransitiveRClass=false
|
||||||
android.nonFinalResIds=false
|
android.nonFinalResIds=false
|
||||||
|
kotlin.jvm.target.validation.mode = IGNORE
|
||||||
|
@ -22,9 +22,9 @@
|
|||||||
"explore": "Explore",
|
"explore": "Explore",
|
||||||
"posts": "Posts",
|
"posts": "Posts",
|
||||||
"unlink": "Unlink",
|
"unlink": "Unlink",
|
||||||
"feedSearch": "Search Feed",
|
"postSearch": "Search Post",
|
||||||
"feedSearchWithTag": "Searching with tag #@key",
|
"postSearchWithTag": "Searching with tag #@key",
|
||||||
"feedSearchWithCategory": "Searching in category @category",
|
"postSearchWithCategory": "Searching in category @category",
|
||||||
"feedUnreadCount": "@count posts you may missed",
|
"feedUnreadCount": "@count posts you may missed",
|
||||||
"messages": "Messages",
|
"messages": "Messages",
|
||||||
"messagesUnreadCount": "@count messages unread",
|
"messagesUnreadCount": "@count messages unread",
|
||||||
@ -98,6 +98,8 @@
|
|||||||
"accountFriendBlocked": "Friend blocklist",
|
"accountFriendBlocked": "Friend blocklist",
|
||||||
"accountFriendListHint": "Swipe left to decline, right to approve",
|
"accountFriendListHint": "Swipe left to decline, right to approve",
|
||||||
"accountFriendRequestSent": "Friend request sent, waiting for processing...",
|
"accountFriendRequestSent": "Friend request sent, waiting for processing...",
|
||||||
|
"accountBlocked": "Account has been blocked",
|
||||||
|
"accountUnblocked": "Account has been unblocked",
|
||||||
"accountSuspended": "Account was suspended",
|
"accountSuspended": "Account was suspended",
|
||||||
"accountSuspendedAt": "Account was suspended since @date",
|
"accountSuspendedAt": "Account was suspended since @date",
|
||||||
"aspectRatio": "Aspect Ratio",
|
"aspectRatio": "Aspect Ratio",
|
||||||
@ -428,9 +430,43 @@
|
|||||||
"preferencesApplied": "Preferences has been applied.",
|
"preferencesApplied": "Preferences has been applied.",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"updateAvailable": "Update available",
|
"updateAvailable": "Update available",
|
||||||
"updateAvailableDesc": "There is an update available (@version). Do you want to download and install it now? You can still use the app normally while waiting for the download to complete.",
|
"updateAvailableDesc": "There is an update available (@from to @to). Do you want to download and install it now? You can still use the app normally while waiting for the download to complete.",
|
||||||
"update": "Update",
|
"update": "Update",
|
||||||
"updateCheckStrictly": "Strict mode",
|
"updateCheckStrictly": "Strict mode",
|
||||||
"updateCheckStrictlyDesc": "If enabled, the app will ask for updating once the local version is different from remote one.",
|
"updateCheckStrictlyDesc": "If enabled, the app will ask for updating once the local version is different from remote one.",
|
||||||
"updateMayAvailable": "App version @version is available, you can update from app store or our website."
|
"updateMayAvailable": "App version @version is available, you can update from app store or our website.",
|
||||||
|
"updateNow": "Update now",
|
||||||
|
"termAccept": "I've read and agree to Solar Network's Terms",
|
||||||
|
"termAcceptDesc": "Including but not limited to \"User Agreement\" and \"Privacy Policy\"",
|
||||||
|
"termAcceptLink": "View terms",
|
||||||
|
"termAcceptNextWithAgree": "By clicking the \"Next\", it means you agree to our terms and its updates. You should already agreed with them while you sign up.",
|
||||||
|
"termRelated": "Related Terms",
|
||||||
|
"appDetails": "App Details",
|
||||||
|
"projectWebsite": "Project Website",
|
||||||
|
"iAmNotRobot": "I'm not a Robot",
|
||||||
|
"report": "Report",
|
||||||
|
"reportAbuse": "Report abuse",
|
||||||
|
"reportAbuseDesc": "Report any violation of service terms",
|
||||||
|
"reportAbuseResource": "Resource identifier",
|
||||||
|
"reportAbuseReason": "Report reason",
|
||||||
|
"reportSubmitted": "Report submitted, thank you for your contribution. We will send a notification about the result of the report within 24 hours for you.",
|
||||||
|
"accountDeletion": "Request account deletion",
|
||||||
|
"accountDeletionDesc": "Delete the current account and all its data. Note that this action is irreversible!",
|
||||||
|
"accountDeletionConfirm": "Confirm request account deletion",
|
||||||
|
"accountDeletionConfirmDesc": "Are you sure to delete account @account? You will receive a confirmation email with a link to confirm the deletion of the account within 24 hours. Note that this action is irreversible, and all data associated with the account will be deleted, and you should be careful about it.",
|
||||||
|
"accountDeletionRequested": "Account deletion requested, check your inbox to confirm the request.",
|
||||||
|
"slideToConfirm": "Slide to confirm",
|
||||||
|
"serviceStatus": "Status of Service",
|
||||||
|
"firstBootTime": "First boot at @time",
|
||||||
|
"rateTheApp": "Rate the app",
|
||||||
|
"rateTheAppDesc": "Rate Solar Network on the App Store to let us serve you better!",
|
||||||
|
"friendAdd": "Add as friend",
|
||||||
|
"blockUser": "Block user",
|
||||||
|
"unblockUser": "Unblock user",
|
||||||
|
"learnMoreAboutPerson": "Learn more about that person",
|
||||||
|
"global": "Global",
|
||||||
|
"all": "All",
|
||||||
|
"unablePreview": "Unable to preview",
|
||||||
|
"dashboardNav": "Dash",
|
||||||
|
"accountNav": "You"
|
||||||
}
|
}
|
||||||
|
@ -32,9 +32,9 @@
|
|||||||
"dashboard": "仪表盘",
|
"dashboard": "仪表盘",
|
||||||
"today": "今日",
|
"today": "今日",
|
||||||
"yesterday": "昨日",
|
"yesterday": "昨日",
|
||||||
"feedSearch": "搜索资讯",
|
"postSearch": "搜索帖子",
|
||||||
"feedSearchWithTag": "检索带有 #@key 标签的资讯",
|
"postSearchWithTag": "检索带有 #@key 标签的资讯",
|
||||||
"feedSearchWithCategory": "检索位于分类 @category 的资讯",
|
"postSearchWithCategory": "检索位于分类 @category 的资讯",
|
||||||
"feedUnreadCount": "@count 条你可能错过的帖子",
|
"feedUnreadCount": "@count 条你可能错过的帖子",
|
||||||
"messages": "消息",
|
"messages": "消息",
|
||||||
"messagesUnreadCount": "@count 条未读的消息",
|
"messagesUnreadCount": "@count 条未读的消息",
|
||||||
@ -98,6 +98,8 @@
|
|||||||
"accountFriendBlocked": "好友黑名单",
|
"accountFriendBlocked": "好友黑名单",
|
||||||
"accountFriendListHint": "左滑来拒绝,右滑来接受",
|
"accountFriendListHint": "左滑来拒绝,右滑来接受",
|
||||||
"accountFriendRequestSent": "好友请求已发送,等待处理对方中……",
|
"accountFriendRequestSent": "好友请求已发送,等待处理对方中……",
|
||||||
|
"accountBlocked": "已屏蔽账号",
|
||||||
|
"accountUnblocked": "已解除屏蔽账号",
|
||||||
"accountSuspended": "帐号被停用",
|
"accountSuspended": "帐号被停用",
|
||||||
"accountSuspendedAt": "该帐号自 @date 起被停用",
|
"accountSuspendedAt": "该帐号自 @date 起被停用",
|
||||||
"aspectRatio": "纵横比",
|
"aspectRatio": "纵横比",
|
||||||
@ -428,5 +430,39 @@
|
|||||||
"update": "更新",
|
"update": "更新",
|
||||||
"updateCheckStrictly": "严格模式",
|
"updateCheckStrictly": "严格模式",
|
||||||
"updateCheckStrictlyDesc": "如果启用,应用程序将会在本地版本与远程版本不同时询问更新,而不会检查版本号大小。",
|
"updateCheckStrictlyDesc": "如果启用,应用程序将会在本地版本与远程版本不同时询问更新,而不会检查版本号大小。",
|
||||||
"updateMayAvailable": "版本 @version 现已可用,你可以前往应用商店或是我们的官网下载更新。"
|
"updateNow": "立即更新",
|
||||||
|
"updateMayAvailable": "版本 @version 现已可用,你可以前往应用商店或是我们的官网下载更新。",
|
||||||
|
"termAccept": "我已阅读并同意 Solar Network 各项条款",
|
||||||
|
"termAcceptDesc": "包括但不限于《用户守则》和《隐私政策》",
|
||||||
|
"termAcceptLink": "浏览条款",
|
||||||
|
"termAcceptNextWithAgree": "点击 “下一步”,即表示你同意我们的各项条款,包括其之后的更新。你应该在注册时已经同意过了。",
|
||||||
|
"termRelated": "相关条款",
|
||||||
|
"projectWebsite": "项目网站",
|
||||||
|
"appDetails": "应用详情",
|
||||||
|
"iAmNotRobot": "我不是机器人",
|
||||||
|
"report": "举报",
|
||||||
|
"reportAbuse": "举报滥用",
|
||||||
|
"reportAbuseDesc": "举报任何违反服务条款的行为",
|
||||||
|
"reportAbuseResource": "举报的资源",
|
||||||
|
"reportAbuseReason": "举报的原因",
|
||||||
|
"reportSubmitted": "举报已提交,感谢你的贡献。我们将通过通知在 24 小时内通知该举报的处理结果。",
|
||||||
|
"accountDeletion": "请求删除账号",
|
||||||
|
"accountDeletionDesc": "删除目前登陆的账号,及其所有的数据。注意,该操作不可撤销!",
|
||||||
|
"accountDeletionConfirm": "确认账号删除请求",
|
||||||
|
"accountDeletionConfirmDesc": "你确定要删除账号 @account 吗?你将会在其绑定的主要邮件地址收到一封包含着确认删除账号连接的邮件,在二十四小时内使用该连接即可完成删除账号。注意,本操作不可撤销,并且账号创建或关联的所有数据都将被删除,请三思而后行。",
|
||||||
|
"accountDeletionRequested": "已请求删除账号,检查你的收件箱来确认请求。",
|
||||||
|
"slideToConfirm": "滑动来确认",
|
||||||
|
"serviceStatus": "服务状态",
|
||||||
|
"firstBootTime": "首次启动于 @time",
|
||||||
|
"rateTheApp": "给应用评分",
|
||||||
|
"rateTheAppDesc": "在 App Store 上给 Solar Network 评分,让我们更好地为您服务吧!",
|
||||||
|
"friendAdd": "添加好友",
|
||||||
|
"blockUser": "屏蔽用户",
|
||||||
|
"unblockUser": "解除屏蔽用户",
|
||||||
|
"learnMoreAboutPerson": "了解关于 TA 的更多",
|
||||||
|
"global": "全局",
|
||||||
|
"all": "全部",
|
||||||
|
"unablePreview": "无法预览",
|
||||||
|
"dashboardNav": "仪表盘",
|
||||||
|
"accountNav": "您"
|
||||||
}
|
}
|
||||||
|
@ -227,7 +227,9 @@ PODS:
|
|||||||
- TOCropViewController (~> 2.7.4)
|
- TOCropViewController (~> 2.7.4)
|
||||||
- image_picker_ios (0.0.1):
|
- image_picker_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- livekit_client (2.2.5):
|
- in_app_review (0.2.0):
|
||||||
|
- Flutter
|
||||||
|
- livekit_client (2.2.6):
|
||||||
- Flutter
|
- Flutter
|
||||||
- WebRTC-SDK (= 125.6422.04)
|
- WebRTC-SDK (= 125.6422.04)
|
||||||
- media_kit_libs_ios_video (1.0.4):
|
- media_kit_libs_ios_video (1.0.4):
|
||||||
@ -318,6 +320,7 @@ DEPENDENCIES:
|
|||||||
- gal (from `.symlinks/plugins/gal/darwin`)
|
- gal (from `.symlinks/plugins/gal/darwin`)
|
||||||
- image_cropper (from `.symlinks/plugins/image_cropper/ios`)
|
- image_cropper (from `.symlinks/plugins/image_cropper/ios`)
|
||||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||||
|
- in_app_review (from `.symlinks/plugins/in_app_review/ios`)
|
||||||
- livekit_client (from `.symlinks/plugins/livekit_client/ios`)
|
- livekit_client (from `.symlinks/plugins/livekit_client/ios`)
|
||||||
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
|
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
|
||||||
- media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`)
|
- media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`)
|
||||||
@ -406,6 +409,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/image_cropper/ios"
|
:path: ".symlinks/plugins/image_cropper/ios"
|
||||||
image_picker_ios:
|
image_picker_ios:
|
||||||
:path: ".symlinks/plugins/image_picker_ios/ios"
|
:path: ".symlinks/plugins/image_picker_ios/ios"
|
||||||
|
in_app_review:
|
||||||
|
:path: ".symlinks/plugins/in_app_review/ios"
|
||||||
livekit_client:
|
livekit_client:
|
||||||
:path: ".symlinks/plugins/livekit_client/ios"
|
:path: ".symlinks/plugins/livekit_client/ios"
|
||||||
media_kit_libs_ios_video:
|
media_kit_libs_ios_video:
|
||||||
@ -482,7 +487,8 @@ SPEC CHECKSUMS:
|
|||||||
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
|
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
|
||||||
image_cropper: 37d40f62177c101ff4c164906d259ea2c3aa70cf
|
image_cropper: 37d40f62177c101ff4c164906d259ea2c3aa70cf
|
||||||
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
||||||
livekit_client: 9c8080879256a0fb16da13c9be4845248209d896
|
in_app_review: 318597b3a06c22bb46dc454d56828c85f444f99d
|
||||||
|
livekit_client: 20e01637431bc108dad451c8a11c1d206e1dd2cd
|
||||||
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
|
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
|
||||||
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
|
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
|
||||||
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
|
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
|
||||||
|
@ -1,87 +1,92 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
<array>
|
<array>
|
||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleURLName</key>
|
<key>CFBundleURLName</key>
|
||||||
<string></string>
|
<string></string>
|
||||||
<key>CFBundleTypeRole</key>
|
<key>CFBundleTypeRole</key>
|
||||||
<string>Editor</string>
|
<string>Editor</string>
|
||||||
<key>CFBundleURLSchemes</key>
|
<key>CFBundleURLSchemes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>solink</string>
|
<string>solink</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>FirebaseMessagingAutoInitEnabled</key>
|
<key>FirebaseMessagingAutoInitEnabled</key>
|
||||||
<false/>
|
<false/>
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>Solian</string>
|
<string>Solian</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>solian</string>
|
<string>solian</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
<false/>
|
<false/>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>NSCameraUsageDescription</key>
|
<key>NSCameraUsageDescription</key>
|
||||||
<string>Allow you take photo/video for your message or post</string>
|
<string>Allow you take photo/video for your message or post</string>
|
||||||
<key>NSMicrophoneUsageDescription</key>
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
<string>Allow you record audio for your message or post</string>
|
<string>Allow you record audio for your message or post</string>
|
||||||
<key>NSPhotoLibraryUsageDescription</key>
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
<string>Allow you add photo to your message or post</string>
|
<string>Allow you add photo to your message or post</string>
|
||||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>UIBackgroundModes</key>
|
<key>UIBackgroundModes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>audio</string>
|
<string>audio</string>
|
||||||
<string>fetch</string>
|
<string>fetch</string>
|
||||||
<string>remote-notification</string>
|
<string>remote-notification</string>
|
||||||
<string>voip</string>
|
<string>voip</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>LaunchScreen</string>
|
<string>LaunchScreen</string>
|
||||||
<key>UIMainStoryboardFile</key>
|
<key>UIMainStoryboardFile</key>
|
||||||
<string>Main</string>
|
<string>Main</string>
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
<array>
|
<array>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
</array>
|
</array>
|
||||||
<key>FlutterDeepLinkingEnabled</key>
|
<key>FlutterDeepLinkingEnabled</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>NSUserActivityTypes</key>
|
<key>NSUserActivityTypes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>INSendMessageIntent</string>
|
<string>INSendMessageIntent</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
<array>
|
<array>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UIStatusBarHidden</key>
|
<key>CFBundleLocalizations</key>
|
||||||
<false/>
|
<array>
|
||||||
</dict>
|
<string>zh_CN</string>
|
||||||
|
<string>en</string>
|
||||||
|
</array>
|
||||||
|
<key>UIStatusBarHidden</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
@ -1,15 +1,17 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:developer';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:in_app_review/in_app_review.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:solian/exceptions/request.dart';
|
||||||
import 'package:solian/exts.dart';
|
import 'package:solian/exts.dart';
|
||||||
import 'package:solian/platform.dart';
|
import 'package:solian/platform.dart';
|
||||||
import 'package:solian/providers/auth.dart';
|
import 'package:solian/providers/auth.dart';
|
||||||
import 'package:solian/providers/content/channel.dart';
|
|
||||||
import 'package:solian/providers/content/realm.dart';
|
import 'package:solian/providers/content/realm.dart';
|
||||||
import 'package:solian/providers/relation.dart';
|
import 'package:solian/providers/relation.dart';
|
||||||
import 'package:solian/providers/theme_switcher.dart';
|
import 'package:solian/providers/theme_switcher.dart';
|
||||||
@ -41,6 +43,106 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
|
|||||||
|
|
||||||
final Completer _bootCompleter = Completer();
|
final Completer _bootCompleter = Completer();
|
||||||
|
|
||||||
|
void _requestRating() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
if (prefs.containsKey('first_boot_time')) {
|
||||||
|
final rawTime = prefs.getString('first_boot_time');
|
||||||
|
final time = DateTime.tryParse(rawTime ?? '');
|
||||||
|
if (time != null &&
|
||||||
|
time.isBefore(DateTime.now().subtract(const Duration(days: 3)))) {
|
||||||
|
final inAppReview = InAppReview.instance;
|
||||||
|
if (prefs.getBool('rating_requested') == true) return;
|
||||||
|
if (await inAppReview.isAvailable()) {
|
||||||
|
await inAppReview.requestReview();
|
||||||
|
prefs.setBool('rating_requested', true);
|
||||||
|
} else {
|
||||||
|
log('Unable request app review, unavailable');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
prefs.setString('first_boot_time', DateTime.now().toIso8601String());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateNow(String localVersionString, String remoteVersionString) {
|
||||||
|
context
|
||||||
|
.showConfirmDialog(
|
||||||
|
'updateAvailable'.tr,
|
||||||
|
'updateAvailableDesc'.trParams({
|
||||||
|
'from': localVersionString,
|
||||||
|
'to': remoteVersionString,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.then((result) {
|
||||||
|
if (result) {
|
||||||
|
final model = UpdateModel(
|
||||||
|
'https://files.solsynth.dev/d/production01/solian/app-arm64-v8a-release.apk',
|
||||||
|
'solian-app-arm64-v8a-release.apk',
|
||||||
|
'ic_launcher',
|
||||||
|
'https://testflight.apple.com/join/YJ0lmN6O',
|
||||||
|
);
|
||||||
|
AzhonAppUpdate.update(model);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _checkForUpdate() async {
|
||||||
|
if (PlatformInfo.isWeb) return;
|
||||||
|
try {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final info = await PackageInfo.fromPlatform();
|
||||||
|
final localVersionString = '${info.version}+${info.buildNumber}';
|
||||||
|
final resp = await GetConnect(
|
||||||
|
timeout: const Duration(seconds: 60),
|
||||||
|
).get(
|
||||||
|
'https://git.solsynth.dev/api/v1/repos/hydrogen/solian/tags?page=1&limit=1',
|
||||||
|
);
|
||||||
|
if (resp.statusCode != 200) {
|
||||||
|
throw RequestException(resp);
|
||||||
|
}
|
||||||
|
final remoteVersionString =
|
||||||
|
(resp.body as List).firstOrNull?['name'] ?? '0.0.0+0';
|
||||||
|
final remoteVersion = Version.parse(remoteVersionString.split('+').first);
|
||||||
|
final localVersion = Version.parse(localVersionString.split('+').first);
|
||||||
|
final remoteBuildNumber =
|
||||||
|
int.tryParse(remoteVersionString.split('+').last) ?? 0;
|
||||||
|
final localBuildNumber =
|
||||||
|
int.tryParse(localVersionString.split('+').last) ?? 0;
|
||||||
|
final strictUpdate = prefs.getBool('check_update_strictly') ?? false;
|
||||||
|
if (remoteVersion > localVersion ||
|
||||||
|
(remoteVersion == localVersion &&
|
||||||
|
remoteBuildNumber > localBuildNumber) ||
|
||||||
|
(remoteVersionString != localVersionString && strictUpdate)) {
|
||||||
|
if (PlatformInfo.isAndroid) {
|
||||||
|
_updateNow(localVersionString, remoteVersionString);
|
||||||
|
} else {
|
||||||
|
context.showInfoDialog(
|
||||||
|
'updateAvailable'.tr,
|
||||||
|
'bsCheckForUpdateDesc'.tr,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (remoteVersionString != localVersionString) {
|
||||||
|
_bootCompleter.future.then((_) {
|
||||||
|
context.showSnackbar(
|
||||||
|
'updateMayAvailable'.trParams({
|
||||||
|
'version': remoteVersionString,
|
||||||
|
}),
|
||||||
|
action: PlatformInfo.isAndroid
|
||||||
|
? SnackBarAction(
|
||||||
|
label: 'updateNow'.tr,
|
||||||
|
onPressed: () {
|
||||||
|
_updateNow(localVersionString, remoteVersionString);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
context.showErrorDialog('Unable to check update: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
late final List<({String label, Future<void> Function() action})> _periods = [
|
late final List<({String label, Future<void> Function() action})> _periods = [
|
||||||
(
|
(
|
||||||
label: 'bsLoadingTheme',
|
label: 'bsLoadingTheme',
|
||||||
@ -48,68 +150,6 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
|
|||||||
await context.read<ThemeSwitcher>().restoreTheme();
|
await context.read<ThemeSwitcher>().restoreTheme();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
(
|
|
||||||
label: 'bsCheckForUpdate',
|
|
||||||
action: () async {
|
|
||||||
if (PlatformInfo.isWeb) return;
|
|
||||||
try {
|
|
||||||
final prefs = await SharedPreferences.getInstance();
|
|
||||||
final info = await PackageInfo.fromPlatform();
|
|
||||||
final localVersionString = '${info.version}+${info.buildNumber}';
|
|
||||||
final resp = await GetConnect().get(
|
|
||||||
'https://git.solsynth.dev/api/v1/repos/hydrogen/solian/tags?page=1&limit=1',
|
|
||||||
);
|
|
||||||
final remoteVersionString =
|
|
||||||
(resp.body as List).firstOrNull?['name'] ?? '0.0.0';
|
|
||||||
final remoteVersion = Version.parse(remoteVersionString ?? '0.0.0');
|
|
||||||
final localVersion =
|
|
||||||
Version.parse(localVersionString.split('+').first);
|
|
||||||
final strictUpdate = prefs.getBool('check_update_strictly') ?? false;
|
|
||||||
if (remoteVersion > localVersion ||
|
|
||||||
(remoteVersionString != localVersionString && strictUpdate)) {
|
|
||||||
setState(() {
|
|
||||||
_isErrored = true;
|
|
||||||
_subtitle = 'bsCheckForUpdateDesc'.tr;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (PlatformInfo.isAndroid) {
|
|
||||||
context
|
|
||||||
.showConfirmDialog(
|
|
||||||
'updateAvailable'.tr,
|
|
||||||
'updateAvailableDesc'.trParams({
|
|
||||||
'from': localVersionString,
|
|
||||||
'to': remoteVersionString,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.then((result) {
|
|
||||||
if (result) {
|
|
||||||
final model = UpdateModel(
|
|
||||||
'https://files.solsynth.dev/d/production01/solian/app-arm64-v8a-release.apk',
|
|
||||||
'solian-app-arm64-v8a-release.apk',
|
|
||||||
'ic_launcher',
|
|
||||||
'https://testflight.apple.com/join/YJ0lmN6O',
|
|
||||||
);
|
|
||||||
AzhonAppUpdate.update(model);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setState(() {
|
|
||||||
_isErrored = true;
|
|
||||||
_subtitle = 'bsCheckForUpdateDesc'.tr;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (remoteVersionString != localVersionString) {
|
|
||||||
_bootCompleter.future.then((_) {
|
|
||||||
context.showSnackbar('updateMayAvailable'.trParams({
|
|
||||||
'version': remoteVersionString,
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
context.showErrorDialog('Unable to check update: $e');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
(
|
(
|
||||||
label: 'bsCheckingServer',
|
label: 'bsCheckingServer',
|
||||||
action: () async {
|
action: () async {
|
||||||
@ -157,8 +197,6 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
|
|||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
try {
|
try {
|
||||||
await Future.wait([
|
await Future.wait([
|
||||||
if (auth.isAuthorized.isTrue)
|
|
||||||
Get.find<ChannelProvider>().refreshAvailableChannel(),
|
|
||||||
if (auth.isAuthorized.isTrue)
|
if (auth.isAuthorized.isTrue)
|
||||||
Get.find<RelationshipProvider>().refreshRelativeList(),
|
Get.find<RelationshipProvider>().refreshRelativeList(),
|
||||||
if (auth.isAuthorized.isTrue)
|
if (auth.isAuthorized.isTrue)
|
||||||
@ -207,6 +245,10 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_runPeriods();
|
_runPeriods();
|
||||||
|
_checkForUpdate();
|
||||||
|
_bootCompleter.future.then((_) {
|
||||||
|
_requestRating();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:action_slider/action_slider.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:solian/exceptions/request.dart';
|
import 'package:solian/exceptions/request.dart';
|
||||||
import 'package:solian/exceptions/unauthorized.dart';
|
import 'package:solian/exceptions/unauthorized.dart';
|
||||||
@ -73,6 +75,47 @@ extension AppExtensions on BuildContext {
|
|||||||
false;
|
false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> showSlideToConfirmDialog(String title, body) async {
|
||||||
|
return await showDialog<bool>(
|
||||||
|
useRootNavigator: true,
|
||||||
|
context: this,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
title: Text(title, textAlign: TextAlign.center),
|
||||||
|
content: SizedBox(
|
||||||
|
width: double.maxFinite,
|
||||||
|
child: ListView(
|
||||||
|
shrinkWrap: true,
|
||||||
|
children: [
|
||||||
|
Text(body, textAlign: TextAlign.center),
|
||||||
|
const Gap(28),
|
||||||
|
ActionSlider.standard(
|
||||||
|
icon: const Icon(Icons.send),
|
||||||
|
iconAlignment: Alignment.center,
|
||||||
|
sliderBehavior: SliderBehavior.move,
|
||||||
|
actionThresholdType: ThresholdType.release,
|
||||||
|
toggleColor: Colors.red,
|
||||||
|
action: (controller) async {
|
||||||
|
controller.success();
|
||||||
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
|
Navigator.pop(ctx, true);
|
||||||
|
},
|
||||||
|
child: Text('slideToConfirm'.tr),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actionsAlignment: MainAxisAlignment.center,
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx, false),
|
||||||
|
child: Text('cancel'.tr),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
) ??
|
||||||
|
false;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> showErrorDialog(dynamic exception) {
|
Future<void> showErrorDialog(dynamic exception) {
|
||||||
Widget content = Text(exception.toString().capitalize!);
|
Widget content = Text(exception.toString().capitalize!);
|
||||||
if (exception is UnauthorizedException) {
|
if (exception is UnauthorizedException) {
|
||||||
|
@ -57,13 +57,16 @@ void main() async {
|
|||||||
|
|
||||||
Future<void> _initializeFirebase() async {
|
Future<void> _initializeFirebase() async {
|
||||||
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
|
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
|
||||||
FlutterError.onError = (errorDetails) {
|
if (PlatformInfo.isIOS || PlatformInfo.isAndroid || PlatformInfo.isMacOS) {
|
||||||
FirebaseCrashlytics.instance.recordFlutterFatalError(errorDetails);
|
// Initialize firebase crashlytics for the platform that supported
|
||||||
};
|
FlutterError.onError = (errorDetails) {
|
||||||
PlatformDispatcher.instance.onError = (error, stack) {
|
FirebaseCrashlytics.instance.recordFlutterFatalError(errorDetails);
|
||||||
FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
|
};
|
||||||
return true;
|
PlatformDispatcher.instance.onError = (error, stack) {
|
||||||
};
|
FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _initializeBackgroundNotificationService() async {
|
Future<void> _initializeBackgroundNotificationService() async {
|
||||||
|
@ -27,6 +27,10 @@ abstract class PlatformInfo {
|
|||||||
|
|
||||||
static bool get canCacheImage => isAndroid || isIOS || isMacOS;
|
static bool get canCacheImage => isAndroid || isIOS || isMacOS;
|
||||||
|
|
||||||
|
static bool get canRateTheApp => isIOS || isMacOS;
|
||||||
|
|
||||||
|
static bool get canCropImage => isIOS || isAndroid || isWeb;
|
||||||
|
|
||||||
static bool get canRecord => (isMobile || isMacOS);
|
static bool get canRecord => (isMobile || isMacOS);
|
||||||
|
|
||||||
static bool get canPushNotification => isAndroid || isIOS || isMacOS;
|
static bool get canPushNotification => isAndroid || isIOS || isMacOS;
|
||||||
@ -38,4 +42,4 @@ abstract class PlatformInfo {
|
|||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
return version;
|
return version;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -392,7 +392,7 @@ class ChatCallProvider extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future gotoScreen(BuildContext context) {
|
Future gotoScreen(BuildContext context) {
|
||||||
return Navigator.of(context, rootNavigator: true).push(
|
return Navigator.of(context).push(
|
||||||
MaterialPageRoute(builder: (context) => const CallScreen()),
|
MaterialPageRoute(builder: (context) => const CallScreen()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -9,25 +9,6 @@ import 'package:uuid/uuid.dart';
|
|||||||
|
|
||||||
class ChannelProvider extends GetxController {
|
class ChannelProvider extends GetxController {
|
||||||
RxBool isLoading = false.obs;
|
RxBool isLoading = false.obs;
|
||||||
RxList<Channel> availableChannels = RxList.empty(growable: true);
|
|
||||||
|
|
||||||
List<Channel> get groupChannels =>
|
|
||||||
availableChannels.where((x) => x.type == 0).toList();
|
|
||||||
List<Channel> get directChannels =>
|
|
||||||
availableChannels.where((x) => x.type == 1).toList();
|
|
||||||
|
|
||||||
Future<void> refreshAvailableChannel() async {
|
|
||||||
final AuthProvider auth = Get.find();
|
|
||||||
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
|
||||||
|
|
||||||
isLoading.value = true;
|
|
||||||
final resp = await listAvailableChannel();
|
|
||||||
isLoading.value = false;
|
|
||||||
|
|
||||||
availableChannels.value =
|
|
||||||
resp.body.map((x) => Channel.fromJson(x)).toList().cast<Channel>();
|
|
||||||
availableChannels.refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Response> getChannel(String alias, {String realm = 'global'}) async {
|
Future<Response> getChannel(String alias, {String realm = 'global'}) async {
|
||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
@ -89,18 +70,22 @@ class ChannelProvider extends GetxController {
|
|||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Response> listAvailableChannel({String scope = 'global'}) async {
|
Future<List<Channel>> listAvailableChannel({
|
||||||
|
String scope = 'global',
|
||||||
|
bool isDirect = false,
|
||||||
|
}) async {
|
||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
||||||
|
|
||||||
final client = await auth.configureClient('messaging');
|
final client = await auth.configureClient('messaging');
|
||||||
|
|
||||||
final resp = await client.get('/channels/$scope/me/available');
|
final resp =
|
||||||
|
await client.get('/channels/$scope/me/available?direct=$isDirect');
|
||||||
if (resp.statusCode != 200) {
|
if (resp.statusCode != 200) {
|
||||||
throw RequestException(resp);
|
throw RequestException(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
return resp;
|
return List.from(resp.body.map((x) => Channel.fromJson(x)));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Response> createChannel(String scope, dynamic payload) async {
|
Future<Response> createChannel(String scope, dynamic payload) async {
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:get/get.dart' hide Value;
|
import 'package:get/get.dart' hide Value;
|
||||||
import 'package:solian/exceptions/request.dart';
|
import 'package:solian/exceptions/request.dart';
|
||||||
@ -182,4 +185,26 @@ class MessagesFetchingProvider extends GetxController {
|
|||||||
..orderBy([(t) => OrderingTerm.desc(t.id)]))
|
..orderBy([(t) => OrderingTerm.desc(t.id)]))
|
||||||
.getSingleOrNull();
|
.getSingleOrNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Map<int, List<LocalMessageEventTableData>>>
|
||||||
|
getLastInAllChannels() async {
|
||||||
|
final database = Get.find<DatabaseProvider>().database;
|
||||||
|
final rows = await database.customSelect('''
|
||||||
|
SELECT id, channel_id, data, created_at
|
||||||
|
FROM ${database.localMessageEventTable.actualTableName}
|
||||||
|
WHERE (channel_id, created_at) IN (
|
||||||
|
SELECT channel_id, MAX(created_at)
|
||||||
|
FROM ${database.localMessageEventTable.actualTableName}
|
||||||
|
GROUP BY channel_id
|
||||||
|
)
|
||||||
|
''', readsFrom: {database.localMessageEventTable}).get();
|
||||||
|
return rows.map((row) {
|
||||||
|
return LocalMessageEventTableData(
|
||||||
|
id: row.read<int>('id'),
|
||||||
|
channelId: row.read<int>('channel_id'),
|
||||||
|
data: Event.fromJson(jsonDecode(row.read<String>('data'))),
|
||||||
|
createdAt: row.read<DateTime>('created_at'),
|
||||||
|
);
|
||||||
|
}).groupListsBy((x) => x.channelId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,19 @@ class RelationshipProvider extends GetxController {
|
|||||||
return _friends.any((x) => x.relatedId == account.id);
|
return _friends.any((x) => x.relatedId == account.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Relationship?> getRelationship(int relatedId) async {
|
||||||
|
final AuthProvider auth = Get.find();
|
||||||
|
final client = await auth.configureClient('auth');
|
||||||
|
final resp = await client.get('/users/me/relations/$relatedId');
|
||||||
|
if (resp.statusCode == 404) {
|
||||||
|
return null;
|
||||||
|
} else if (resp.statusCode != 200) {
|
||||||
|
throw RequestException(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Relationship.fromJson(resp.body);
|
||||||
|
}
|
||||||
|
|
||||||
Future<Response> listRelation() async {
|
Future<Response> listRelation() async {
|
||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
final client = await auth.configureClient('auth');
|
final client = await auth.configureClient('auth');
|
||||||
@ -38,7 +51,19 @@ class RelationshipProvider extends GetxController {
|
|||||||
return client.get('/users/me/relations?status=$status');
|
return client.get('/users/me/relations?status=$status');
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Response> makeFriend(String username) async {
|
Future<Relationship?> blockUser(String username) async {
|
||||||
|
final AuthProvider auth = Get.find();
|
||||||
|
final client = await auth.configureClient('auth');
|
||||||
|
final resp =
|
||||||
|
await client.post('/users/me/relations/block?related=$username', {});
|
||||||
|
if (resp.statusCode != 200) {
|
||||||
|
throw RequestException(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Relationship.fromJson(resp.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Relationship?> makeFriend(String username) async {
|
||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
final client = await auth.configureClient('auth');
|
final client = await auth.configureClient('auth');
|
||||||
final resp = await client.post('/users/me/relations?related=$username', {});
|
final resp = await client.post('/users/me/relations?related=$username', {});
|
||||||
@ -46,7 +71,7 @@ class RelationshipProvider extends GetxController {
|
|||||||
throw RequestException(resp);
|
throw RequestException(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
return resp;
|
return Relationship.fromJson(resp.body);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Response> handleRelation(
|
Future<Response> handleRelation(
|
||||||
@ -64,17 +89,17 @@ class RelationshipProvider extends GetxController {
|
|||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Response> editRelation(Relationship relationship, int status) async {
|
Future<Relationship?> editRelation(int relatedId, int status) async {
|
||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
final client = await auth.configureClient('auth');
|
final client = await auth.configureClient('auth');
|
||||||
final resp = await client.patch(
|
final resp = await client.put(
|
||||||
'/users/me/relations/${relationship.relatedId}',
|
'/users/me/relations/$relatedId',
|
||||||
{'status': status},
|
{'status': status},
|
||||||
);
|
);
|
||||||
if (resp.statusCode != 200) {
|
if (resp.statusCode != 200) {
|
||||||
throw RequestException(resp);
|
throw RequestException(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
return resp;
|
return Relationship.fromJson(resp.body);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,11 +23,13 @@ import 'package:solian/screens/realms.dart';
|
|||||||
import 'package:solian/screens/realms/realm_detail.dart';
|
import 'package:solian/screens/realms/realm_detail.dart';
|
||||||
import 'package:solian/screens/realms/realm_organize.dart';
|
import 'package:solian/screens/realms/realm_organize.dart';
|
||||||
import 'package:solian/screens/realms/realm_view.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/posts/post_editor.dart';
|
||||||
import 'package:solian/screens/settings.dart';
|
import 'package:solian/screens/settings.dart';
|
||||||
import 'package:solian/shells/root_shell.dart';
|
import 'package:solian/shells/root_shell.dart';
|
||||||
import 'package:solian/shells/title_shell.dart';
|
import 'package:solian/shells/title_shell.dart';
|
||||||
|
import 'package:solian/theme.dart';
|
||||||
|
import 'package:solian/widgets/sidebar/empty_placeholder.dart';
|
||||||
|
|
||||||
abstract class AppRouter {
|
abstract class AppRouter {
|
||||||
static GoRouter instance = GoRouter(
|
static GoRouter instance = GoRouter(
|
||||||
@ -78,13 +80,18 @@ abstract class AppRouter {
|
|||||||
builder: (context, state, child) => child,
|
builder: (context, state, child) => child,
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/feed',
|
path: '/explore',
|
||||||
name: 'feed',
|
name: 'explore',
|
||||||
builder: (context, state) => const FeedScreen(),
|
builder: (context, state) => const ExploreScreen(),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/feed/search',
|
path: '/drafts',
|
||||||
name: 'feedSearch',
|
name: 'draftBox',
|
||||||
|
builder: (context, state) => const DraftBoxScreen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/posts/search',
|
||||||
|
name: 'postSearch',
|
||||||
builder: (context, state) => TitleShell(
|
builder: (context, state) => TitleShell(
|
||||||
state: state,
|
state: state,
|
||||||
child: FeedSearchScreen(
|
child: FeedSearchScreen(
|
||||||
@ -93,11 +100,6 @@ abstract class AppRouter {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
GoRoute(
|
|
||||||
path: '/drafts',
|
|
||||||
name: 'draftBox',
|
|
||||||
builder: (context, state) => const DraftBoxScreen(),
|
|
||||||
),
|
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/posts/view/:id',
|
path: '/posts/view/:id',
|
||||||
name: 'postDetail',
|
name: 'postDetail',
|
||||||
@ -137,12 +139,15 @@ abstract class AppRouter {
|
|||||||
);
|
);
|
||||||
|
|
||||||
static final ShellRoute _chatRoute = ShellRoute(
|
static final ShellRoute _chatRoute = ShellRoute(
|
||||||
builder: (context, state, child) => child,
|
builder: (context, state, child) =>
|
||||||
|
AppTheme.isLargeScreen(context) ? ChatListShell(child: child) : child,
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/chat',
|
path: '/chat',
|
||||||
name: 'chat',
|
name: 'chat',
|
||||||
builder: (context, state) => const ChatScreen(),
|
builder: (context, state) => AppTheme.isLargeScreen(context)
|
||||||
|
? const EmptyPagePlaceholder()
|
||||||
|
: const ChatScreen(),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/chat/organize',
|
path: '/chat/organize',
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:solian/widgets/sized_container.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
class AboutScreen extends StatelessWidget {
|
class AboutScreen extends StatelessWidget {
|
||||||
@ -47,31 +51,58 @@ class AboutScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
Text('Copyright © ${DateTime.now().year} Solsynth LLC'),
|
Text('Copyright © ${DateTime.now().year} Solsynth LLC'),
|
||||||
const Gap(16),
|
const Gap(16),
|
||||||
TextButton(
|
CenteredContainer(
|
||||||
style: denseButtonStyle,
|
maxWidth: 280,
|
||||||
child: const Text('App Details'),
|
child: Wrap(
|
||||||
onPressed: () async {
|
spacing: 4,
|
||||||
final info = await PackageInfo.fromPlatform();
|
runSpacing: 4,
|
||||||
|
alignment: WrapAlignment.center,
|
||||||
|
children: [
|
||||||
|
TextButton(
|
||||||
|
style: denseButtonStyle,
|
||||||
|
child: Text('appDetails'.tr),
|
||||||
|
onPressed: () async {
|
||||||
|
final info = await PackageInfo.fromPlatform();
|
||||||
|
|
||||||
showAboutDialog(
|
showAboutDialog(
|
||||||
context: context,
|
context: context,
|
||||||
applicationVersion: '${info.version} (${info.buildNumber})',
|
applicationVersion:
|
||||||
applicationLegalese:
|
'${info.version} (${info.buildNumber})',
|
||||||
'The Solar Network App is an intuitive and self-hostable social network and computing platform. Experience the freedom of a user-friendly design that empowers you to create and connect with communities on your own terms. Embrace the future of social networking with a platform that prioritizes your independence and privacy.',
|
applicationLegalese:
|
||||||
applicationIcon: ClipRRect(
|
'The Solar Network App is an intuitive and open-source social network and computing platform. Experience the freedom of a user-friendly design that empowers you to create and connect with communities on your own terms. Embrace the future of social networking with a platform that prioritizes your independence and privacy.',
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
applicationIcon: ClipRRect(
|
||||||
child:
|
borderRadius:
|
||||||
Image.asset('assets/logo.png', width: 60, height: 60),
|
const BorderRadius.all(Radius.circular(16)),
|
||||||
|
child: Image.asset('assets/logo.png',
|
||||||
|
width: 60, height: 60),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
TextButton(
|
||||||
},
|
style: denseButtonStyle,
|
||||||
),
|
child: Text('projectWebsite'.tr),
|
||||||
TextButton(
|
onPressed: () {
|
||||||
style: denseButtonStyle,
|
launchUrlString(
|
||||||
child: const Text('Project Website'),
|
'https://solsynth.dev/products/solar-network');
|
||||||
onPressed: () {
|
},
|
||||||
launchUrlString('https://solsynth.dev/products/solar-network');
|
),
|
||||||
},
|
TextButton(
|
||||||
|
style: denseButtonStyle,
|
||||||
|
child: Text('termRelated'.tr),
|
||||||
|
onPressed: () {
|
||||||
|
launchUrlString('https://solsynth.dev/terms');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
style: denseButtonStyle,
|
||||||
|
child: Text('serviceStatus'.tr),
|
||||||
|
onPressed: () {
|
||||||
|
launchUrlString('https://status.solsynth.dev');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const Gap(16),
|
const Gap(16),
|
||||||
const Text(
|
const Text(
|
||||||
@ -81,6 +112,34 @@ class AboutScreen extends StatelessWidget {
|
|||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
FutureBuilder(
|
||||||
|
future: SharedPreferences.getInstance(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
const textStyle = TextStyle(
|
||||||
|
fontWeight: FontWeight.w300,
|
||||||
|
fontSize: 12,
|
||||||
|
);
|
||||||
|
if (!snapshot.hasData ||
|
||||||
|
!snapshot.data!.containsKey('first_boot_time')) {
|
||||||
|
return Text(
|
||||||
|
'firstBootTime'.trParams({'time': 'unknown'.tr}),
|
||||||
|
style: textStyle,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Text(
|
||||||
|
'firstBootTime'.trParams({
|
||||||
|
'time': DateFormat('yyyy-MM-dd').format(
|
||||||
|
DateTime.tryParse(
|
||||||
|
snapshot.data!.getString('first_boot_time')!,
|
||||||
|
)?.toLocal() ??
|
||||||
|
DateTime.now(),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
style: textStyle,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_animate/flutter_animate.dart';
|
import 'package:flutter_animate/flutter_animate.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
@ -9,6 +7,7 @@ import 'package:image_picker/image_picker.dart';
|
|||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:solian/exts.dart';
|
import 'package:solian/exts.dart';
|
||||||
import 'package:solian/models/attachment.dart';
|
import 'package:solian/models/attachment.dart';
|
||||||
|
import 'package:solian/platform.dart';
|
||||||
import 'package:solian/providers/auth.dart';
|
import 'package:solian/providers/auth.dart';
|
||||||
import 'package:solian/providers/content/attachment.dart';
|
import 'package:solian/providers/content/attachment.dart';
|
||||||
import 'package:solian/services.dart';
|
import 'package:solian/services.dart';
|
||||||
@ -77,36 +76,42 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
|||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
if (auth.isAuthorized.isFalse) return;
|
if (auth.isAuthorized.isFalse) return;
|
||||||
|
|
||||||
|
XFile file;
|
||||||
|
|
||||||
final image = await _imagePicker.pickImage(source: ImageSource.gallery);
|
final image = await _imagePicker.pickImage(source: ImageSource.gallery);
|
||||||
if (image == null) return;
|
if (image == null) return;
|
||||||
|
|
||||||
CroppedFile? croppedFile = await ImageCropper().cropImage(
|
if (PlatformInfo.canCropImage) {
|
||||||
sourcePath: image.path,
|
CroppedFile? croppedFile = await ImageCropper().cropImage(
|
||||||
uiSettings: [
|
sourcePath: image.path,
|
||||||
AndroidUiSettings(
|
uiSettings: [
|
||||||
toolbarTitle: 'cropImage'.tr,
|
AndroidUiSettings(
|
||||||
toolbarColor: Theme.of(context).colorScheme.primary,
|
toolbarTitle: 'cropImage'.tr,
|
||||||
toolbarWidgetColor: Theme.of(context).colorScheme.onPrimary,
|
toolbarColor: Theme.of(context).colorScheme.primary,
|
||||||
aspectRatioPresets: [
|
toolbarWidgetColor: Theme.of(context).colorScheme.onPrimary,
|
||||||
if (position == 'avatar') CropAspectRatioPreset.square,
|
aspectRatioPresets: [
|
||||||
if (position == 'banner') _BannerCropAspectRatioPreset(),
|
if (position == 'avatar') CropAspectRatioPreset.square,
|
||||||
],
|
if (position == 'banner') _BannerCropAspectRatioPreset(),
|
||||||
),
|
],
|
||||||
IOSUiSettings(
|
),
|
||||||
title: 'cropImage'.tr,
|
IOSUiSettings(
|
||||||
aspectRatioPresets: [
|
title: 'cropImage'.tr,
|
||||||
if (position == 'avatar') CropAspectRatioPreset.square,
|
aspectRatioPresets: [
|
||||||
if (position == 'banner') _BannerCropAspectRatioPreset(),
|
if (position == 'avatar') CropAspectRatioPreset.square,
|
||||||
],
|
if (position == 'banner') _BannerCropAspectRatioPreset(),
|
||||||
),
|
],
|
||||||
WebUiSettings(
|
),
|
||||||
context: context,
|
WebUiSettings(
|
||||||
),
|
context: context,
|
||||||
],
|
),
|
||||||
);
|
],
|
||||||
|
);
|
||||||
|
|
||||||
if (croppedFile == null) return;
|
if (croppedFile == null) return;
|
||||||
final file = File(croppedFile.path);
|
file = XFile(croppedFile.path);
|
||||||
|
} else {
|
||||||
|
file = XFile(image.path);
|
||||||
|
}
|
||||||
|
|
||||||
setState(() => _isBusy = true);
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
|
@ -1,13 +1,19 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:fl_chart/fl_chart.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
import 'package:solian/controllers/post_list_controller.dart';
|
import 'package:solian/controllers/post_list_controller.dart';
|
||||||
import 'package:solian/exts.dart';
|
import 'package:solian/exts.dart';
|
||||||
import 'package:solian/models/account.dart';
|
import 'package:solian/models/account.dart';
|
||||||
import 'package:solian/models/attachment.dart';
|
import 'package:solian/models/attachment.dart';
|
||||||
|
import 'package:solian/models/daily_sign.dart';
|
||||||
import 'package:solian/models/pagination.dart';
|
import 'package:solian/models/pagination.dart';
|
||||||
import 'package:solian/models/post.dart';
|
import 'package:solian/models/post.dart';
|
||||||
|
import 'package:solian/models/relations.dart';
|
||||||
import 'package:solian/models/subscription.dart';
|
import 'package:solian/models/subscription.dart';
|
||||||
import 'package:solian/providers/account_status.dart';
|
import 'package:solian/providers/account_status.dart';
|
||||||
import 'package:solian/providers/relation.dart';
|
import 'package:solian/providers/relation.dart';
|
||||||
@ -18,8 +24,10 @@ import 'package:solian/widgets/account/account_avatar.dart';
|
|||||||
import 'package:solian/widgets/account/account_heading.dart';
|
import 'package:solian/widgets/account/account_heading.dart';
|
||||||
import 'package:solian/widgets/app_bar_leading.dart';
|
import 'package:solian/widgets/app_bar_leading.dart';
|
||||||
import 'package:solian/widgets/attachments/attachment_list.dart';
|
import 'package:solian/widgets/attachments/attachment_list.dart';
|
||||||
|
import 'package:solian/widgets/daily_sign/history_chart.dart';
|
||||||
import 'package:solian/widgets/posts/post_list.dart';
|
import 'package:solian/widgets/posts/post_list.dart';
|
||||||
import 'package:solian/widgets/posts/post_warped_list.dart';
|
import 'package:solian/widgets/posts/post_warped_list.dart';
|
||||||
|
import 'package:solian/widgets/reports/abuse_report.dart';
|
||||||
import 'package:solian/widgets/sized_container.dart';
|
import 'package:solian/widgets/sized_container.dart';
|
||||||
|
|
||||||
class AccountProfilePage extends StatefulWidget {
|
class AccountProfilePage extends StatefulWidget {
|
||||||
@ -44,7 +52,9 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
|||||||
|
|
||||||
Account? _userinfo;
|
Account? _userinfo;
|
||||||
Subscription? _subscription;
|
Subscription? _subscription;
|
||||||
|
Relationship? _relationship;
|
||||||
List<Post> _pinnedPosts = List.empty();
|
List<Post> _pinnedPosts = List.empty();
|
||||||
|
List<DailySignRecord> _dailySignRecords = List.empty();
|
||||||
int _totalUpvote = 0, _totalDownvote = 0;
|
int _totalUpvote = 0, _totalDownvote = 0;
|
||||||
|
|
||||||
Future<void> _getSubscription() async {
|
Future<void> _getSubscription() async {
|
||||||
@ -54,10 +64,19 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
|||||||
setState(() => _isSubscribing = false);
|
setState(() => _isSubscribing = false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _getRelationship() async {
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
|
final relations = Get.find<RelationshipProvider>();
|
||||||
|
_relationship = await relations.getRelationship(_userinfo!.id);
|
||||||
|
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _getUserinfo() async {
|
Future<void> _getUserinfo() async {
|
||||||
setState(() => _isBusy = true);
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
var client = await ServiceFinder.configureClient('auth');
|
var client = await ServiceFinder.configureClient('id');
|
||||||
var resp = await client.get('/users/${widget.name}');
|
var resp = await client.get('/users/${widget.name}');
|
||||||
if (resp.statusCode != 200) {
|
if (resp.statusCode != 200) {
|
||||||
context.showErrorDialog(resp.bodyString).then((_) {
|
context.showErrorDialog(resp.bodyString).then((_) {
|
||||||
@ -67,7 +86,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
|||||||
_userinfo = Account.fromJson(resp.body);
|
_userinfo = Account.fromJson(resp.body);
|
||||||
}
|
}
|
||||||
|
|
||||||
client = await ServiceFinder.configureClient('interactive');
|
client = await ServiceFinder.configureClient('co');
|
||||||
resp = await client.get('/users/${widget.name}');
|
resp = await client.get('/users/${widget.name}');
|
||||||
if (resp.statusCode != 200) {
|
if (resp.statusCode != 200) {
|
||||||
context.showErrorDialog(resp.bodyString).then((_) {
|
context.showErrorDialog(resp.bodyString).then((_) {
|
||||||
@ -82,7 +101,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _getPinnedPosts() async {
|
Future<void> _getPinnedPosts() async {
|
||||||
final client = await ServiceFinder.configureClient('interactive');
|
final client = await ServiceFinder.configureClient('co');
|
||||||
final resp = await client.get('/users/${widget.name}/pin');
|
final resp = await client.get('/users/${widget.name}/pin');
|
||||||
if (resp.statusCode != 200) {
|
if (resp.statusCode != 200) {
|
||||||
context.showErrorDialog(resp.bodyString).then((_) {
|
context.showErrorDialog(resp.bodyString).then((_) {
|
||||||
@ -96,6 +115,80 @@ 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)) ?? [],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _subscribeToUser() async {
|
||||||
|
setState(() => _isSubscribing = true);
|
||||||
|
_subscription =
|
||||||
|
await Get.find<SubscriptionProvider>().subscribeToUser(_userinfo!.id);
|
||||||
|
setState(() => _isSubscribing = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _unsubscribeFromUser() async {
|
||||||
|
setState(() => _isSubscribing = true);
|
||||||
|
await Get.find<SubscriptionProvider>().unsubscribeFromUser(_userinfo!.id);
|
||||||
|
_subscription = null;
|
||||||
|
setState(() => _isSubscribing = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _makeFriend() async {
|
||||||
|
setState(() => _isMakingFriend = true);
|
||||||
|
try {
|
||||||
|
_relationship = await _relationshipProvider.makeFriend(widget.name);
|
||||||
|
context.showSnackbar(
|
||||||
|
'accountFriendRequestSent'.tr,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
context.showErrorDialog(e);
|
||||||
|
} finally {
|
||||||
|
setState(() => _isMakingFriend = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _blockUser() async {
|
||||||
|
setState(() => _isMakingFriend = true);
|
||||||
|
try {
|
||||||
|
_relationship = await _relationshipProvider.blockUser(widget.name);
|
||||||
|
context.showSnackbar(
|
||||||
|
'accountBlocked'.tr,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
context.showErrorDialog(e);
|
||||||
|
} finally {
|
||||||
|
setState(() => _isMakingFriend = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _unblockUser() async {
|
||||||
|
setState(() => _isMakingFriend = true);
|
||||||
|
try {
|
||||||
|
_relationship =
|
||||||
|
await _relationshipProvider.editRelation(_userinfo!.id, 1);
|
||||||
|
context.showSnackbar(
|
||||||
|
'accountUnblocked'.tr,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
context.showErrorDialog(e);
|
||||||
|
} finally {
|
||||||
|
setState(() => _isMakingFriend = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int get _userSocialCreditPoints {
|
int get _userSocialCreditPoints {
|
||||||
return _totalUpvote * 2 - _totalDownvote + _postController.postTotal.value;
|
return _totalUpvote * 2 - _totalDownvote + _postController.postTotal.value;
|
||||||
}
|
}
|
||||||
@ -127,28 +220,13 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
_getUserinfo().then((_) {
|
_getUserinfo().then((_) {
|
||||||
|
_getRelationship();
|
||||||
_getSubscription();
|
_getSubscription();
|
||||||
_getPinnedPosts();
|
_getPinnedPosts();
|
||||||
|
_getDailySignRecords();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildStatisticsEntry(String label, String content) {
|
|
||||||
return Expanded(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
label,
|
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
content,
|
|
||||||
style: Theme.of(context).textTheme.bodyLarge,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (_isBusy || _userinfo == null) {
|
if (_isBusy || _userinfo == null) {
|
||||||
@ -168,96 +246,71 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
|||||||
toolbarHeight: AppTheme.toolbarHeight(context),
|
toolbarHeight: AppTheme.toolbarHeight(context),
|
||||||
leadingWidth: 24,
|
leadingWidth: 24,
|
||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
flexibleSpace: Row(
|
flexibleSpace: SizedBox(
|
||||||
children: [
|
height: 56,
|
||||||
AppBarLeadingButton.adaptive(context) ?? const Gap(8),
|
child: Row(
|
||||||
const Gap(8),
|
children: [
|
||||||
if (_userinfo != null)
|
AppBarLeadingButton.adaptive(context) ?? const Gap(8),
|
||||||
AccountAvatar(content: _userinfo!.avatar, radius: 16),
|
const Gap(8),
|
||||||
const Gap(12),
|
if (_userinfo != null)
|
||||||
Expanded(
|
AccountAvatar(content: _userinfo!.avatar, radius: 16),
|
||||||
child: Column(
|
const Gap(12),
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
Expanded(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: Column(
|
||||||
children: [
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
if (_userinfo != null)
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
Text(
|
children: [
|
||||||
_userinfo!.nick,
|
if (_userinfo != null)
|
||||||
style: Theme.of(context).textTheme.bodyLarge,
|
Text(
|
||||||
),
|
_userinfo!.nick,
|
||||||
if (_userinfo != null)
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
Text(
|
),
|
||||||
'@${_userinfo!.name}',
|
if (_userinfo != null)
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
Text(
|
||||||
),
|
'@${_userinfo!.name}',
|
||||||
],
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
if (_userinfo != null && _subscription == null)
|
|
||||||
OutlinedButton(
|
|
||||||
style: const ButtonStyle(
|
|
||||||
visualDensity:
|
|
||||||
VisualDensity(horizontal: -4, vertical: -2),
|
|
||||||
),
|
),
|
||||||
onPressed: _isSubscribing
|
),
|
||||||
? null
|
if (_userinfo != null && _subscription == null)
|
||||||
: () async {
|
IconButton(
|
||||||
setState(() => _isSubscribing = true);
|
style: const ButtonStyle(
|
||||||
_subscription =
|
visualDensity:
|
||||||
await Get.find<SubscriptionProvider>()
|
VisualDensity(horizontal: -4, vertical: -2),
|
||||||
.subscribeToUser(_userinfo!.id);
|
),
|
||||||
setState(() => _isSubscribing = false);
|
onPressed: _isSubscribing ? null : _subscribeToUser,
|
||||||
},
|
icon: const Icon(Icons.add_circle_outline),
|
||||||
child: Text('subscribe'.tr),
|
tooltip: 'subscribe'.tr,
|
||||||
)
|
)
|
||||||
else if (_userinfo != null)
|
else if (_userinfo != null)
|
||||||
OutlinedButton(
|
IconButton(
|
||||||
style: const ButtonStyle(
|
style: const ButtonStyle(
|
||||||
visualDensity:
|
visualDensity:
|
||||||
VisualDensity(horizontal: -4, vertical: -2),
|
VisualDensity(horizontal: -4, vertical: -2),
|
||||||
|
),
|
||||||
|
onPressed:
|
||||||
|
_isSubscribing ? null : _unsubscribeFromUser,
|
||||||
|
icon: const Icon(Icons.remove_circle_outline),
|
||||||
|
tooltip: 'unsubscribe'.tr,
|
||||||
),
|
),
|
||||||
onPressed: _isSubscribing
|
if (_userinfo != null && _relationship == null)
|
||||||
? null
|
IconButton(
|
||||||
: () async {
|
icon: const Icon(Icons.person_add),
|
||||||
setState(() => _isSubscribing = true);
|
onPressed: _isMakingFriend ? null : _makeFriend,
|
||||||
await Get.find<SubscriptionProvider>()
|
tooltip: 'friendAdd'.tr,
|
||||||
.unsubscribeFromUser(_userinfo!.id);
|
)
|
||||||
_subscription = null;
|
else
|
||||||
setState(() => _isSubscribing = false);
|
const IconButton(
|
||||||
},
|
icon: Icon(Icons.handshake),
|
||||||
child: Text('unsubscribe'.tr),
|
onPressed: null,
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: AppTheme.isLargeScreen(context) ? 8 : 16,
|
||||||
),
|
),
|
||||||
if (_userinfo != null &&
|
],
|
||||||
!_relationshipProvider.hasFriend(_userinfo!))
|
),
|
||||||
IconButton(
|
).paddingOnly(top: MediaQuery.of(context).padding.top),
|
||||||
icon: const Icon(Icons.person_add),
|
|
||||||
onPressed: _isMakingFriend
|
|
||||||
? null
|
|
||||||
: () async {
|
|
||||||
setState(() => _isMakingFriend = true);
|
|
||||||
try {
|
|
||||||
await _relationshipProvider
|
|
||||||
.makeFriend(widget.name);
|
|
||||||
context.showSnackbar(
|
|
||||||
'accountFriendRequestSent'.tr,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
context.showErrorDialog(e);
|
|
||||||
} finally {
|
|
||||||
setState(() => _isMakingFriend = false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
else
|
|
||||||
const IconButton(
|
|
||||||
icon: Icon(Icons.handshake),
|
|
||||||
onPressed: null,
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
width: AppTheme.isLargeScreen(context) ? 8 : 16,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
bottom: TabBar(
|
bottom: TabBar(
|
||||||
tabs: [
|
tabs: [
|
||||||
Tab(text: 'profilePage'.tr),
|
Tab(text: 'profilePage'.tr),
|
||||||
@ -271,21 +324,205 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
|||||||
body: TabBarView(
|
body: TabBarView(
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
children: [
|
children: [
|
||||||
Column(
|
ListView(
|
||||||
|
padding: const EdgeInsets.only(top: 16, bottom: 16),
|
||||||
children: [
|
children: [
|
||||||
const Gap(16),
|
CenteredContainer(
|
||||||
AccountHeadingWidget(
|
child: AccountHeadingWidget(
|
||||||
name: _userinfo!.name,
|
name: _userinfo!.name,
|
||||||
nick: _userinfo!.nick,
|
nick: _userinfo!.nick,
|
||||||
desc: _userinfo!.description,
|
desc: _userinfo!.description,
|
||||||
badges: _userinfo!.badges,
|
badges: _userinfo!.badges,
|
||||||
banner: _userinfo!.banner,
|
banner: _userinfo!.banner,
|
||||||
avatar: _userinfo!.avatar,
|
avatar: _userinfo!.avatar,
|
||||||
status: Get.find<StatusProvider>()
|
status: Get.find<StatusProvider>()
|
||||||
.getSomeoneStatus(_userinfo!.name),
|
.getSomeoneStatus(_userinfo!.name),
|
||||||
detail: _userinfo,
|
detail: _userinfo,
|
||||||
profile: _userinfo!.profile,
|
profile: _userinfo!.profile,
|
||||||
extraWidgets: const [],
|
extraWidgets: [
|
||||||
|
if (_dailySignRecords.isNotEmpty)
|
||||||
|
Card(
|
||||||
|
child: SizedBox(
|
||||||
|
height: 180,
|
||||||
|
width:
|
||||||
|
max(640, MediaQuery.of(context).size.width),
|
||||||
|
child: LineChart(
|
||||||
|
LineChartData(
|
||||||
|
lineBarsData: [
|
||||||
|
LineChartBarData(
|
||||||
|
isCurved: true,
|
||||||
|
isStrokeCapRound: true,
|
||||||
|
isStrokeJoinRound: true,
|
||||||
|
color:
|
||||||
|
Theme.of(context).colorScheme.primary,
|
||||||
|
belowBarData: BarAreaData(
|
||||||
|
show: true,
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: List.filled(
|
||||||
|
_dailySignRecords.length,
|
||||||
|
Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.primary
|
||||||
|
.withOpacity(0.3),
|
||||||
|
).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
spots: _dailySignRecords
|
||||||
|
.map(
|
||||||
|
(x) => FlSpot(
|
||||||
|
x.createdAt
|
||||||
|
.copyWith(
|
||||||
|
hour: 0,
|
||||||
|
minute: 0,
|
||||||
|
second: 0,
|
||||||
|
millisecond: 0,
|
||||||
|
microsecond: 0,
|
||||||
|
)
|
||||||
|
.millisecondsSinceEpoch
|
||||||
|
.toDouble(),
|
||||||
|
x.resultTier.toDouble(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
lineTouchData: LineTouchData(
|
||||||
|
touchTooltipData: LineTouchTooltipData(
|
||||||
|
getTooltipItems: (spots) => spots
|
||||||
|
.map((spot) => LineTooltipItem(
|
||||||
|
'${DailySignHistoryChartDialog.signSymbols[spot.y.toInt()]}\n${DateFormat('MM/dd').format(DateTime.fromMillisecondsSinceEpoch(spot.x.toInt()))}',
|
||||||
|
TextStyle(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSurface,
|
||||||
|
),
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
getTooltipColor: (_) => Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.surfaceContainerHigh,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
titlesData: FlTitlesData(
|
||||||
|
topTitles: const AxisTitles(
|
||||||
|
sideTitles: SideTitles(showTitles: false),
|
||||||
|
),
|
||||||
|
rightTitles: const AxisTitles(
|
||||||
|
sideTitles: SideTitles(showTitles: false),
|
||||||
|
),
|
||||||
|
leftTitles: AxisTitles(
|
||||||
|
sideTitles: SideTitles(
|
||||||
|
showTitles: true,
|
||||||
|
reservedSize: 40,
|
||||||
|
interval: 1,
|
||||||
|
getTitlesWidget: (value, _) => Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: Text(
|
||||||
|
DailySignHistoryChartDialog
|
||||||
|
.signSymbols[value.toInt()],
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
).paddingOnly(right: 8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
bottomTitles: AxisTitles(
|
||||||
|
sideTitles: SideTitles(
|
||||||
|
showTitles: true,
|
||||||
|
reservedSize: 28,
|
||||||
|
interval: 86400000,
|
||||||
|
getTitlesWidget: (value, _) => Text(
|
||||||
|
DateFormat('dd').format(
|
||||||
|
DateTime.fromMillisecondsSinceEpoch(
|
||||||
|
value.toInt(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
).paddingOnly(top: 8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
gridData: const FlGridData(show: false),
|
||||||
|
borderData: FlBorderData(show: false),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).marginOnly(
|
||||||
|
right: 24,
|
||||||
|
left: 12,
|
||||||
|
bottom: 8,
|
||||||
|
top: 24,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
appendWidgets: [
|
||||||
|
Card(
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 4,
|
||||||
|
horizontal: 8,
|
||||||
|
),
|
||||||
|
width: double.maxFinite,
|
||||||
|
child: Wrap(
|
||||||
|
alignment: WrapAlignment.spaceAround,
|
||||||
|
children: [
|
||||||
|
TextButton.icon(
|
||||||
|
style: const ButtonStyle(
|
||||||
|
visualDensity: VisualDensity(
|
||||||
|
horizontal: -4,
|
||||||
|
vertical: -2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AbuseReportDialog(
|
||||||
|
resourceId: 'user:${_userinfo!.id}',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.flag,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
label: Text('reportAbuse'.tr),
|
||||||
|
),
|
||||||
|
if (_relationship?.status != 2)
|
||||||
|
TextButton.icon(
|
||||||
|
style: const ButtonStyle(
|
||||||
|
visualDensity: VisualDensity(
|
||||||
|
horizontal: -4,
|
||||||
|
vertical: -2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed:
|
||||||
|
_isMakingFriend ? null : _blockUser,
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.block,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
label: Text('blockUser'.tr),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
TextButton.icon(
|
||||||
|
style: const ButtonStyle(
|
||||||
|
visualDensity: VisualDensity(
|
||||||
|
horizontal: -4,
|
||||||
|
vertical: -2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed:
|
||||||
|
_isMakingFriend ? null : _unblockUser,
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.add_circle_outline,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
label: Text('unblockUser'.tr),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -301,7 +538,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
children: [
|
children: [
|
||||||
_buildStatisticsEntry(
|
_StatsWidget(
|
||||||
'totalSocialCreditPoints'.tr,
|
'totalSocialCreditPoints'.tr,
|
||||||
_userinfo != null
|
_userinfo != null
|
||||||
? _userSocialCreditPoints.toString()
|
? _userSocialCreditPoints.toString()
|
||||||
@ -314,16 +551,16 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
children: [
|
children: [
|
||||||
Obx(
|
Obx(
|
||||||
() => _buildStatisticsEntry(
|
() => _StatsWidget(
|
||||||
'totalPostCount'.tr,
|
'totalPostCount'.tr,
|
||||||
_postController.postTotal.value.toString(),
|
_postController.postTotal.value.toString(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_buildStatisticsEntry(
|
_StatsWidget(
|
||||||
'totalUpvote'.tr,
|
'totalUpvote'.tr,
|
||||||
_totalUpvote.toString(),
|
_totalUpvote.toString(),
|
||||||
),
|
),
|
||||||
_buildStatisticsEntry(
|
_StatsWidget(
|
||||||
'totalDownvote'.tr,
|
'totalDownvote'.tr,
|
||||||
_totalDownvote.toString(),
|
_totalDownvote.toString(),
|
||||||
),
|
),
|
||||||
@ -373,8 +610,9 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
|||||||
),
|
),
|
||||||
CenteredContainer(
|
CenteredContainer(
|
||||||
child: RefreshIndicator(
|
child: RefreshIndicator(
|
||||||
onRefresh: () =>
|
onRefresh: () => Future.sync(
|
||||||
Future.sync(() => _albumPagingController.refresh()),
|
() => _albumPagingController.refresh(),
|
||||||
|
),
|
||||||
child: PagedGridView<int, Attachment>(
|
child: PagedGridView<int, Attachment>(
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
pagingController: _albumPagingController,
|
pagingController: _albumPagingController,
|
||||||
@ -420,3 +658,28 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _StatsWidget extends StatelessWidget {
|
||||||
|
final String label;
|
||||||
|
final String content;
|
||||||
|
|
||||||
|
const _StatsWidget(this.label, this.content);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Expanded(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
content,
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -7,12 +7,12 @@ import 'package:solian/exceptions/request.dart';
|
|||||||
import 'package:solian/exts.dart';
|
import 'package:solian/exts.dart';
|
||||||
import 'package:solian/models/auth.dart';
|
import 'package:solian/models/auth.dart';
|
||||||
import 'package:solian/providers/auth.dart';
|
import 'package:solian/providers/auth.dart';
|
||||||
import 'package:solian/providers/content/channel.dart';
|
|
||||||
import 'package:solian/providers/content/realm.dart';
|
import 'package:solian/providers/content/realm.dart';
|
||||||
import 'package:solian/providers/relation.dart';
|
import 'package:solian/providers/relation.dart';
|
||||||
import 'package:solian/providers/websocket.dart';
|
import 'package:solian/providers/websocket.dart';
|
||||||
import 'package:solian/services.dart';
|
import 'package:solian/services.dart';
|
||||||
import 'package:solian/widgets/sized_container.dart';
|
import 'package:solian/widgets/sized_container.dart';
|
||||||
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
class SignInScreen extends StatefulWidget {
|
class SignInScreen extends StatefulWidget {
|
||||||
const SignInScreen({super.key});
|
const SignInScreen({super.key});
|
||||||
@ -167,7 +167,6 @@ class _SignInScreenState extends State<SignInScreen> {
|
|||||||
|
|
||||||
final result = AuthResult.fromJson(resp.body);
|
final result = AuthResult.fromJson(resp.body);
|
||||||
_currentTicket = result.ticket;
|
_currentTicket = result.ticket;
|
||||||
_passwordController.clear();
|
|
||||||
|
|
||||||
// Finish sign in if possible
|
// Finish sign in if possible
|
||||||
if (result.isFinished) {
|
if (result.isFinished) {
|
||||||
@ -177,7 +176,6 @@ class _SignInScreenState extends State<SignInScreen> {
|
|||||||
await auth.refreshAuthorizeStatus();
|
await auth.refreshAuthorizeStatus();
|
||||||
await auth.refreshUserProfile();
|
await auth.refreshUserProfile();
|
||||||
|
|
||||||
Get.find<ChannelProvider>().refreshAvailableChannel();
|
|
||||||
Get.find<RealmProvider>().refreshAvailableRealms();
|
Get.find<RealmProvider>().refreshAvailableRealms();
|
||||||
Get.find<RelationshipProvider>().refreshRelativeList();
|
Get.find<RelationshipProvider>().refreshRelativeList();
|
||||||
Get.find<WebSocketProvider>().registerPushNotifications();
|
Get.find<WebSocketProvider>().registerPushNotifications();
|
||||||
@ -185,11 +183,13 @@ class _SignInScreenState extends State<SignInScreen> {
|
|||||||
autoStartBackgroundNotificationService();
|
autoStartBackgroundNotificationService();
|
||||||
|
|
||||||
Navigator.pop(context, true);
|
Navigator.pop(context, true);
|
||||||
|
_passwordController.clear();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Skip the first step
|
// Skip the first step
|
||||||
_factorPicked = null;
|
_factorPicked = null;
|
||||||
_factorPickedType = null;
|
_factorPickedType = null;
|
||||||
|
_passwordController.clear();
|
||||||
setState(() => _period += 2);
|
setState(() => _period += 2);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -210,9 +210,8 @@ class _SignInScreenState extends State<SignInScreen> {
|
|||||||
case 2:
|
case 2:
|
||||||
_passwordController.clear();
|
_passwordController.clear();
|
||||||
_factorPickedType = null;
|
_factorPickedType = null;
|
||||||
default:
|
|
||||||
setState(() => _period--);
|
|
||||||
}
|
}
|
||||||
|
setState(() => _period--);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -235,16 +234,18 @@ class _SignInScreenState extends State<SignInScreen> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: switch (_period % 3) {
|
child: switch (_period % 3) {
|
||||||
1 => Column(
|
1 => ListView(
|
||||||
|
shrinkWrap: true,
|
||||||
key: const ValueKey<int>(1),
|
key: const ValueKey<int>(1),
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
children: [
|
||||||
ClipRRect(
|
Align(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
alignment: Alignment.centerLeft,
|
||||||
child:
|
child: ClipRRect(
|
||||||
Image.asset('assets/logo.png', width: 64, height: 64),
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
).paddingOnly(bottom: 8, left: 4),
|
child:
|
||||||
|
Image.asset('assets/logo.png', width: 64, height: 64),
|
||||||
|
).paddingOnly(bottom: 8, left: 4),
|
||||||
|
),
|
||||||
Text(
|
Text(
|
||||||
'signinPickFactor'.tr,
|
'signinPickFactor'.tr,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
@ -323,16 +324,18 @@ class _SignInScreenState extends State<SignInScreen> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
2 => Column(
|
2 => ListView(
|
||||||
key: const ValueKey<int>(2),
|
key: const ValueKey<int>(2),
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
shrinkWrap: true,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
children: [
|
||||||
ClipRRect(
|
Align(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
alignment: Alignment.centerLeft,
|
||||||
child:
|
child: ClipRRect(
|
||||||
Image.asset('assets/logo.png', width: 64, height: 64),
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
).paddingOnly(bottom: 8, left: 4),
|
child:
|
||||||
|
Image.asset('assets/logo.png', width: 64, height: 64),
|
||||||
|
).paddingOnly(bottom: 8, left: 4),
|
||||||
|
),
|
||||||
Text(
|
Text(
|
||||||
'signinEnterPassword'.tr,
|
'signinEnterPassword'.tr,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
@ -396,16 +399,18 @@ class _SignInScreenState extends State<SignInScreen> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
_ => Column(
|
_ => ListView(
|
||||||
key: const ValueKey<int>(0),
|
key: const ValueKey<int>(0),
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
shrinkWrap: true,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
children: [
|
||||||
ClipRRect(
|
Align(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
alignment: Alignment.centerLeft,
|
||||||
child:
|
child: ClipRRect(
|
||||||
Image.asset('assets/logo.png', width: 64, height: 64),
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
).paddingOnly(bottom: 8, left: 4),
|
child:
|
||||||
|
Image.asset('assets/logo.png', width: 64, height: 64),
|
||||||
|
).paddingOnly(bottom: 8, left: 4),
|
||||||
|
),
|
||||||
Text(
|
Text(
|
||||||
'signinGreeting'.tr,
|
'signinGreeting'.tr,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
@ -451,11 +456,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),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import 'package:get/get.dart';
|
|||||||
import 'package:solian/exts.dart';
|
import 'package:solian/exts.dart';
|
||||||
import 'package:solian/services.dart';
|
import 'package:solian/services.dart';
|
||||||
import 'package:solian/widgets/sized_container.dart';
|
import 'package:solian/widgets/sized_container.dart';
|
||||||
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
class SignUpScreen extends StatefulWidget {
|
class SignUpScreen extends StatefulWidget {
|
||||||
const SignUpScreen({super.key});
|
const SignUpScreen({super.key});
|
||||||
@ -18,7 +19,7 @@ class _SignUpScreenState extends State<SignUpScreen> {
|
|||||||
final _nicknameController = TextEditingController();
|
final _nicknameController = TextEditingController();
|
||||||
final _passwordController = TextEditingController();
|
final _passwordController = TextEditingController();
|
||||||
|
|
||||||
void performAction(BuildContext context) async {
|
void _performAction(BuildContext context) async {
|
||||||
final email = _emailController.value.text;
|
final email = _emailController.value.text;
|
||||||
final username = _usernameController.value.text;
|
final username = _usernameController.value.text;
|
||||||
final nickname = _nicknameController.value.text;
|
final nickname = _nicknameController.value.text;
|
||||||
@ -60,20 +61,24 @@ class _SignUpScreenState extends State<SignUpScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _isTermAccepted = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Material(
|
return Material(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
color: Theme.of(context).colorScheme.surface,
|
||||||
child: CenteredContainer(
|
child: CenteredContainer(
|
||||||
maxWidth: 360,
|
maxWidth: 360,
|
||||||
child: Column(
|
child: ListView(
|
||||||
mainAxisSize: MainAxisSize.min,
|
shrinkWrap: true,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
children: [
|
||||||
ClipRRect(
|
Align(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
alignment: Alignment.centerLeft,
|
||||||
child: Image.asset('assets/logo.png', width: 64, height: 64),
|
child: ClipRRect(
|
||||||
).paddingOnly(bottom: 8, left: 4),
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
child: Image.asset('assets/logo.png', width: 64, height: 64),
|
||||||
|
).paddingOnly(bottom: 8, left: 4),
|
||||||
|
),
|
||||||
Text(
|
Text(
|
||||||
'signupGreeting'.tr,
|
'signupGreeting'.tr,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
@ -136,12 +141,61 @@ class _SignUpScreenState extends State<SignUpScreen> {
|
|||||||
),
|
),
|
||||||
onTapOutside: (_) =>
|
onTapOutside: (_) =>
|
||||||
FocusManager.instance.primaryFocus?.unfocus(),
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
onSubmitted: (_) => performAction(context),
|
onSubmitted: (_) => _performAction(context),
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
CheckboxListTile(
|
||||||
|
value: _isTermAccepted,
|
||||||
|
title: Text(
|
||||||
|
'termAccept'.tr,
|
||||||
|
style: const TextStyle(height: 1.2),
|
||||||
|
).paddingOnly(bottom: 4),
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: RichText(
|
||||||
|
text: TextSpan(
|
||||||
|
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSurface
|
||||||
|
.withOpacity(0.75),
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
TextSpan(text: 'termAcceptDesc'.tr),
|
||||||
|
WidgetSpan(
|
||||||
|
child: Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text('termAcceptLink'.tr),
|
||||||
|
const Gap(4),
|
||||||
|
const Icon(Icons.launch, size: 14),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
launchUrlString('https://solsynth.dev/terms');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() => _isTermAccepted = value ?? false);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
const Gap(16),
|
const Gap(16),
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
child: TextButton(
|
child: TextButton(
|
||||||
|
onPressed:
|
||||||
|
!_isTermAccepted ? null : () => _performAction(context),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
@ -149,12 +203,11 @@ class _SignUpScreenState extends State<SignUpScreen> {
|
|||||||
const Icon(Icons.chevron_right),
|
const Icon(Icons.chevron_right),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
onPressed: () => performAction(context),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
).paddingAll(24),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -201,9 +201,10 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
|
|||||||
String title = _channel?.name ?? 'loading'.tr;
|
String title = _channel?.name ?? 'loading'.tr;
|
||||||
String? placeholder;
|
String? placeholder;
|
||||||
|
|
||||||
if (_channel?.type == 1) {
|
final otherside =
|
||||||
final otherside =
|
_channel?.members!.where((e) => e.account.id != _accountId).firstOrNull;
|
||||||
_channel!.members!.where((e) => e.account.id != _accountId).first;
|
|
||||||
|
if (_channel?.type == 1 && otherside != null) {
|
||||||
title = otherside.account.nick;
|
title = otherside.account.nick;
|
||||||
placeholder = 'messageInputPlaceholder'.trParams(
|
placeholder = 'messageInputPlaceholder'.trParams(
|
||||||
{'channel': '@${otherside.account.name}'},
|
{'channel': '@${otherside.account.name}'},
|
||||||
@ -274,7 +275,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
|
|||||||
channel: _channel!,
|
channel: _channel!,
|
||||||
ongoingCall: _ongoingCall!,
|
ongoingCall: _ongoingCall!,
|
||||||
onJoin: () {
|
onJoin: () {
|
||||||
if (!AppTheme.isLargeScreen(context)) {
|
if (!AppTheme.isUltraLargeScreen(context)) {
|
||||||
final ChatCallProvider call = Get.find();
|
final ChatCallProvider call = Get.find();
|
||||||
call.gotoScreen(context);
|
call.gotoScreen(context);
|
||||||
}
|
}
|
||||||
@ -328,7 +329,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
|
|||||||
),
|
),
|
||||||
Obx(() {
|
Obx(() {
|
||||||
final ChatCallProvider call = Get.find();
|
final ChatCallProvider call = Get.find();
|
||||||
if (call.isMounted.value && AppTheme.isLargeScreen(context)) {
|
if (call.isMounted.value &&
|
||||||
|
AppTheme.isUltraLargeScreen(context)) {
|
||||||
return const Expanded(
|
return const Expanded(
|
||||||
child: Row(children: [
|
child: Row(children: [
|
||||||
VerticalDivider(width: 0.3, thickness: 0.3),
|
VerticalDivider(width: 0.3, thickness: 0.3),
|
||||||
|
@ -1,145 +1,322 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:solian/controllers/chat_events_controller.dart';
|
||||||
import 'package:solian/exts.dart';
|
import 'package:solian/exts.dart';
|
||||||
|
import 'package:solian/models/channel.dart';
|
||||||
import 'package:solian/providers/auth.dart';
|
import 'package:solian/providers/auth.dart';
|
||||||
import 'package:solian/providers/content/channel.dart';
|
import 'package:solian/providers/content/channel.dart';
|
||||||
|
import 'package:solian/providers/content/realm.dart';
|
||||||
|
import 'package:solian/providers/database/database.dart';
|
||||||
import 'package:solian/router.dart';
|
import 'package:solian/router.dart';
|
||||||
import 'package:solian/screens/account/notification.dart';
|
import 'package:solian/screens/account/notification.dart';
|
||||||
import 'package:solian/theme.dart';
|
import 'package:solian/theme.dart';
|
||||||
|
import 'package:solian/widgets/account/account_avatar.dart';
|
||||||
import 'package:solian/widgets/account/signin_required_overlay.dart';
|
import 'package:solian/widgets/account/signin_required_overlay.dart';
|
||||||
import 'package:solian/widgets/app_bar_leading.dart';
|
import 'package:solian/widgets/app_bar_leading.dart';
|
||||||
import 'package:solian/widgets/app_bar_title.dart';
|
import 'package:solian/widgets/app_bar_title.dart';
|
||||||
import 'package:solian/widgets/channel/channel_list.dart';
|
import 'package:solian/widgets/channel/channel_list.dart';
|
||||||
import 'package:solian/widgets/chat/call/chat_call_indicator.dart';
|
import 'package:solian/widgets/chat/call/chat_call_indicator.dart';
|
||||||
import 'package:solian/widgets/current_state_action.dart';
|
import 'package:solian/widgets/current_state_action.dart';
|
||||||
import 'package:solian/widgets/sized_container.dart';
|
import 'package:solian/widgets/sidebar/empty_placeholder.dart';
|
||||||
|
|
||||||
class ChatScreen extends StatefulWidget {
|
class ChatScreen extends StatelessWidget {
|
||||||
const ChatScreen({super.key});
|
const ChatScreen({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ChatScreen> createState() => _ChatScreenState();
|
Widget build(BuildContext context) {
|
||||||
|
return Material(
|
||||||
|
color: Theme.of(context).colorScheme.surface,
|
||||||
|
child: const ChatList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ChatScreenState extends State<ChatScreen> {
|
class ChatListShell extends StatelessWidget {
|
||||||
late final ChannelProvider _channels;
|
final Widget? child;
|
||||||
|
|
||||||
|
const ChatListShell({super.key, required this.child});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Material(
|
||||||
|
color: Theme.of(context).colorScheme.surface,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const SizedBox(
|
||||||
|
width: 360,
|
||||||
|
child: ChatList(),
|
||||||
|
),
|
||||||
|
const VerticalDivider(thickness: 0.3, width: 0.3),
|
||||||
|
Expanded(child: child ?? const EmptyPagePlaceholder()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChatList extends StatefulWidget {
|
||||||
|
const ChatList({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ChatList> createState() => _ChatListState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChatListState extends State<ChatList> {
|
||||||
|
List<Channel> _normalChannels = List.empty();
|
||||||
|
List<Channel> _directChannels = List.empty();
|
||||||
|
final Map<String, List<Channel>> _realmChannels = {};
|
||||||
|
|
||||||
|
late final ChannelProvider _channels = Get.find();
|
||||||
|
|
||||||
|
List<Channel> _sortChannels(List<Channel> channels) {
|
||||||
|
channels.sort(
|
||||||
|
(a, b) =>
|
||||||
|
_lastMessages?[b.id]?.createdAt.compareTo(
|
||||||
|
_lastMessages?[a.id]?.createdAt ??
|
||||||
|
DateTime.fromMillisecondsSinceEpoch(0),
|
||||||
|
) ??
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
return channels;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadNormalChannels() async {
|
||||||
|
final resp = await _channels.listAvailableChannel(isDirect: false);
|
||||||
|
setState(() {
|
||||||
|
_normalChannels = _sortChannels(resp);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadDirectChannels() async {
|
||||||
|
final resp = await _channels.listAvailableChannel(isDirect: true);
|
||||||
|
setState(() {
|
||||||
|
_directChannels = _sortChannels(resp);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadRealmChannels(String realm) async {
|
||||||
|
final resp = await _channels.listAvailableChannel(scope: realm);
|
||||||
|
setState(() {
|
||||||
|
_realmChannels[realm] = _sortChannels(List.from(resp));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadAllChannels() async {
|
||||||
|
final RealmProvider realms = Get.find();
|
||||||
|
Future.wait([
|
||||||
|
_loadNormalChannels(),
|
||||||
|
_loadDirectChannels(),
|
||||||
|
...realms.availableRealms.map((x) => _loadRealmChannels(x.alias)),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<int, LocalMessageEventTableData>? _lastMessages;
|
||||||
|
|
||||||
|
Future<void> _loadLastMessages() async {
|
||||||
|
final ctrl = ChatEventController();
|
||||||
|
await ctrl.initialize();
|
||||||
|
final messages = await ctrl.src.getLastInAllChannels();
|
||||||
|
setState(() {
|
||||||
|
_lastMessages = messages
|
||||||
|
.map((k, v) => MapEntry(k, v.firstOrNull))
|
||||||
|
.cast<int, LocalMessageEventTableData>();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
try {
|
_loadLastMessages().then((_) {
|
||||||
_channels = Get.find();
|
_loadAllChannels();
|
||||||
_channels.refreshAvailableChannel();
|
});
|
||||||
} catch (e) {
|
|
||||||
context.showErrorDialog(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
|
final RealmProvider realms = Get.find();
|
||||||
|
|
||||||
return Material(
|
return Obx(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
() => DefaultTabController(
|
||||||
child: Scaffold(
|
length: 2 + realms.availableRealms.length,
|
||||||
appBar: AppBar(
|
child: Scaffold(
|
||||||
leading: AppBarLeadingButton.adaptive(context),
|
appBar: AppBar(
|
||||||
title: AppBarTitle('chat'.tr),
|
leading: Obx(() {
|
||||||
centerTitle: true,
|
final adaptive = AppBarLeadingButton.adaptive(context);
|
||||||
toolbarHeight: AppTheme.toolbarHeight(context),
|
if (adaptive != null) return adaptive;
|
||||||
actions: [
|
if (_channels.isLoading.value) {
|
||||||
const BackgroundStateWidget(),
|
return const CircularProgressIndicator(
|
||||||
const NotificationButton(),
|
strokeWidth: 3,
|
||||||
PopupMenuButton(
|
).paddingAll(18);
|
||||||
icon: const Icon(Icons.add_circle),
|
}
|
||||||
itemBuilder: (BuildContext context) => [
|
return const SizedBox.shrink();
|
||||||
PopupMenuItem(
|
}),
|
||||||
child: ListTile(
|
title: AppBarTitle('chat'.tr),
|
||||||
title: Text('channelOrganizeCommon'.tr),
|
centerTitle: true,
|
||||||
leading: const Icon(Icons.tag),
|
toolbarHeight: AppTheme.toolbarHeight(context),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
|
actions: [
|
||||||
),
|
const BackgroundStateWidget(),
|
||||||
onTap: () {
|
const NotificationButton(),
|
||||||
AppRouter.instance.pushNamed('channelOrganizing').then(
|
PopupMenuButton(
|
||||||
(value) {
|
icon: const Icon(Icons.add_circle),
|
||||||
if (value != null) {
|
itemBuilder: (BuildContext context) => [
|
||||||
_channels.refreshAvailableChannel();
|
PopupMenuItem(
|
||||||
}
|
child: ListTile(
|
||||||
},
|
title: Text('channelOrganizeCommon'.tr),
|
||||||
);
|
leading: const Icon(Icons.tag),
|
||||||
},
|
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
),
|
|
||||||
PopupMenuItem(
|
|
||||||
child: ListTile(
|
|
||||||
title: Text('channelOrganizeDirect'.tr),
|
|
||||||
leading: const FaIcon(
|
|
||||||
FontAwesomeIcons.userGroup,
|
|
||||||
size: 16,
|
|
||||||
),
|
),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
|
onTap: () {
|
||||||
|
AppRouter.instance.pushNamed('channelOrganizing').then(
|
||||||
|
(value) {
|
||||||
|
if (value != null) {
|
||||||
|
_loadAllChannels();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
onTap: () {
|
PopupMenuItem(
|
||||||
final ChannelProvider channels = Get.find();
|
child: ListTile(
|
||||||
channels
|
title: Text('channelOrganizeDirect'.tr),
|
||||||
.createDirectChannel(context, 'global')
|
leading: const FaIcon(
|
||||||
.then((resp) {
|
FontAwesomeIcons.userGroup,
|
||||||
if (resp != null) {
|
size: 16,
|
||||||
_channels.refreshAvailableChannel();
|
|
||||||
}
|
|
||||||
}).catchError((e) {
|
|
||||||
context.showErrorDialog(e);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
width: AppTheme.isLargeScreen(context) ? 8 : 16,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
body: Obx(() {
|
|
||||||
if (auth.isAuthorized.isFalse) {
|
|
||||||
return SigninRequiredOverlay(
|
|
||||||
onDone: () => _channels.refreshAvailableChannel(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final selfId = auth.userProfile.value!['id'];
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
Obx(() {
|
|
||||||
if (_channels.isLoading.isFalse) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
} else {
|
|
||||||
return const LinearProgressIndicator();
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
const ChatCallCurrentIndicator(),
|
|
||||||
Expanded(
|
|
||||||
child: CenteredContainer(
|
|
||||||
child: RefreshIndicator(
|
|
||||||
onRefresh: _channels.refreshAvailableChannel,
|
|
||||||
child: Obx(
|
|
||||||
() => ChannelListWidget(
|
|
||||||
noCategory: true,
|
|
||||||
channels: List.from([
|
|
||||||
..._channels.groupChannels
|
|
||||||
.where((x) => x.realmId == null),
|
|
||||||
..._channels.directChannels
|
|
||||||
]),
|
|
||||||
selfId: selfId,
|
|
||||||
useReplace: true,
|
|
||||||
),
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
),
|
),
|
||||||
|
onTap: () {
|
||||||
|
final ChannelProvider channels = Get.find();
|
||||||
|
channels
|
||||||
|
.createDirectChannel(context, 'global')
|
||||||
|
.then((resp) {
|
||||||
|
if (resp != null) {
|
||||||
|
_loadAllChannels();
|
||||||
|
}
|
||||||
|
}).catchError((e) {
|
||||||
|
context.showErrorDialog(e);
|
||||||
|
});
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: AppTheme.isLargeScreen(context) ? 8 : 16,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
bottom: TabBar(
|
||||||
}),
|
isScrollable: true,
|
||||||
|
dividerHeight: 0.3,
|
||||||
|
tabAlignment: TabAlignment.startOffset,
|
||||||
|
tabs: [
|
||||||
|
Tab(
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
CircleAvatar(
|
||||||
|
radius: 14,
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||||
|
child: const Icon(
|
||||||
|
Icons.forum,
|
||||||
|
size: 16,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
Text('all'.tr),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Tab(
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const CircleAvatar(
|
||||||
|
radius: 14,
|
||||||
|
child: Icon(
|
||||||
|
Icons.chat_bubble,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
Text('channelTypeDirect'.tr),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
...realms.availableRealms.map((x) => Tab(
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
AccountAvatar(
|
||||||
|
content: x.avatar,
|
||||||
|
radius: 14,
|
||||||
|
fallbackWidget: const Icon(
|
||||||
|
Icons.workspaces,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
Text(x.name),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: Obx(() {
|
||||||
|
if (auth.isAuthorized.isFalse) {
|
||||||
|
return SigninRequiredOverlay(
|
||||||
|
onDone: () => _loadAllChannels(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final selfId = auth.userProfile.value!['id'];
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
const ChatCallCurrentIndicator(),
|
||||||
|
Expanded(
|
||||||
|
child: TabBarView(
|
||||||
|
children: [
|
||||||
|
RefreshIndicator(
|
||||||
|
onRefresh: _loadNormalChannels,
|
||||||
|
child: ChannelListWidget(
|
||||||
|
channels: _sortChannels([
|
||||||
|
..._normalChannels,
|
||||||
|
..._directChannels,
|
||||||
|
..._realmChannels.values.expand((x) => x),
|
||||||
|
]),
|
||||||
|
selfId: selfId,
|
||||||
|
useReplace: AppTheme.isLargeScreen(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
RefreshIndicator(
|
||||||
|
onRefresh: _loadDirectChannels,
|
||||||
|
child: ChannelListWidget(
|
||||||
|
channels: _directChannels,
|
||||||
|
selfId: selfId,
|
||||||
|
useReplace: AppTheme.isLargeScreen(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
...realms.availableRealms.map(
|
||||||
|
(x) => RefreshIndicator(
|
||||||
|
onRefresh: () => _loadRealmChannels(x.alias),
|
||||||
|
child: ChannelListWidget(
|
||||||
|
channels: _realmChannels[x.alias] ?? [],
|
||||||
|
selfId: selfId,
|
||||||
|
useReplace: AppTheme.isLargeScreen(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -88,7 +88,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
|||||||
Future<void> _pullDaily() async {
|
Future<void> _pullDaily() async {
|
||||||
try {
|
try {
|
||||||
_signRecord = await _dailySign.getToday();
|
_signRecord = await _dailySign.getToday();
|
||||||
_dailySign.listLastRecord(30).then((value) {
|
_dailySign.listLastRecord(14).then((value) {
|
||||||
setState(() => _signRecordHistory = value);
|
setState(() => _signRecordHistory = value);
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -103,7 +103,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
_signRecord = await _dailySign.signToday();
|
_signRecord = await _dailySign.signToday();
|
||||||
_dailySign.listLastRecord(30).then((value) {
|
_dailySign.listLastRecord(14).then((value) {
|
||||||
setState(() => _signRecordHistory = value);
|
setState(() => _signRecordHistory = value);
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -10,20 +10,20 @@ import 'package:solian/router.dart';
|
|||||||
import 'package:solian/screens/account/notification.dart';
|
import 'package:solian/screens/account/notification.dart';
|
||||||
import 'package:solian/theme.dart';
|
import 'package:solian/theme.dart';
|
||||||
import 'package:solian/widgets/account/signin_required_overlay.dart';
|
import 'package:solian/widgets/account/signin_required_overlay.dart';
|
||||||
import 'package:solian/widgets/app_bar_title.dart';
|
|
||||||
import 'package:solian/widgets/current_state_action.dart';
|
import 'package:solian/widgets/current_state_action.dart';
|
||||||
import 'package:solian/widgets/app_bar_leading.dart';
|
import 'package:solian/widgets/app_bar_leading.dart';
|
||||||
|
import 'package:solian/widgets/navigation/realm_switcher.dart';
|
||||||
import 'package:solian/widgets/posts/post_shuffle_swiper.dart';
|
import 'package:solian/widgets/posts/post_shuffle_swiper.dart';
|
||||||
import 'package:solian/widgets/posts/post_warped_list.dart';
|
import 'package:solian/widgets/posts/post_warped_list.dart';
|
||||||
|
|
||||||
class FeedScreen extends StatefulWidget {
|
class ExploreScreen extends StatefulWidget {
|
||||||
const FeedScreen({super.key});
|
const ExploreScreen({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<FeedScreen> createState() => _FeedScreenState();
|
State<ExploreScreen> createState() => _ExploreScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FeedScreenState extends State<FeedScreen>
|
class _ExploreScreenState extends State<ExploreScreen>
|
||||||
with SingleTickerProviderStateMixin {
|
with SingleTickerProviderStateMixin {
|
||||||
late final PostListController _postController;
|
late final PostListController _postController;
|
||||||
late final TabController _tabController;
|
late final TabController _tabController;
|
||||||
@ -55,7 +55,6 @@ class _FeedScreenState extends State<FeedScreen>
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
final NavigationStateProvider navState = Get.find();
|
|
||||||
|
|
||||||
return Material(
|
return Material(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
color: Theme.of(context).colorScheme.surface,
|
||||||
@ -82,8 +81,14 @@ class _FeedScreenState extends State<FeedScreen>
|
|||||||
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
||||||
return [
|
return [
|
||||||
SliverAppBar(
|
SliverAppBar(
|
||||||
title: AppBarTitle('feed'.tr),
|
flexibleSpace: SizedBox(
|
||||||
centerTitle: false,
|
height: 48,
|
||||||
|
child: const Row(
|
||||||
|
children: [
|
||||||
|
RealmSwitcher(),
|
||||||
|
],
|
||||||
|
).paddingSymmetric(horizontal: 8),
|
||||||
|
).paddingOnly(top: MediaQuery.of(context).padding.top),
|
||||||
floating: true,
|
floating: true,
|
||||||
toolbarHeight: AppTheme.toolbarHeight(context),
|
toolbarHeight: AppTheme.toolbarHeight(context),
|
||||||
leading: AppBarLeadingButton.adaptive(context),
|
leading: AppBarLeadingButton.adaptive(context),
|
||||||
@ -96,10 +101,39 @@ class _FeedScreenState extends State<FeedScreen>
|
|||||||
],
|
],
|
||||||
bottom: TabBar(
|
bottom: TabBar(
|
||||||
controller: _tabController,
|
controller: _tabController,
|
||||||
|
dividerHeight: 0.3,
|
||||||
|
tabAlignment: TabAlignment.fill,
|
||||||
tabs: [
|
tabs: [
|
||||||
Tab(text: 'postListNews'.tr),
|
Tab(
|
||||||
Tab(text: 'postListFriends'.tr),
|
child: Row(
|
||||||
Tab(text: 'postListShuffle'.tr),
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.feed, size: 20),
|
||||||
|
const Gap(8),
|
||||||
|
Text('postListNews'.tr),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Tab(
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.people, size: 20),
|
||||||
|
const Gap(8),
|
||||||
|
Text('postListFriends'.tr),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Tab(
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.shuffle_on_outlined, size: 20),
|
||||||
|
const Gap(8),
|
||||||
|
Text('postListShuffle'.tr),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -114,16 +148,6 @@ class _FeedScreenState extends State<FeedScreen>
|
|||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
if (navState.focusedRealm.value != null)
|
|
||||||
MaterialBanner(
|
|
||||||
leading: const Icon(Icons.layers),
|
|
||||||
content: Text(
|
|
||||||
'postBrowsingIn'.trParams({
|
|
||||||
'region': '#${navState.focusedRealm.value!.alias}',
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
actions: const [SizedBox.shrink()],
|
|
||||||
),
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TabBarView(
|
child: TabBarView(
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
@ -63,13 +63,13 @@ class _FeedSearchScreenState extends State<FeedSearchScreen> {
|
|||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.label),
|
leading: const Icon(Icons.label),
|
||||||
tileColor: Theme.of(context).colorScheme.surfaceContainer,
|
tileColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
title: Text('feedSearchWithTag'.trParams({'key': widget.tag!})),
|
title: Text('postSearchWithTag'.trParams({'key': widget.tag!})),
|
||||||
),
|
),
|
||||||
if (widget.category != null)
|
if (widget.category != null)
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.category),
|
leading: const Icon(Icons.category),
|
||||||
tileColor: Theme.of(context).colorScheme.surfaceContainer,
|
tileColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
title: Text('feedSearchWithCategory'
|
title: Text('postSearchWithCategory'
|
||||||
.trParams({'key': widget.category!})),
|
.trParams({'key': widget.category!})),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
|
@ -376,6 +376,7 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: MarkdownTextContent(
|
child: MarkdownTextContent(
|
||||||
|
isAutoWarp: _editorController.mode.value == 0,
|
||||||
content: _editorController.contentController.text,
|
content: _editorController.contentController.text,
|
||||||
parentId: 'post-editor-preview',
|
parentId: 'post-editor-preview',
|
||||||
).paddingOnly(top: 12, right: 16),
|
).paddingOnly(top: 12, right: 16),
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_animate/flutter_animate.dart';
|
import 'package:flutter_animate/flutter_animate.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@ -8,6 +6,7 @@ import 'package:image_picker/image_picker.dart';
|
|||||||
import 'package:solian/exts.dart';
|
import 'package:solian/exts.dart';
|
||||||
import 'package:solian/models/attachment.dart';
|
import 'package:solian/models/attachment.dart';
|
||||||
import 'package:solian/models/realm.dart';
|
import 'package:solian/models/realm.dart';
|
||||||
|
import 'package:solian/platform.dart';
|
||||||
import 'package:solian/providers/auth.dart';
|
import 'package:solian/providers/auth.dart';
|
||||||
import 'package:solian/providers/content/attachment.dart';
|
import 'package:solian/providers/content/attachment.dart';
|
||||||
import 'package:solian/router.dart';
|
import 'package:solian/router.dart';
|
||||||
@ -84,36 +83,42 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
|
|||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
if (auth.isAuthorized.isFalse) return;
|
if (auth.isAuthorized.isFalse) return;
|
||||||
|
|
||||||
|
XFile file;
|
||||||
|
|
||||||
final image = await _imagePicker.pickImage(source: ImageSource.gallery);
|
final image = await _imagePicker.pickImage(source: ImageSource.gallery);
|
||||||
if (image == null) return;
|
if (image == null) return;
|
||||||
|
|
||||||
CroppedFile? croppedFile = await ImageCropper().cropImage(
|
if (PlatformInfo.canCropImage) {
|
||||||
sourcePath: image.path,
|
CroppedFile? croppedFile = await ImageCropper().cropImage(
|
||||||
uiSettings: [
|
sourcePath: image.path,
|
||||||
AndroidUiSettings(
|
uiSettings: [
|
||||||
toolbarTitle: 'cropImage'.tr,
|
AndroidUiSettings(
|
||||||
toolbarColor: Theme.of(context).colorScheme.primary,
|
toolbarTitle: 'cropImage'.tr,
|
||||||
toolbarWidgetColor: Theme.of(context).colorScheme.onPrimary,
|
toolbarColor: Theme.of(context).colorScheme.primary,
|
||||||
aspectRatioPresets: [
|
toolbarWidgetColor: Theme.of(context).colorScheme.onPrimary,
|
||||||
if (position == 'avatar') CropAspectRatioPreset.square,
|
aspectRatioPresets: [
|
||||||
if (position == 'banner') _BannerCropAspectRatioPreset(),
|
if (position == 'avatar') CropAspectRatioPreset.square,
|
||||||
],
|
if (position == 'banner') _BannerCropAspectRatioPreset(),
|
||||||
),
|
],
|
||||||
IOSUiSettings(
|
),
|
||||||
title: 'cropImage'.tr,
|
IOSUiSettings(
|
||||||
aspectRatioPresets: [
|
title: 'cropImage'.tr,
|
||||||
if (position == 'avatar') CropAspectRatioPreset.square,
|
aspectRatioPresets: [
|
||||||
if (position == 'banner') _BannerCropAspectRatioPreset(),
|
if (position == 'avatar') CropAspectRatioPreset.square,
|
||||||
],
|
if (position == 'banner') _BannerCropAspectRatioPreset(),
|
||||||
),
|
],
|
||||||
WebUiSettings(
|
),
|
||||||
context: context,
|
WebUiSettings(
|
||||||
),
|
context: context,
|
||||||
],
|
),
|
||||||
);
|
],
|
||||||
|
);
|
||||||
|
|
||||||
if (croppedFile == null) return;
|
if (croppedFile == null) return;
|
||||||
final file = File(croppedFile.path);
|
file = XFile(croppedFile.path);
|
||||||
|
} else {
|
||||||
|
file = XFile(image.path);
|
||||||
|
}
|
||||||
|
|
||||||
setState(() => _isBusy = true);
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
|
@ -68,12 +68,7 @@ class _RealmViewScreenState extends State<RealmViewScreen> {
|
|||||||
_channels.addAll(
|
_channels.addAll(
|
||||||
resp.body.map((e) => Channel.fromJson(e)).toList().cast<Channel>(),
|
resp.body.map((e) => Channel.fromJson(e)).toList().cast<Channel>(),
|
||||||
);
|
);
|
||||||
_channels.addAll(
|
_channels.addAll(availableResp);
|
||||||
availableResp.body
|
|
||||||
.map((e) => Channel.fromJson(e))
|
|
||||||
.toList()
|
|
||||||
.cast<Channel>(),
|
|
||||||
);
|
|
||||||
_channels.retainWhere((x) => channelIdx.add(x.id));
|
_channels.retainWhere((x) => channelIdx.add(x.id));
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -260,7 +255,6 @@ class RealmChannelListWidget extends StatelessWidget {
|
|||||||
child: ChannelListWidget(
|
child: ChannelListWidget(
|
||||||
channels: channels,
|
channels: channels,
|
||||||
selfId: auth.userProfile.value!['id'],
|
selfId: auth.userProfile.value!['id'],
|
||||||
noCategory: true,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
@ -1,13 +1,17 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:in_app_review/in_app_review.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:solian/exceptions/request.dart';
|
||||||
import 'package:solian/exts.dart';
|
import 'package:solian/exts.dart';
|
||||||
import 'package:solian/platform.dart';
|
import 'package:solian/platform.dart';
|
||||||
|
import 'package:solian/providers/auth.dart';
|
||||||
import 'package:solian/providers/database/database.dart';
|
import 'package:solian/providers/database/database.dart';
|
||||||
import 'package:solian/providers/theme_switcher.dart';
|
import 'package:solian/providers/theme_switcher.dart';
|
||||||
import 'package:solian/router.dart';
|
import 'package:solian/router.dart';
|
||||||
import 'package:solian/theme.dart';
|
import 'package:solian/theme.dart';
|
||||||
|
import 'package:solian/widgets/reports/abuse_report.dart';
|
||||||
|
|
||||||
class SettingScreen extends StatefulWidget {
|
class SettingScreen extends StatefulWidget {
|
||||||
const SettingScreen({super.key});
|
const SettingScreen({super.key});
|
||||||
@ -129,6 +133,54 @@ class _SettingScreenState extends State<SettingScreen> {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
Obx(() {
|
||||||
|
final AuthProvider auth = Get.find<AuthProvider>();
|
||||||
|
if (!auth.isAuthorized.value) return const SizedBox.shrink();
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
_buildCaptionHeader('account'.tr),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.flag),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
|
||||||
|
title: Text('reportAbuse'.tr),
|
||||||
|
subtitle: Text('reportAbuseDesc'.tr),
|
||||||
|
onTap: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => const AbuseReportDialog(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.person_remove),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
|
||||||
|
title: Text('accountDeletion'.tr),
|
||||||
|
subtitle: Text('accountDeletionDesc'.tr),
|
||||||
|
onTap: () {
|
||||||
|
context
|
||||||
|
.showSlideToConfirmDialog(
|
||||||
|
'accountDeletionConfirm'.tr,
|
||||||
|
'accountDeletionConfirmDesc'.trParams({
|
||||||
|
'account': '@${auth.userProfile.value!['name']}',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.then((value) async {
|
||||||
|
if (value != true) return;
|
||||||
|
final client = await auth.configureClient('id');
|
||||||
|
final resp = await client.post('/users/me/deletion', {});
|
||||||
|
if (resp.statusCode != 200) {
|
||||||
|
context.showErrorDialog(RequestException(resp));
|
||||||
|
} else {
|
||||||
|
context.showSnackbar('accountDeletionRequested'.tr);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
_buildCaptionHeader('more'.tr),
|
_buildCaptionHeader('more'.tr),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.delete_sweep),
|
leading: const Icon(Icons.delete_sweep),
|
||||||
@ -154,6 +206,21 @@ class _SettingScreenState extends State<SettingScreen> {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
if (PlatformInfo.canRateTheApp)
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.star),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
|
||||||
|
title: Text('rateTheApp'.tr),
|
||||||
|
subtitle: Text('rateTheAppDesc'.tr),
|
||||||
|
onTap: () {
|
||||||
|
final inAppReview = InAppReview.instance;
|
||||||
|
|
||||||
|
inAppReview.openStoreListing(
|
||||||
|
appStoreId: '6499032345',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.info_outline),
|
leading: const Icon(Icons.info_outline),
|
||||||
trailing: const Icon(Icons.chevron_right),
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
@ -2,7 +2,9 @@ import 'package:firebase_analytics/firebase_analytics.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:solian/theme.dart';
|
import 'package:solian/theme.dart';
|
||||||
import 'package:solian/widgets/navigation/app_navigation_drawer.dart';
|
import 'package:solian/widgets/navigation/app_navigation.dart';
|
||||||
|
import 'package:solian/widgets/navigation/app_navigation_bottom.dart';
|
||||||
|
import 'package:solian/widgets/navigation/app_navigation_rail.dart';
|
||||||
|
|
||||||
final GlobalKey<ScaffoldState> rootScaffoldKey = GlobalKey<ScaffoldState>();
|
final GlobalKey<ScaffoldState> rootScaffoldKey = GlobalKey<ScaffoldState>();
|
||||||
|
|
||||||
@ -39,17 +41,28 @@ class RootShell extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final showRailNavigation = AppTheme.isLargeScreen(context);
|
||||||
|
|
||||||
|
final destNames = AppNavigation.destinations.map((x) => x.page).toList();
|
||||||
|
final showBottomNavigation =
|
||||||
|
destNames.contains(routeName) && !showRailNavigation;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
key: rootScaffoldKey,
|
key: rootScaffoldKey,
|
||||||
drawer: AppTheme.isLargeScreen(context)
|
bottomNavigationBar: showBottomNavigation
|
||||||
? null
|
? AppNavigationBottom(
|
||||||
: AppNavigationDrawer(routeName: routeName),
|
initialIndex: destNames.indexOf(routeName ?? 'page'),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
body: AppTheme.isLargeScreen(context)
|
body: AppTheme.isLargeScreen(context)
|
||||||
? Row(
|
? Row(
|
||||||
children: [
|
children: [
|
||||||
if (showNavigation) AppNavigationDrawer(routeName: routeName),
|
if (showRailNavigation) const AppNavigationRail(),
|
||||||
if (showNavigation)
|
if (showRailNavigation)
|
||||||
const VerticalDivider(thickness: 0.3, width: 1),
|
const VerticalDivider(
|
||||||
|
width: 0.3,
|
||||||
|
thickness: 0.3,
|
||||||
|
),
|
||||||
Expanded(child: child),
|
Expanded(child: child),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -1,62 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
import 'package:solian/theme.dart';
|
|
||||||
import 'package:solian/widgets/app_bar_leading.dart';
|
|
||||||
import 'package:solian/widgets/app_bar_title.dart';
|
|
||||||
import 'package:solian/widgets/sidebar/sidebar_placeholder.dart';
|
|
||||||
|
|
||||||
class SidebarShell extends StatelessWidget {
|
|
||||||
final bool showAppBar;
|
|
||||||
final GoRouterState state;
|
|
||||||
final Widget child;
|
|
||||||
|
|
||||||
final bool sidebarFirst;
|
|
||||||
final Widget? sidebar;
|
|
||||||
|
|
||||||
const SidebarShell({
|
|
||||||
super.key,
|
|
||||||
required this.child,
|
|
||||||
required this.state,
|
|
||||||
this.showAppBar = true,
|
|
||||||
this.sidebarFirst = false,
|
|
||||||
this.sidebar,
|
|
||||||
});
|
|
||||||
|
|
||||||
List<Widget> buildContent(BuildContext context) {
|
|
||||||
return [
|
|
||||||
Flexible(
|
|
||||||
flex: 2,
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
if (AppTheme.isExtraLargeScreen(context))
|
|
||||||
const VerticalDivider(thickness: 0.3, width: 1),
|
|
||||||
if (AppTheme.isExtraLargeScreen(context))
|
|
||||||
Flexible(
|
|
||||||
flex: 1,
|
|
||||||
child: sidebar ?? const SidebarPlaceholder(),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: showAppBar
|
|
||||||
? AppBar(
|
|
||||||
leading: AppBarLeadingButton.adaptive(context),
|
|
||||||
title: AppBarTitle(state.topRoute?.name?.tr ?? 'page'.tr),
|
|
||||||
centerTitle: false,
|
|
||||||
toolbarHeight: AppTheme.toolbarHeight(context),
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
body: AppTheme.isLargeScreen(context)
|
|
||||||
? Row(
|
|
||||||
children: sidebarFirst
|
|
||||||
? buildContent(context).reversed.toList()
|
|
||||||
: buildContent(context),
|
|
||||||
)
|
|
||||||
: child,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -6,7 +6,10 @@ abstract class AppTheme {
|
|||||||
MediaQuery.of(context).size.width > 640;
|
MediaQuery.of(context).size.width > 640;
|
||||||
|
|
||||||
static bool isExtraLargeScreen(BuildContext context) =>
|
static bool isExtraLargeScreen(BuildContext context) =>
|
||||||
MediaQuery.of(context).size.width > 720;
|
MediaQuery.of(context).size.width > 920;
|
||||||
|
|
||||||
|
static bool isUltraLargeScreen(BuildContext context) =>
|
||||||
|
MediaQuery.of(context).size.width > 1200;
|
||||||
|
|
||||||
static bool isSpecializedMacOS(BuildContext context) =>
|
static bool isSpecializedMacOS(BuildContext context) =>
|
||||||
PlatformInfo.isMacOS && !AppTheme.isLargeScreen(context);
|
PlatformInfo.isMacOS && !AppTheme.isLargeScreen(context);
|
||||||
@ -35,6 +38,9 @@ abstract class AppTheme {
|
|||||||
brightness: brightness,
|
brightness: brightness,
|
||||||
seedColor: seedColor ?? const Color.fromRGBO(154, 98, 91, 1),
|
seedColor: seedColor ?? const Color.fromRGBO(154, 98, 91, 1),
|
||||||
),
|
),
|
||||||
|
snackBarTheme: const SnackBarThemeData(
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
),
|
||||||
fontFamily: 'Comfortaa',
|
fontFamily: 'Comfortaa',
|
||||||
fontFamilyFallback: [
|
fontFamilyFallback: [
|
||||||
'NotoSansSC',
|
'NotoSansSC',
|
||||||
|
@ -7,6 +7,7 @@ class AccountAvatar extends StatelessWidget {
|
|||||||
final Color? bgColor;
|
final Color? bgColor;
|
||||||
final Color? feColor;
|
final Color? feColor;
|
||||||
final double? radius;
|
final double? radius;
|
||||||
|
final Widget? fallbackWidget;
|
||||||
|
|
||||||
const AccountAvatar({
|
const AccountAvatar({
|
||||||
super.key,
|
super.key,
|
||||||
@ -14,6 +15,7 @@ class AccountAvatar extends StatelessWidget {
|
|||||||
this.bgColor,
|
this.bgColor,
|
||||||
this.feColor,
|
this.feColor,
|
||||||
this.radius,
|
this.radius,
|
||||||
|
this.fallbackWidget,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -35,11 +37,12 @@ class AccountAvatar extends StatelessWidget {
|
|||||||
backgroundColor: bgColor,
|
backgroundColor: bgColor,
|
||||||
backgroundImage: !isEmpty ? AutoCacheImage.provider(url) : null,
|
backgroundImage: !isEmpty ? AutoCacheImage.provider(url) : null,
|
||||||
child: isEmpty
|
child: isEmpty
|
||||||
? Icon(
|
? (fallbackWidget ??
|
||||||
Icons.account_circle,
|
Icon(
|
||||||
size: radius != null ? radius! * 1.2 : 24,
|
Icons.account_circle,
|
||||||
color: feColor,
|
size: radius != null ? radius! * 1.2 : 24,
|
||||||
)
|
color: feColor,
|
||||||
|
))
|
||||||
: null,
|
: null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,7 @@ class AccountHeadingWidget extends StatelessWidget {
|
|||||||
final AccountProfile? profile;
|
final AccountProfile? profile;
|
||||||
final List<AccountBadge>? badges;
|
final List<AccountBadge>? badges;
|
||||||
final List<Widget>? extraWidgets;
|
final List<Widget>? extraWidgets;
|
||||||
|
final List<Widget>? appendWidgets;
|
||||||
|
|
||||||
final Future<Response>? status;
|
final Future<Response>? status;
|
||||||
final Function? onEditStatus;
|
final Function? onEditStatus;
|
||||||
@ -39,6 +40,7 @@ class AccountHeadingWidget extends StatelessWidget {
|
|||||||
this.profile,
|
this.profile,
|
||||||
this.status,
|
this.status,
|
||||||
this.extraWidgets,
|
this.extraWidgets,
|
||||||
|
this.appendWidgets,
|
||||||
this.onEditStatus,
|
this.onEditStatus,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -257,6 +259,7 @@ class AccountHeadingWidget extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
).paddingSymmetric(horizontal: 16),
|
).paddingSymmetric(horizontal: 16),
|
||||||
|
...?appendWidgets?.map((x) => x.paddingSymmetric(horizontal: 16)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -106,10 +106,14 @@ class _AccountProfilePopupState extends State<AccountProfilePopup> {
|
|||||||
extraWidgets: [
|
extraWidgets: [
|
||||||
Card(
|
Card(
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
|
leading: const Icon(
|
||||||
|
Icons.contact_page_outlined,
|
||||||
|
),
|
||||||
shape: const RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||||
),
|
),
|
||||||
title: Text('visitProfilePage'.tr),
|
title: Text('visitProfilePage'.tr),
|
||||||
|
subtitle: Text('learnMoreAboutPerson'.tr),
|
||||||
visualDensity:
|
visualDensity:
|
||||||
const VisualDensity(horizontal: -4, vertical: -2),
|
const VisualDensity(horizontal: -4, vertical: -2),
|
||||||
trailing: const Icon(Icons.chevron_right),
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
@ -28,42 +28,46 @@ class SilverRelativeList extends StatelessWidget {
|
|||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
useRootNavigator: true,
|
useRootNavigator: true,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
backgroundColor: Theme
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.surface,
|
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) =>
|
builder: (context) => AccountProfilePopup(
|
||||||
AccountProfilePopup(
|
name: element.related.name,
|
||||||
name: element.related.name,
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
trailing: Row(
|
trailing: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
if(element.status != 1 && element.status != 3)
|
if (element.status != 1 && element.status != 3)
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.check),
|
icon: const Icon(Icons.check),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
final RelationshipProvider provider = Get.find();
|
final RelationshipProvider provider = Get.find();
|
||||||
if (element.status == 0) {
|
if (element.status == 0) {
|
||||||
provider.handleRelation(element, true).then((_) => onUpdate());
|
provider
|
||||||
|
.handleRelation(element, true)
|
||||||
|
.then((_) => onUpdate());
|
||||||
} else {
|
} else {
|
||||||
provider.editRelation(element, 1).then((_) => onUpdate());
|
provider
|
||||||
|
.editRelation(element.relatedId, 1)
|
||||||
|
.then((_) => onUpdate());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if(element.status != 2 && element.status != 3)
|
if (element.status != 2 && element.status != 3)
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.close),
|
icon: const Icon(Icons.close),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
final RelationshipProvider provider = Get.find();
|
final RelationshipProvider provider = Get.find();
|
||||||
if (element.status == 0) {
|
if (element.status == 0) {
|
||||||
provider.handleRelation(element, false).then((_) => onUpdate());
|
provider
|
||||||
|
.handleRelation(element, false)
|
||||||
|
.then((_) => onUpdate());
|
||||||
} else {
|
} else {
|
||||||
provider.editRelation(element, 2).then((_) => onUpdate());
|
provider
|
||||||
|
.editRelation(element.relatedId, 2)
|
||||||
|
.then((_) => onUpdate());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -396,7 +396,8 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
|
|||||||
),
|
),
|
||||||
if (!element.isCompleted &&
|
if (!element.isCompleted &&
|
||||||
element.error == null &&
|
element.error == null &&
|
||||||
canBeCrop)
|
canBeCrop &&
|
||||||
|
PlatformInfo.canCropImage)
|
||||||
Obx(
|
Obx(
|
||||||
() => IconButton(
|
() => IconButton(
|
||||||
color: Colors.teal,
|
color: Colors.teal,
|
||||||
@ -744,8 +745,8 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
|
|||||||
return IgnorePointer(
|
return IgnorePointer(
|
||||||
ignoring: _uploadController.isUploading.value,
|
ignoring: _uploadController.isUploading.value,
|
||||||
child: Container(
|
child: Container(
|
||||||
height: 64,
|
|
||||||
width: MediaQuery.of(context).size.width,
|
width: MediaQuery.of(context).size.width,
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border(
|
border: Border(
|
||||||
top: BorderSide(
|
top: BorderSide(
|
||||||
@ -754,67 +755,72 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: SingleChildScrollView(
|
child: Wrap(
|
||||||
scrollDirection: Axis.horizontal,
|
spacing: 8,
|
||||||
child: Wrap(
|
runSpacing: 8,
|
||||||
spacing: 8,
|
alignment: WrapAlignment.center,
|
||||||
runSpacing: 0,
|
runAlignment: WrapAlignment.center,
|
||||||
alignment: WrapAlignment.center,
|
children: [
|
||||||
runAlignment: WrapAlignment.center,
|
if ((PlatformInfo.isDesktop ||
|
||||||
children: [
|
PlatformInfo.isIOS ||
|
||||||
if ((PlatformInfo.isDesktop ||
|
PlatformInfo.isWeb) &&
|
||||||
PlatformInfo.isIOS ||
|
!widget.imageOnly)
|
||||||
PlatformInfo.isWeb) &&
|
IconButton(
|
||||||
!widget.imageOnly)
|
icon: const Icon(Icons.paste),
|
||||||
ElevatedButton.icon(
|
tooltip: 'attachmentAddClipboard'.tr,
|
||||||
icon: const Icon(Icons.paste),
|
|
||||||
label: Text('attachmentAddClipboard'.tr),
|
|
||||||
style: const ButtonStyle(visualDensity: density),
|
|
||||||
onPressed: () => _pasteFileToUpload(),
|
|
||||||
),
|
|
||||||
ElevatedButton.icon(
|
|
||||||
icon: const Icon(Icons.add_photo_alternate),
|
|
||||||
label: Text('attachmentAddGalleryPhoto'.tr),
|
|
||||||
style: const ButtonStyle(visualDensity: density),
|
style: const ButtonStyle(visualDensity: density),
|
||||||
onPressed: () => _pickPhotoToUpload(),
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
onPressed: () => _pasteFileToUpload(),
|
||||||
),
|
),
|
||||||
if (!widget.imageOnly)
|
IconButton(
|
||||||
ElevatedButton.icon(
|
icon: const Icon(Icons.add_photo_alternate),
|
||||||
icon: const Icon(Icons.add_road),
|
tooltip: 'attachmentAddGalleryPhoto'.tr,
|
||||||
label: Text('attachmentAddGalleryVideo'.tr),
|
style: const ButtonStyle(visualDensity: density),
|
||||||
style: const ButtonStyle(visualDensity: density),
|
color: Theme.of(context).colorScheme.primary,
|
||||||
onPressed: () => _pickVideoToUpload(),
|
onPressed: () => _pickPhotoToUpload(),
|
||||||
),
|
),
|
||||||
ElevatedButton.icon(
|
if (!widget.imageOnly)
|
||||||
icon: const Icon(Icons.photo_camera_back),
|
IconButton(
|
||||||
label: Text('attachmentAddCameraPhoto'.tr),
|
icon: const Icon(Icons.add_road),
|
||||||
|
tooltip: 'attachmentAddGalleryVideo'.tr,
|
||||||
style: const ButtonStyle(visualDensity: density),
|
style: const ButtonStyle(visualDensity: density),
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
onPressed: () => _pickVideoToUpload(),
|
||||||
|
),
|
||||||
|
if (PlatformInfo.isMobile)
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.photo_camera_back),
|
||||||
|
tooltip: 'attachmentAddCameraPhoto'.tr,
|
||||||
|
style: const ButtonStyle(visualDensity: density),
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
onPressed: () => _takeMediaToUpload(false),
|
onPressed: () => _takeMediaToUpload(false),
|
||||||
),
|
),
|
||||||
if (!widget.imageOnly)
|
if (!widget.imageOnly && PlatformInfo.isMobile)
|
||||||
ElevatedButton.icon(
|
IconButton(
|
||||||
icon: const Icon(Icons.video_camera_back_outlined),
|
icon: const Icon(Icons.video_camera_back_outlined),
|
||||||
label: Text('attachmentAddCameraVideo'.tr),
|
tooltip: 'attachmentAddCameraVideo'.tr,
|
||||||
style: const ButtonStyle(visualDensity: density),
|
style: const ButtonStyle(visualDensity: density),
|
||||||
onPressed: () => _takeMediaToUpload(true),
|
color: Theme.of(context).colorScheme.primary,
|
||||||
),
|
onPressed: () => _takeMediaToUpload(true),
|
||||||
if (!widget.imageOnly)
|
),
|
||||||
ElevatedButton.icon(
|
if (!widget.imageOnly)
|
||||||
icon: const Icon(Icons.file_present_rounded),
|
IconButton(
|
||||||
label: Text('attachmentAddFile'.tr),
|
icon: const Icon(Icons.file_present_rounded),
|
||||||
style: const ButtonStyle(visualDensity: density),
|
tooltip: 'attachmentAddFile'.tr,
|
||||||
onPressed: () => _pickFileToUpload(),
|
style: const ButtonStyle(visualDensity: density),
|
||||||
),
|
color: Theme.of(context).colorScheme.primary,
|
||||||
if (!widget.imageOnly)
|
onPressed: () => _pickFileToUpload(),
|
||||||
ElevatedButton.icon(
|
),
|
||||||
icon: const Icon(Icons.link),
|
if (!widget.imageOnly)
|
||||||
label: Text('attachmentAddFile'.tr),
|
IconButton(
|
||||||
style: const ButtonStyle(visualDensity: density),
|
icon: const Icon(Icons.link),
|
||||||
onPressed: () => _linkAttachments(),
|
tooltip: 'attachmentAddLink'.tr,
|
||||||
),
|
style: const ButtonStyle(visualDensity: density),
|
||||||
],
|
color: Theme.of(context).colorScheme.primary,
|
||||||
).paddingSymmetric(horizontal: 12),
|
onPressed: () => _linkAttachments(),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
).paddingSymmetric(horizontal: 12),
|
||||||
)
|
)
|
||||||
.animate(
|
.animate(
|
||||||
target: _uploadController.isUploading.value ? 0 : 1,
|
target: _uploadController.isUploading.value ? 0 : 1,
|
||||||
|
@ -21,6 +21,7 @@ class AttachmentItem extends StatefulWidget {
|
|||||||
final bool showBadge;
|
final bool showBadge;
|
||||||
final bool showHideButton;
|
final bool showHideButton;
|
||||||
final bool autoload;
|
final bool autoload;
|
||||||
|
final bool isDense;
|
||||||
final BoxFit fit;
|
final BoxFit fit;
|
||||||
final String? badge;
|
final String? badge;
|
||||||
final Function? onHide;
|
final Function? onHide;
|
||||||
@ -34,6 +35,7 @@ class AttachmentItem extends StatefulWidget {
|
|||||||
this.showBadge = true,
|
this.showBadge = true,
|
||||||
this.showHideButton = true,
|
this.showHideButton = true,
|
||||||
this.autoload = false,
|
this.autoload = false,
|
||||||
|
this.isDense = false,
|
||||||
this.onHide,
|
this.onHide,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -53,6 +55,7 @@ class _AttachmentItemState extends State<AttachmentItem> {
|
|||||||
fit: widget.fit,
|
fit: widget.fit,
|
||||||
showBadge: widget.showBadge,
|
showBadge: widget.showBadge,
|
||||||
showHideButton: widget.showHideButton,
|
showHideButton: widget.showHideButton,
|
||||||
|
isDense: widget.isDense,
|
||||||
onHide: widget.onHide,
|
onHide: widget.onHide,
|
||||||
);
|
);
|
||||||
case 'video':
|
case 'video':
|
||||||
@ -120,6 +123,7 @@ class _AttachmentItemImage extends StatelessWidget {
|
|||||||
final bool showBadge;
|
final bool showBadge;
|
||||||
final bool showHideButton;
|
final bool showHideButton;
|
||||||
final BoxFit fit;
|
final BoxFit fit;
|
||||||
|
final bool isDense;
|
||||||
final String? badge;
|
final String? badge;
|
||||||
final Function? onHide;
|
final Function? onHide;
|
||||||
|
|
||||||
@ -128,6 +132,7 @@ class _AttachmentItemImage extends StatelessWidget {
|
|||||||
required this.item,
|
required this.item,
|
||||||
required this.showBadge,
|
required this.showBadge,
|
||||||
required this.showHideButton,
|
required this.showHideButton,
|
||||||
|
required this.isDense,
|
||||||
required this.fit,
|
required this.fit,
|
||||||
this.badge,
|
this.badge,
|
||||||
this.onHide,
|
this.onHide,
|
||||||
@ -146,6 +151,7 @@ class _AttachmentItemImage extends StatelessWidget {
|
|||||||
'/attachments/${item.rid}',
|
'/attachments/${item.rid}',
|
||||||
),
|
),
|
||||||
fit: fit,
|
fit: fit,
|
||||||
|
isDense: isDense,
|
||||||
),
|
),
|
||||||
if (showBadge && badge != null)
|
if (showBadge && badge != null)
|
||||||
Positioned(
|
Positioned(
|
||||||
|
@ -338,6 +338,7 @@ class AttachmentListEntry extends StatelessWidget {
|
|||||||
badge: showBadge ? badgeContent : null,
|
badge: showBadge ? badgeContent : null,
|
||||||
showHideButton: !item!.isMature || showMature,
|
showHideButton: !item!.isMature || showMature,
|
||||||
autoload: autoload,
|
autoload: autoload,
|
||||||
|
isDense: isDense,
|
||||||
onHide: () {
|
onHide: () {
|
||||||
onReveal(false);
|
onReveal(false);
|
||||||
},
|
},
|
||||||
|
@ -34,8 +34,17 @@ class AutoCacheImage extends StatelessWidget {
|
|||||||
progressIndicatorBuilder: noProgressIndicator
|
progressIndicatorBuilder: noProgressIndicator
|
||||||
? null
|
? null
|
||||||
: (context, url, downloadProgress) => Center(
|
: (context, url, downloadProgress) => Center(
|
||||||
child: CircularProgressIndicator(
|
child: TweenAnimationBuilder(
|
||||||
value: downloadProgress.progress,
|
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
|
errorWidget: noErrorWidget
|
||||||
@ -74,11 +83,20 @@ class AutoCacheImage extends StatelessWidget {
|
|||||||
ImageChunkEvent? loadingProgress) {
|
ImageChunkEvent? loadingProgress) {
|
||||||
if (loadingProgress == null) return child;
|
if (loadingProgress == null) return child;
|
||||||
return Center(
|
return Center(
|
||||||
child: CircularProgressIndicator(
|
child: TweenAnimationBuilder(
|
||||||
value: loadingProgress.expectedTotalBytes != null
|
tween: Tween(
|
||||||
? loadingProgress.cumulativeBytesLoaded /
|
begin: 0,
|
||||||
loadingProgress.expectedTotalBytes!
|
end: loadingProgress.expectedTotalBytes != null
|
||||||
: null,
|
? loadingProgress.cumulativeBytesLoaded /
|
||||||
|
loadingProgress.expectedTotalBytes!
|
||||||
|
: 0,
|
||||||
|
),
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
builder: (context, value, _) => CircularProgressIndicator(
|
||||||
|
value: loadingProgress.expectedTotalBytes != null
|
||||||
|
? value.toDouble()
|
||||||
|
: null,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -98,12 +98,12 @@ class ChannelCallIndicator extends StatelessWidget {
|
|||||||
child: Text('callJoin'.tr),
|
child: Text('callJoin'.tr),
|
||||||
);
|
);
|
||||||
} else if (call.channel.value?.id == channel.id &&
|
} else if (call.channel.value?.id == channel.id &&
|
||||||
!AppTheme.isLargeScreen(context)) {
|
!AppTheme.isUltraLargeScreen(context)) {
|
||||||
return TextButton(
|
return TextButton(
|
||||||
onPressed: () => onJoin(),
|
onPressed: () => onJoin(),
|
||||||
child: Text('callResume'.tr),
|
child: Text('callResume'.tr),
|
||||||
);
|
);
|
||||||
} else if (!AppTheme.isLargeScreen(context)) {
|
} else if (!AppTheme.isUltraLargeScreen(context)) {
|
||||||
return TextButton(
|
return TextButton(
|
||||||
onPressed: null,
|
onPressed: null,
|
||||||
child: Text('callJoin'.tr),
|
child: Text('callJoin'.tr),
|
||||||
|
@ -4,18 +4,18 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
import 'package:solian/controllers/chat_events_controller.dart';
|
import 'package:solian/controllers/chat_events_controller.dart';
|
||||||
import 'package:solian/models/channel.dart';
|
import 'package:solian/models/channel.dart';
|
||||||
import 'package:solian/platform.dart';
|
import 'package:solian/platform.dart';
|
||||||
|
import 'package:solian/providers/database/database.dart';
|
||||||
import 'package:solian/router.dart';
|
import 'package:solian/router.dart';
|
||||||
import 'package:solian/widgets/account/account_avatar.dart';
|
import 'package:solian/widgets/account/account_avatar.dart';
|
||||||
|
import 'package:badges/badges.dart' as badges;
|
||||||
|
|
||||||
class ChannelListWidget extends StatefulWidget {
|
class ChannelListWidget extends StatefulWidget {
|
||||||
final List<Channel> channels;
|
final List<Channel> channels;
|
||||||
final int selfId;
|
final int selfId;
|
||||||
final bool isDense;
|
|
||||||
final bool isCollapsed;
|
|
||||||
final bool noCategory;
|
|
||||||
final bool useReplace;
|
final bool useReplace;
|
||||||
final Function(Channel)? onSelected;
|
final Function(Channel)? onSelected;
|
||||||
|
|
||||||
@ -23,9 +23,6 @@ class ChannelListWidget extends StatefulWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
required this.channels,
|
required this.channels,
|
||||||
required this.selfId,
|
required this.selfId,
|
||||||
this.isDense = false,
|
|
||||||
this.isCollapsed = false,
|
|
||||||
this.noCategory = false,
|
|
||||||
this.useReplace = false,
|
this.useReplace = false,
|
||||||
this.onSelected,
|
this.onSelected,
|
||||||
});
|
});
|
||||||
@ -35,43 +32,25 @@ class ChannelListWidget extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _ChannelListWidgetState extends State<ChannelListWidget> {
|
class _ChannelListWidgetState extends State<ChannelListWidget> {
|
||||||
final List<Channel> _globalChannels = List.empty(growable: true);
|
Map<int, LocalMessageEventTableData>? _lastMessages;
|
||||||
final Map<String, List<Channel>> _inRealms = {};
|
|
||||||
|
|
||||||
final ChatEventController _eventController = ChatEventController();
|
final ChatEventController _eventController = ChatEventController();
|
||||||
|
|
||||||
void _mapChannels() {
|
Future<void> _loadLastMessages() async {
|
||||||
_inRealms.clear();
|
final messages = await _eventController.src.getLastInAllChannels();
|
||||||
_globalChannels.clear();
|
setState(() {
|
||||||
|
_lastMessages = messages
|
||||||
if (widget.noCategory) {
|
.map((k, v) => MapEntry(k, v.firstOrNull))
|
||||||
_globalChannels.addAll(widget.channels);
|
.cast<int, LocalMessageEventTableData>();
|
||||||
return;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
for (final channel in widget.channels) {
|
|
||||||
if (channel.realmId != null) {
|
|
||||||
if (_inRealms[channel.realm!.alias] == null) {
|
|
||||||
_inRealms[channel.realm!.alias] = List.empty(growable: true);
|
|
||||||
}
|
|
||||||
_inRealms[channel.realm!.alias]!.add(channel);
|
|
||||||
} else {
|
|
||||||
_globalChannels.add(channel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didUpdateWidget(covariant ChannelListWidget oldWidget) {
|
|
||||||
super.didUpdateWidget(oldWidget);
|
|
||||||
setState(() => _mapChannels());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_mapChannels();
|
_eventController.initialize().then((_) {
|
||||||
_eventController.initialize();
|
_loadLastMessages();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _gotoChannel(Channel item) {
|
void _gotoChannel(Channel item) {
|
||||||
@ -98,107 +77,183 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDirectMessageDescription(Channel item, ChannelMember otherside) {
|
Widget _buildTitle(Channel item, ChannelMember? otherside) {
|
||||||
|
if (otherside != null) {
|
||||||
|
return Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Expanded(child: Text(otherside.account.nick)),
|
||||||
|
if (_lastMessages != null && _lastMessages![item.id] != null)
|
||||||
|
Text(
|
||||||
|
DateFormat('MM/dd').format(
|
||||||
|
_lastMessages![item.id]!.createdAt.toLocal(),
|
||||||
|
),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color:
|
||||||
|
Theme.of(context).colorScheme.onSurface.withOpacity(0.75),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Expanded(child: Text(item.name)),
|
||||||
|
if (_lastMessages != null && _lastMessages![item.id] != null)
|
||||||
|
Text(
|
||||||
|
DateFormat('MM/dd').format(
|
||||||
|
_lastMessages![item.id]!.createdAt.toLocal(),
|
||||||
|
),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.75),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSubtitle(Channel item, ChannelMember? otherside) {
|
||||||
if (PlatformInfo.isWeb) {
|
if (PlatformInfo.isWeb) {
|
||||||
return Text('channelDirectDescription'.trParams(
|
return otherside != null
|
||||||
{'username': '@${otherside.account.name}'},
|
? Text(
|
||||||
));
|
'channelDirectDescription'.trParams(
|
||||||
|
{'username': '@${otherside.account.name}'},
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
item.description,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return FutureBuilder(
|
return AnimatedSwitcher(
|
||||||
future: Future.delayed(
|
switchInCurve: Curves.easeIn,
|
||||||
const Duration(milliseconds: 500),
|
switchOutCurve: Curves.easeOut,
|
||||||
() => _eventController.src.getLastInChannel(item),
|
transitionBuilder: (child, animation) {
|
||||||
),
|
return FadeTransition(opacity: animation, child: child);
|
||||||
builder: (context, snapshot) {
|
},
|
||||||
if (!snapshot.hasData && snapshot.data == null) {
|
duration: const Duration(milliseconds: 300),
|
||||||
return Text('channelDirectDescription'.trParams(
|
child: (_lastMessages == null || _lastMessages![item.id] == null)
|
||||||
{'username': '@${otherside.account.name}'},
|
? Builder(
|
||||||
));
|
builder: (context) {
|
||||||
}
|
return otherside != null
|
||||||
|
? Text(
|
||||||
final data = snapshot.data!.data!;
|
'channelDirectDescription'.trParams(
|
||||||
return Text(
|
{'username': '@${otherside.account.name}'},
|
||||||
'${data.sender.account.nick}: ${data.body['text'] ?? 'Unsupported message to preview'}',
|
),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
item.description,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: Builder(
|
||||||
|
builder: (context) {
|
||||||
|
final data = _lastMessages![item.id]!.data!;
|
||||||
|
return Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
if (item.type == 0)
|
||||||
|
Badge(
|
||||||
|
label: Text(data.sender.account.nick),
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).colorScheme.secondaryContainer,
|
||||||
|
textColor:
|
||||||
|
Theme.of(context).colorScheme.onSecondaryContainer,
|
||||||
|
),
|
||||||
|
if (item.type == 0) const Gap(6),
|
||||||
|
if (data.body['text'] != null)
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
data.body['text'],
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Badge(label: Text('unablePreview'.tr)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
layoutBuilder: (currentChild, previousChildren) {
|
||||||
|
return Stack(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
children: <Widget>[
|
||||||
|
...previousChildren,
|
||||||
|
if (currentChild != null) currentChild,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildEntry(Channel item) {
|
Widget _buildEntry(Channel item) {
|
||||||
final padding = widget.isDense
|
const padding = EdgeInsets.symmetric(horizontal: 20);
|
||||||
? const EdgeInsets.symmetric(horizontal: 20)
|
|
||||||
: const EdgeInsets.symmetric(horizontal: 16);
|
|
||||||
|
|
||||||
if (item.type == 1) {
|
final otherside =
|
||||||
final otherside =
|
item.members!.where((e) => e.account.id != widget.selfId).firstOrNull;
|
||||||
item.members!.where((e) => e.account.id != widget.selfId).first;
|
|
||||||
|
|
||||||
|
if (item.type == 1 && otherside != null) {
|
||||||
final avatar = AccountAvatar(
|
final avatar = AccountAvatar(
|
||||||
content: otherside.account.avatar,
|
content: otherside.account.avatar,
|
||||||
radius: widget.isDense ? 12 : 20,
|
radius: 20,
|
||||||
bgColor: Theme.of(context).colorScheme.primary,
|
bgColor: Theme.of(context).colorScheme.primary,
|
||||||
feColor: Theme.of(context).colorScheme.onPrimary,
|
feColor: Theme.of(context).colorScheme.onPrimary,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (widget.isCollapsed) {
|
|
||||||
return Tooltip(
|
|
||||||
message: otherside.account.nick,
|
|
||||||
child: InkWell(
|
|
||||||
child: avatar.paddingSymmetric(vertical: 12),
|
|
||||||
onTap: () => _gotoChannel(item),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: avatar,
|
leading: avatar,
|
||||||
contentPadding: padding,
|
contentPadding: padding,
|
||||||
title: Text(otherside.account.nick),
|
title: _buildTitle(item, otherside),
|
||||||
subtitle: !widget.isDense
|
subtitle: _buildSubtitle(item, otherside),
|
||||||
? _buildDirectMessageDescription(item, otherside)
|
|
||||||
: null,
|
|
||||||
onTap: () => _gotoChannel(item),
|
onTap: () => _gotoChannel(item),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
final avatar = CircleAvatar(
|
final avatar = CircleAvatar(
|
||||||
backgroundColor: item.realmId == null
|
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||||
? Theme.of(context).colorScheme.primary
|
radius: 20,
|
||||||
: Colors.transparent,
|
|
||||||
radius: widget.isDense ? 12 : 20,
|
|
||||||
child: FaIcon(
|
child: FaIcon(
|
||||||
FontAwesomeIcons.hashtag,
|
FontAwesomeIcons.hashtag,
|
||||||
color: item.realmId == null
|
color: Theme.of(context).colorScheme.onPrimary,
|
||||||
? Theme.of(context).colorScheme.onPrimary
|
size: 16,
|
||||||
: Theme.of(context).colorScheme.primary,
|
|
||||||
size: widget.isDense ? 12 : 16,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (widget.isCollapsed) {
|
|
||||||
return Tooltip(
|
|
||||||
message: item.name,
|
|
||||||
child: InkWell(
|
|
||||||
child: avatar.paddingSymmetric(vertical: 12),
|
|
||||||
onTap: () => _gotoChannel(item),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
minTileHeight: widget.isDense ? 48 : null,
|
minTileHeight: null,
|
||||||
leading: avatar,
|
leading: item.realmId == null
|
||||||
|
? avatar
|
||||||
|
: badges.Badge(
|
||||||
|
position: badges.BadgePosition.bottomEnd(bottom: -4, end: -6),
|
||||||
|
badgeStyle: badges.BadgeStyle(
|
||||||
|
badgeColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
|
padding: const EdgeInsets.all(2),
|
||||||
|
elevation: 8,
|
||||||
|
),
|
||||||
|
badgeContent: AccountAvatar(
|
||||||
|
content: item.realm?.avatar,
|
||||||
|
radius: 10,
|
||||||
|
fallbackWidget: const Icon(
|
||||||
|
Icons.workspaces,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: avatar,
|
||||||
|
),
|
||||||
contentPadding: padding,
|
contentPadding: padding,
|
||||||
title: Text(item.name),
|
title: _buildTitle(item, null),
|
||||||
subtitle: !widget.isDense
|
subtitle: _buildSubtitle(item, null),
|
||||||
? Text(
|
|
||||||
item.description,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
onTap: () => _gotoChannel(item),
|
onTap: () => _gotoChannel(item),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -206,49 +261,16 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (widget.noCategory) {
|
|
||||||
return CustomScrollView(
|
|
||||||
slivers: [
|
|
||||||
SliverList.builder(
|
|
||||||
itemCount: _globalChannels.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final element = _globalChannels[index];
|
|
||||||
return _buildEntry(element);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
SliverGap(max(16, MediaQuery.of(context).padding.bottom)),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return CustomScrollView(
|
return CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverList.builder(
|
SliverList.builder(
|
||||||
itemCount: _globalChannels.length,
|
itemCount: widget.channels.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final element = _globalChannels[index];
|
final element = widget.channels[index];
|
||||||
return _buildEntry(element);
|
return _buildEntry(element);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
SliverList.list(
|
SliverGap(max(16, MediaQuery.of(context).padding.bottom)),
|
||||||
children: _inRealms.entries.map((element) {
|
|
||||||
return ExpansionTile(
|
|
||||||
tilePadding: const EdgeInsets.only(left: 20, right: 24),
|
|
||||||
minTileHeight: 48,
|
|
||||||
title: Text(element.value.first.realm!.name),
|
|
||||||
leading: CircleAvatar(
|
|
||||||
backgroundColor: Colors.teal,
|
|
||||||
radius: widget.isDense ? 12 : 24,
|
|
||||||
child: Icon(
|
|
||||||
Icons.workspaces,
|
|
||||||
color: Colors.white,
|
|
||||||
size: widget.isDense ? 12 : 16,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
children: element.value.map((x) => _buildEntry(x)).toList(),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -49,6 +49,7 @@ class ChatEventMessage extends StatelessWidget {
|
|||||||
return MarkdownTextContent(
|
return MarkdownTextContent(
|
||||||
parentId: 'm${item.id}',
|
parentId: 'm${item.id}',
|
||||||
isSelectable: true,
|
isSelectable: true,
|
||||||
|
isAutoWarp: true,
|
||||||
content: body.text,
|
content: body.text,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ class DailySignHistoryChartDialog extends StatelessWidget {
|
|||||||
|
|
||||||
const DailySignHistoryChartDialog({super.key, required this.data});
|
const DailySignHistoryChartDialog({super.key, required this.data});
|
||||||
|
|
||||||
static List<String> signSymbols = ['大凶', '凶', '中平', '吉', '大吉'];
|
static final List<String> signSymbols = ['大凶', '凶', '中平', '吉', '大吉'];
|
||||||
|
|
||||||
DateTime? get _firstRecordDate => data?.map((x) => x.createdAt).reduce(
|
DateTime? get _firstRecordDate => data?.map((x) => x.createdAt).reduce(
|
||||||
(a, b) => DateTime.fromMillisecondsSinceEpoch(
|
(a, b) => DateTime.fromMillisecondsSinceEpoch(
|
||||||
@ -42,215 +42,222 @@ class DailySignHistoryChartDialog extends StatelessWidget {
|
|||||||
child: CircularProgressIndicator(),
|
child: CircularProgressIndicator(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: Column(
|
: SizedBox(
|
||||||
mainAxisSize: MainAxisSize.min,
|
width: double.maxFinite,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: ListView(
|
||||||
children: [
|
shrinkWrap: true,
|
||||||
Text(
|
children: [
|
||||||
'dailySignHistoryRecent'.tr,
|
Text(
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
'dailySignHistoryRecent'.tr,
|
||||||
).paddingOnly(bottom: 18),
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
SizedBox(
|
).paddingOnly(bottom: 18),
|
||||||
height: 180,
|
SizedBox(
|
||||||
width: max(640, MediaQuery.of(context).size.width),
|
height: 180,
|
||||||
child: LineChart(
|
width: max(640, MediaQuery.of(context).size.width),
|
||||||
LineChartData(
|
child: LineChart(
|
||||||
lineBarsData: [
|
LineChartData(
|
||||||
LineChartBarData(
|
lineBarsData: [
|
||||||
isCurved: true,
|
LineChartBarData(
|
||||||
isStrokeCapRound: true,
|
isCurved: true,
|
||||||
isStrokeJoinRound: true,
|
isStrokeCapRound: true,
|
||||||
color: Theme.of(context).colorScheme.primary,
|
isStrokeJoinRound: true,
|
||||||
belowBarData: BarAreaData(
|
color: Theme.of(context).colorScheme.primary,
|
||||||
show: true,
|
belowBarData: BarAreaData(
|
||||||
gradient: LinearGradient(
|
show: true,
|
||||||
colors: List.filled(
|
gradient: LinearGradient(
|
||||||
data!.length,
|
colors: List.filled(
|
||||||
Theme.of(context)
|
data!.length,
|
||||||
.colorScheme
|
Theme.of(context)
|
||||||
.primary
|
.colorScheme
|
||||||
.withOpacity(0.3),
|
.primary
|
||||||
).toList(),
|
.withOpacity(0.3),
|
||||||
),
|
).toList(),
|
||||||
),
|
|
||||||
spots: data!
|
|
||||||
.map(
|
|
||||||
(x) => FlSpot(
|
|
||||||
x.createdAt
|
|
||||||
.copyWith(
|
|
||||||
hour: 0,
|
|
||||||
minute: 0,
|
|
||||||
second: 0,
|
|
||||||
millisecond: 0,
|
|
||||||
microsecond: 0,
|
|
||||||
)
|
|
||||||
.millisecondsSinceEpoch
|
|
||||||
.toDouble(),
|
|
||||||
x.resultTier.toDouble(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
lineTouchData: LineTouchData(
|
|
||||||
touchTooltipData: LineTouchTooltipData(
|
|
||||||
getTooltipItems: (spots) => spots
|
|
||||||
.map((spot) => LineTooltipItem(
|
|
||||||
'${signSymbols[spot.y.toInt()]}\n${DateFormat('MM/dd').format(DateTime.fromMillisecondsSinceEpoch(spot.x.toInt()))}',
|
|
||||||
TextStyle(
|
|
||||||
color:
|
|
||||||
Theme.of(context).colorScheme.onSurface,
|
|
||||||
),
|
|
||||||
))
|
|
||||||
.toList(),
|
|
||||||
getTooltipColor: (_) =>
|
|
||||||
Theme.of(context).colorScheme.surfaceContainerHigh,
|
|
||||||
)),
|
|
||||||
titlesData: FlTitlesData(
|
|
||||||
topTitles: const AxisTitles(
|
|
||||||
sideTitles: SideTitles(showTitles: false),
|
|
||||||
),
|
|
||||||
rightTitles: const AxisTitles(
|
|
||||||
sideTitles: SideTitles(showTitles: false),
|
|
||||||
),
|
|
||||||
leftTitles: AxisTitles(
|
|
||||||
sideTitles: SideTitles(
|
|
||||||
showTitles: true,
|
|
||||||
reservedSize: 40,
|
|
||||||
interval: 1,
|
|
||||||
getTitlesWidget: (value, _) => Align(
|
|
||||||
alignment: Alignment.centerRight,
|
|
||||||
child: Text(
|
|
||||||
signSymbols[value.toInt()],
|
|
||||||
textAlign: TextAlign.right,
|
|
||||||
).paddingOnly(right: 8),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
bottomTitles: AxisTitles(
|
|
||||||
sideTitles: SideTitles(
|
|
||||||
showTitles: true,
|
|
||||||
reservedSize: 28,
|
|
||||||
interval: 86400000,
|
|
||||||
getTitlesWidget: (value, _) => Text(
|
|
||||||
DateFormat('dd').format(
|
|
||||||
DateTime.fromMillisecondsSinceEpoch(
|
|
||||||
value.toInt(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
|
||||||
).paddingOnly(top: 8),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
gridData: const FlGridData(show: false),
|
|
||||||
borderData: FlBorderData(show: false),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
).marginOnly(right: 24, bottom: 8, top: 8),
|
|
||||||
const Gap(16),
|
|
||||||
Text(
|
|
||||||
'dailySignHistoryReward'.tr,
|
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
|
||||||
).paddingOnly(bottom: 18),
|
|
||||||
SizedBox(
|
|
||||||
height: 180,
|
|
||||||
width: max(640, MediaQuery.of(context).size.width),
|
|
||||||
child: LineChart(
|
|
||||||
LineChartData(
|
|
||||||
lineBarsData: [
|
|
||||||
LineChartBarData(
|
|
||||||
isCurved: true,
|
|
||||||
isStrokeCapRound: true,
|
|
||||||
isStrokeJoinRound: true,
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
belowBarData: BarAreaData(
|
|
||||||
show: true,
|
|
||||||
gradient: LinearGradient(
|
|
||||||
colors: List.filled(
|
|
||||||
data!.length,
|
|
||||||
Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.primary
|
|
||||||
.withOpacity(0.3),
|
|
||||||
).toList(),
|
|
||||||
),
|
),
|
||||||
),
|
spots: data!
|
||||||
spots: data!
|
.map(
|
||||||
.map(
|
(x) => FlSpot(
|
||||||
(x) => FlSpot(
|
x.createdAt
|
||||||
x.createdAt
|
.copyWith(
|
||||||
.copyWith(
|
hour: 0,
|
||||||
hour: 0,
|
minute: 0,
|
||||||
minute: 0,
|
second: 0,
|
||||||
second: 0,
|
millisecond: 0,
|
||||||
millisecond: 0,
|
microsecond: 0,
|
||||||
microsecond: 0,
|
)
|
||||||
)
|
.millisecondsSinceEpoch
|
||||||
.millisecondsSinceEpoch
|
.toDouble(),
|
||||||
.toDouble(),
|
x.resultTier.toDouble(),
|
||||||
x.resultExperience.toDouble(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
lineTouchData: LineTouchData(
|
|
||||||
touchTooltipData: LineTouchTooltipData(
|
|
||||||
getTooltipItems: (spots) => spots
|
|
||||||
.map((spot) => LineTooltipItem(
|
|
||||||
'+${spot.y.toStringAsFixed(0)} EXP\n${DateFormat('MM/dd').format(DateTime.fromMillisecondsSinceEpoch(spot.x.toInt()))}',
|
|
||||||
TextStyle(
|
|
||||||
color:
|
|
||||||
Theme.of(context).colorScheme.onSurface,
|
|
||||||
),
|
),
|
||||||
))
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
getTooltipColor: (_) =>
|
)
|
||||||
Theme.of(context).colorScheme.surfaceContainerHigh,
|
],
|
||||||
)),
|
lineTouchData: LineTouchData(
|
||||||
titlesData: FlTitlesData(
|
touchTooltipData: LineTouchTooltipData(
|
||||||
topTitles: const AxisTitles(
|
getTooltipItems: (spots) => spots
|
||||||
sideTitles: SideTitles(showTitles: false),
|
.map((spot) => LineTooltipItem(
|
||||||
|
'${signSymbols[spot.y.toInt()]}\n${DateFormat('MM/dd').format(DateTime.fromMillisecondsSinceEpoch(spot.x.toInt()))}',
|
||||||
|
TextStyle(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSurface,
|
||||||
|
),
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
getTooltipColor: (_) => Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.surfaceContainerHigh,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
rightTitles: const AxisTitles(
|
titlesData: FlTitlesData(
|
||||||
sideTitles: SideTitles(showTitles: false),
|
topTitles: const AxisTitles(
|
||||||
),
|
sideTitles: SideTitles(showTitles: false),
|
||||||
leftTitles: AxisTitles(
|
),
|
||||||
sideTitles: SideTitles(
|
rightTitles: const AxisTitles(
|
||||||
showTitles: true,
|
sideTitles: SideTitles(showTitles: false),
|
||||||
reservedSize: 40,
|
),
|
||||||
getTitlesWidget: (value, _) => Align(
|
leftTitles: AxisTitles(
|
||||||
alignment: Alignment.centerRight,
|
sideTitles: SideTitles(
|
||||||
child: Text(
|
showTitles: true,
|
||||||
value.toStringAsFixed(0),
|
reservedSize: 40,
|
||||||
textAlign: TextAlign.right,
|
interval: 1,
|
||||||
).paddingOnly(right: 8),
|
getTitlesWidget: (value, _) => Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: Text(
|
||||||
|
signSymbols[value.toInt()],
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
).paddingOnly(right: 8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
bottomTitles: AxisTitles(
|
||||||
|
sideTitles: SideTitles(
|
||||||
|
showTitles: true,
|
||||||
|
reservedSize: 28,
|
||||||
|
interval: 86400000,
|
||||||
|
getTitlesWidget: (value, _) => Text(
|
||||||
|
DateFormat('dd').format(
|
||||||
|
DateTime.fromMillisecondsSinceEpoch(
|
||||||
|
value.toInt(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
).paddingOnly(top: 8),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
bottomTitles: AxisTitles(
|
gridData: const FlGridData(show: false),
|
||||||
sideTitles: SideTitles(
|
borderData: FlBorderData(show: false),
|
||||||
showTitles: true,
|
),
|
||||||
reservedSize: 28,
|
),
|
||||||
interval: 86400000,
|
).marginOnly(right: 24, bottom: 8, top: 8),
|
||||||
getTitlesWidget: (value, _) => Text(
|
const Gap(16),
|
||||||
DateFormat('dd').format(
|
Text(
|
||||||
DateTime.fromMillisecondsSinceEpoch(
|
'dailySignHistoryReward'.tr,
|
||||||
value.toInt(),
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
),
|
).paddingOnly(bottom: 18),
|
||||||
|
SizedBox(
|
||||||
|
height: 180,
|
||||||
|
width: max(640, MediaQuery.of(context).size.width),
|
||||||
|
child: LineChart(
|
||||||
|
LineChartData(
|
||||||
|
lineBarsData: [
|
||||||
|
LineChartBarData(
|
||||||
|
isCurved: true,
|
||||||
|
isStrokeCapRound: true,
|
||||||
|
isStrokeJoinRound: true,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
belowBarData: BarAreaData(
|
||||||
|
show: true,
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: List.filled(
|
||||||
|
data!.length,
|
||||||
|
Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.primary
|
||||||
|
.withOpacity(0.3),
|
||||||
|
).toList(),
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
),
|
||||||
).paddingOnly(top: 8),
|
spots: data!
|
||||||
|
.map(
|
||||||
|
(x) => FlSpot(
|
||||||
|
x.createdAt
|
||||||
|
.copyWith(
|
||||||
|
hour: 0,
|
||||||
|
minute: 0,
|
||||||
|
second: 0,
|
||||||
|
millisecond: 0,
|
||||||
|
microsecond: 0,
|
||||||
|
)
|
||||||
|
.millisecondsSinceEpoch
|
||||||
|
.toDouble(),
|
||||||
|
x.resultExperience.toDouble(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
lineTouchData: LineTouchData(
|
||||||
|
touchTooltipData: LineTouchTooltipData(
|
||||||
|
getTooltipItems: (spots) => spots
|
||||||
|
.map((spot) => LineTooltipItem(
|
||||||
|
'+${spot.y.toStringAsFixed(0)} EXP\n${DateFormat('MM/dd').format(DateTime.fromMillisecondsSinceEpoch(spot.x.toInt()))}',
|
||||||
|
TextStyle(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSurface,
|
||||||
|
),
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
getTooltipColor: (_) => Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.surfaceContainerHigh,
|
||||||
|
)),
|
||||||
|
titlesData: FlTitlesData(
|
||||||
|
topTitles: const AxisTitles(
|
||||||
|
sideTitles: SideTitles(showTitles: false),
|
||||||
|
),
|
||||||
|
rightTitles: const AxisTitles(
|
||||||
|
sideTitles: SideTitles(showTitles: false),
|
||||||
|
),
|
||||||
|
leftTitles: AxisTitles(
|
||||||
|
sideTitles: SideTitles(
|
||||||
|
showTitles: true,
|
||||||
|
reservedSize: 40,
|
||||||
|
getTitlesWidget: (value, _) => Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: Text(
|
||||||
|
value.toStringAsFixed(0),
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
).paddingOnly(right: 8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
bottomTitles: AxisTitles(
|
||||||
|
sideTitles: SideTitles(
|
||||||
|
showTitles: true,
|
||||||
|
reservedSize: 28,
|
||||||
|
interval: 86400000,
|
||||||
|
getTitlesWidget: (value, _) => Text(
|
||||||
|
DateFormat('dd').format(
|
||||||
|
DateTime.fromMillisecondsSinceEpoch(
|
||||||
|
value.toInt(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
).paddingOnly(top: 8),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
gridData: const FlGridData(show: false),
|
||||||
|
borderData: FlBorderData(show: false),
|
||||||
),
|
),
|
||||||
gridData: const FlGridData(show: false),
|
|
||||||
borderData: FlBorderData(show: false),
|
|
||||||
),
|
),
|
||||||
),
|
).marginOnly(right: 24, bottom: 8, top: 8),
|
||||||
).marginOnly(right: 24, bottom: 8, top: 8),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -74,9 +74,13 @@ class LinkExpansion extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
).paddingOnly(right: 8),
|
).paddingOnly(right: 8),
|
||||||
if (snapshot.data!.siteName != null)
|
if (snapshot.data!.siteName != null)
|
||||||
Text(
|
Expanded(
|
||||||
snapshot.data!.siteName!,
|
child: Text(
|
||||||
style: Theme.of(context).textTheme.labelLarge,
|
snapshot.data!.siteName!,
|
||||||
|
style: Theme.of(context).textTheme.labelLarge,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
).paddingOnly(
|
).paddingOnly(
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_markdown_selectionarea/flutter_markdown.dart';
|
import 'package:flutter_markdown_selectionarea/flutter_markdown.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:markdown/markdown.dart' as markdown;
|
import 'package:markdown/markdown.dart' as markdown;
|
||||||
import 'package:markdown/markdown.dart';
|
import 'package:markdown/markdown.dart';
|
||||||
@ -15,6 +16,7 @@ class MarkdownTextContent extends StatelessWidget {
|
|||||||
final String parentId;
|
final String parentId;
|
||||||
final bool isSelectable;
|
final bool isSelectable;
|
||||||
final bool isLargeText;
|
final bool isLargeText;
|
||||||
|
final bool isAutoWarp;
|
||||||
|
|
||||||
const MarkdownTextContent({
|
const MarkdownTextContent({
|
||||||
super.key,
|
super.key,
|
||||||
@ -22,139 +24,175 @@ class MarkdownTextContent extends StatelessWidget {
|
|||||||
required this.parentId,
|
required this.parentId,
|
||||||
this.isSelectable = false,
|
this.isSelectable = false,
|
||||||
this.isLargeText = false,
|
this.isLargeText = false,
|
||||||
|
this.isAutoWarp = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
Widget _buildContent(BuildContext context) {
|
Widget _buildContent(BuildContext context) {
|
||||||
final emojiRegex = RegExp(r':([-\w]+):');
|
final stickerRegex = RegExp(r':([-\w]+):');
|
||||||
final emojiMatch = emojiRegex.allMatches(content);
|
|
||||||
final isOnlyEmoji = content.replaceAll(emojiRegex, '').trim().isEmpty;
|
|
||||||
|
|
||||||
return Markdown(
|
// Split the content into paragraphs
|
||||||
shrinkWrap: true,
|
final paragraphs = content.split(RegExp(r'\n\s*\n'));
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
|
||||||
data: content,
|
// Iterate over each paragraph to process stickers individually
|
||||||
padding: EdgeInsets.zero,
|
List<Widget> contentWidgets = [];
|
||||||
styleSheet: MarkdownStyleSheet.fromTheme(
|
for (var idx = 0; idx < paragraphs.length; idx++) {
|
||||||
Theme.of(context),
|
// Getting paragraph
|
||||||
).copyWith(
|
var paragraph = paragraphs[idx];
|
||||||
textScaleFactor: isLargeText ? 1.1 : 1,
|
|
||||||
blockquote: TextStyle(
|
// Auto adding new-lines
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
if (isAutoWarp) {
|
||||||
),
|
paragraph = paragraph.replaceAll('\n', '\\\n');
|
||||||
blockquoteDecoration: BoxDecoration(
|
}
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
// Matching stickers
|
||||||
),
|
final stickerMatch = stickerRegex.allMatches(paragraph);
|
||||||
horizontalRuleDecoration: BoxDecoration(
|
final isOnlySticker =
|
||||||
border: Border(
|
paragraph.replaceAll(stickerRegex, '').trim().isEmpty;
|
||||||
top: BorderSide(
|
|
||||||
width: 1.0,
|
contentWidgets.add(
|
||||||
color: Theme.of(context).dividerColor,
|
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,
|
||||||
extensionSet: markdown.ExtensionSet(
|
<markdown.InlineSyntax>[
|
||||||
markdown.ExtensionSet.gitHubFlavored.blockSyntaxes,
|
_UserNameCardInlineSyntax(),
|
||||||
<markdown.InlineSyntax>[
|
_CustomEmoteInlineSyntax(),
|
||||||
_UserNameCardInlineSyntax(),
|
markdown.EmojiSyntax(),
|
||||||
_CustomEmoteInlineSyntax(),
|
markdown.AutolinkSyntax(),
|
||||||
markdown.EmojiSyntax(),
|
markdown.AutolinkExtensionSyntax(),
|
||||||
markdown.AutolinkSyntax(),
|
...markdown.ExtensionSet.gitHubFlavored.inlineSyntaxes
|
||||||
markdown.AutolinkExtensionSyntax(),
|
],
|
||||||
...markdown.ExtensionSet.gitHubFlavored.inlineSyntaxes
|
),
|
||||||
],
|
onTapLink: (text, href, title) async {
|
||||||
),
|
if (href == null) return;
|
||||||
onTapLink: (text, href, title) async {
|
if (href.startsWith('solink://')) {
|
||||||
if (href == null) return;
|
final segments = href.replaceFirst('solink://', '').split('/');
|
||||||
if (href.startsWith('solink://')) {
|
switch (segments[0]) {
|
||||||
final segments = href.replaceFirst('solink://', '').split('/');
|
case 'users':
|
||||||
switch (segments[0]) {
|
showModalBottomSheet(
|
||||||
case 'users':
|
useRootNavigator: true,
|
||||||
showModalBottomSheet(
|
isScrollControlled: true,
|
||||||
useRootNavigator: true,
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
isScrollControlled: true,
|
context: context,
|
||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
builder: (context) => AccountProfilePopup(
|
||||||
context: context,
|
name: segments[1],
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
fit = BoxFit.contain;
|
return;
|
||||||
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 AutoCacheImage(
|
await launchUrlString(
|
||||||
url,
|
href,
|
||||||
width: width,
|
mode: LaunchMode.externalApplication,
|
||||||
height: height,
|
);
|
||||||
fit: fit,
|
},
|
||||||
);
|
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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
80
lib/widgets/navigation/app_account_widget.dart
Normal file
80
lib/widgets/navigation/app_account_widget.dart
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:solian/models/account_status.dart';
|
||||||
|
import 'package:solian/providers/account_status.dart';
|
||||||
|
import 'package:solian/providers/auth.dart';
|
||||||
|
import 'package:solian/providers/relation.dart';
|
||||||
|
import 'package:badges/badges.dart' as badges;
|
||||||
|
import 'package:solian/widgets/account/account_avatar.dart';
|
||||||
|
|
||||||
|
class AppAccountWidget extends StatefulWidget {
|
||||||
|
const AppAccountWidget({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AppAccountWidget> createState() => _AppAccountWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AppAccountWidgetState extends State<AppAccountWidget> {
|
||||||
|
AccountStatus? _accountStatus;
|
||||||
|
|
||||||
|
Future<void> _getStatus() async {
|
||||||
|
final StatusProvider provider = Get.find();
|
||||||
|
|
||||||
|
final resp = await provider.getCurrentStatus();
|
||||||
|
final status = AccountStatus.fromJson(resp.body);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_accountStatus = status;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_getStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final AuthProvider auth = Get.find();
|
||||||
|
|
||||||
|
return Obx(() {
|
||||||
|
if (auth.isAuthorized.isFalse || auth.userProfile.value == null) {
|
||||||
|
return const Icon(Icons.account_circle);
|
||||||
|
}
|
||||||
|
|
||||||
|
final statusBadgeColor = _accountStatus != null
|
||||||
|
? StatusProvider.determineStatus(_accountStatus!).$2
|
||||||
|
: Colors.grey;
|
||||||
|
|
||||||
|
final RelationshipProvider relations = Get.find();
|
||||||
|
final accountNotifications = relations.friendRequestCount.value;
|
||||||
|
|
||||||
|
return badges.Badge(
|
||||||
|
badgeContent: Text(
|
||||||
|
accountNotifications.toString(),
|
||||||
|
style: const TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
|
showBadge: accountNotifications > 0,
|
||||||
|
position: badges.BadgePosition.topEnd(
|
||||||
|
top: -10,
|
||||||
|
end: -6,
|
||||||
|
),
|
||||||
|
child: badges.Badge(
|
||||||
|
showBadge: _accountStatus != null,
|
||||||
|
badgeStyle: badges.BadgeStyle(badgeColor: statusBadgeColor),
|
||||||
|
position: badges.BadgePosition.bottomEnd(
|
||||||
|
bottom: 0,
|
||||||
|
end: -2,
|
||||||
|
),
|
||||||
|
child: AccountAvatar(
|
||||||
|
radius: 14,
|
||||||
|
content: auth.userProfile.value!['avatar'],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -1,27 +1,33 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/utils.dart';
|
import 'package:get/utils.dart';
|
||||||
|
import 'package:solian/widgets/navigation/app_account_widget.dart';
|
||||||
|
|
||||||
abstract class AppNavigation {
|
abstract class AppNavigation {
|
||||||
static List<AppNavigationDestination> destinations = [
|
static List<AppNavigationDestination> destinations = [
|
||||||
AppNavigationDestination(
|
AppNavigationDestination(
|
||||||
icon: Icons.dashboard,
|
icon: const Icon(Icons.dashboard),
|
||||||
label: 'dashboard'.tr,
|
label: 'dashboardNav'.tr,
|
||||||
page: 'dashboard',
|
page: 'dashboard',
|
||||||
),
|
),
|
||||||
AppNavigationDestination(
|
AppNavigationDestination(
|
||||||
icon: Icons.newspaper,
|
icon: const Icon(Icons.explore),
|
||||||
label: 'feed'.tr,
|
label: 'explore'.tr,
|
||||||
page: 'feed',
|
page: 'explore',
|
||||||
),
|
),
|
||||||
AppNavigationDestination(
|
AppNavigationDestination(
|
||||||
icon: Icons.workspaces,
|
icon: const Icon(Icons.forum),
|
||||||
|
label: 'chat'.tr,
|
||||||
|
page: 'chat',
|
||||||
|
),
|
||||||
|
AppNavigationDestination(
|
||||||
|
icon: const Icon(Icons.workspaces),
|
||||||
label: 'realms'.tr,
|
label: 'realms'.tr,
|
||||||
page: 'realms',
|
page: 'realms',
|
||||||
),
|
),
|
||||||
AppNavigationDestination(
|
AppNavigationDestination(
|
||||||
icon: Icons.forum,
|
icon: const AppAccountWidget(),
|
||||||
label: 'chat'.tr,
|
label: 'accountNav'.tr,
|
||||||
page: 'chat',
|
page: 'account',
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -30,7 +36,7 @@ abstract class AppNavigation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class AppNavigationDestination {
|
class AppNavigationDestination {
|
||||||
final IconData icon;
|
final Widget icon;
|
||||||
final String label;
|
final String label;
|
||||||
final String page;
|
final String page;
|
||||||
|
|
||||||
|
47
lib/widgets/navigation/app_navigation_bottom.dart
Normal file
47
lib/widgets/navigation/app_navigation_bottom.dart
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:solian/router.dart';
|
||||||
|
import 'package:solian/widgets/navigation/app_navigation.dart';
|
||||||
|
|
||||||
|
class AppNavigationBottom extends StatefulWidget {
|
||||||
|
final int initialIndex;
|
||||||
|
|
||||||
|
const AppNavigationBottom({super.key, this.initialIndex = 0});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AppNavigationBottom> createState() => _AppNavigationBottomState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AppNavigationBottomState extends State<AppNavigationBottom> {
|
||||||
|
int _currentIndex = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
if (widget.initialIndex >= 0) {
|
||||||
|
_currentIndex = widget.initialIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BottomNavigationBar(
|
||||||
|
currentIndex: _currentIndex,
|
||||||
|
type: BottomNavigationBarType.fixed,
|
||||||
|
showUnselectedLabels: false,
|
||||||
|
showSelectedLabels: true,
|
||||||
|
landscapeLayout: BottomNavigationBarLandscapeLayout.centered,
|
||||||
|
items: AppNavigation.destinations
|
||||||
|
.map(
|
||||||
|
(x) => BottomNavigationBarItem(
|
||||||
|
icon: x.icon,
|
||||||
|
label: x.label,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
onTap: (idx) {
|
||||||
|
setState(() => _currentIndex = idx);
|
||||||
|
AppRouter.instance.goNamed(AppNavigation.destinations[idx].page);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,330 +0,0 @@
|
|||||||
import 'dart:math' as math;
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:solian/models/account_status.dart';
|
|
||||||
import 'package:solian/providers/account_status.dart';
|
|
||||||
import 'package:solian/providers/auth.dart';
|
|
||||||
import 'package:solian/providers/relation.dart';
|
|
||||||
import 'package:solian/router.dart';
|
|
||||||
import 'package:solian/shells/root_shell.dart';
|
|
||||||
import 'package:solian/theme.dart';
|
|
||||||
import 'package:solian/widgets/account/account_avatar.dart';
|
|
||||||
import 'package:solian/widgets/account/account_status_action.dart';
|
|
||||||
import 'package:solian/widgets/navigation/app_navigation.dart';
|
|
||||||
import 'package:badges/badges.dart' as badges;
|
|
||||||
import 'package:solian/widgets/navigation/app_navigation_region.dart';
|
|
||||||
|
|
||||||
class AppNavigationDrawer extends StatefulWidget {
|
|
||||||
final String? routeName;
|
|
||||||
|
|
||||||
const AppNavigationDrawer({super.key, this.routeName});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<AppNavigationDrawer> createState() => _AppNavigationDrawerState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AppNavigationDrawerState extends State<AppNavigationDrawer>
|
|
||||||
with TickerProviderStateMixin {
|
|
||||||
bool _isCollapsed = true;
|
|
||||||
|
|
||||||
late final AnimationController _drawerAnimationController =
|
|
||||||
AnimationController(
|
|
||||||
duration: const Duration(milliseconds: 500),
|
|
||||||
vsync: this,
|
|
||||||
);
|
|
||||||
late final Animation<double> _drawerAnimation = Tween<double>(
|
|
||||||
begin: 80.0,
|
|
||||||
end: 304.0,
|
|
||||||
).animate(CurvedAnimation(
|
|
||||||
parent: _drawerAnimationController,
|
|
||||||
curve: Curves.easeInOut,
|
|
||||||
));
|
|
||||||
|
|
||||||
AccountStatus? _accountStatus;
|
|
||||||
|
|
||||||
Future<void> _getStatus() async {
|
|
||||||
final StatusProvider provider = Get.find();
|
|
||||||
|
|
||||||
final resp = await provider.getCurrentStatus();
|
|
||||||
final status = AccountStatus.fromJson(resp.body);
|
|
||||||
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {
|
|
||||||
_accountStatus = status;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Color get _unFocusColor =>
|
|
||||||
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
|
||||||
|
|
||||||
Widget _buildUserInfo() {
|
|
||||||
return Obx(() {
|
|
||||||
final AuthProvider auth = Get.find();
|
|
||||||
if (auth.isAuthorized.isFalse || auth.userProfile.value == null) {
|
|
||||||
if (_isCollapsed) {
|
|
||||||
return InkWell(
|
|
||||||
child: const Icon(Icons.account_circle).paddingSymmetric(
|
|
||||||
horizontal: 28,
|
|
||||||
vertical: 20,
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
AppRouter.instance.goNamed('account');
|
|
||||||
_closeDrawer();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ListTile(
|
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 28),
|
|
||||||
leading: const Icon(Icons.account_circle),
|
|
||||||
title: !_isCollapsed ? Text('guest'.tr) : null,
|
|
||||||
subtitle: !_isCollapsed ? Text('unsignedIn'.tr) : null,
|
|
||||||
onTap: () {
|
|
||||||
AppRouter.instance.goNamed('account');
|
|
||||||
_closeDrawer();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final leading = Obx(() {
|
|
||||||
final statusBadgeColor = _accountStatus != null
|
|
||||||
? StatusProvider.determineStatus(_accountStatus!).$2
|
|
||||||
: Colors.grey;
|
|
||||||
|
|
||||||
final RelationshipProvider relations = Get.find();
|
|
||||||
final accountNotifications = relations.friendRequestCount.value;
|
|
||||||
|
|
||||||
return badges.Badge(
|
|
||||||
badgeContent: Text(
|
|
||||||
accountNotifications.toString(),
|
|
||||||
style: const TextStyle(color: Colors.white),
|
|
||||||
),
|
|
||||||
showBadge: accountNotifications > 0,
|
|
||||||
position: badges.BadgePosition.topEnd(
|
|
||||||
top: -10,
|
|
||||||
end: -6,
|
|
||||||
),
|
|
||||||
child: badges.Badge(
|
|
||||||
showBadge: _accountStatus != null,
|
|
||||||
badgeStyle: badges.BadgeStyle(badgeColor: statusBadgeColor),
|
|
||||||
position: badges.BadgePosition.bottomEnd(
|
|
||||||
bottom: 0,
|
|
||||||
end: -2,
|
|
||||||
),
|
|
||||||
child: AccountAvatar(
|
|
||||||
content: auth.userProfile.value!['avatar'],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return InkWell(
|
|
||||||
child: !_isCollapsed
|
|
||||||
? Row(
|
|
||||||
children: [
|
|
||||||
leading,
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
auth.userProfile.value!['nick'],
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.fade,
|
|
||||||
style: Theme.of(context).textTheme.bodyLarge,
|
|
||||||
).paddingOnly(left: 16),
|
|
||||||
Builder(
|
|
||||||
builder: (context) {
|
|
||||||
if (_accountStatus == null) {
|
|
||||||
return Text(
|
|
||||||
'loading'.tr,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.fade,
|
|
||||||
style: TextStyle(
|
|
||||||
color: _unFocusColor,
|
|
||||||
),
|
|
||||||
).paddingOnly(left: 16);
|
|
||||||
}
|
|
||||||
final info = StatusProvider.determineStatus(
|
|
||||||
_accountStatus!,
|
|
||||||
);
|
|
||||||
return Text(
|
|
||||||
info.$3,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.fade,
|
|
||||||
style: TextStyle(
|
|
||||||
color: _unFocusColor,
|
|
||||||
),
|
|
||||||
).paddingOnly(left: 16);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).paddingSymmetric(horizontal: 20, vertical: 16)
|
|
||||||
: leading.paddingSymmetric(horizontal: 20, vertical: 16),
|
|
||||||
onTap: () {
|
|
||||||
AppRouter.instance.goNamed('account');
|
|
||||||
_closeDrawer();
|
|
||||||
},
|
|
||||||
onLongPress: () {
|
|
||||||
showModalBottomSheet(
|
|
||||||
useRootNavigator: true,
|
|
||||||
context: context,
|
|
||||||
builder: (context) => AccountStatusAction(
|
|
||||||
currentStatus: _accountStatus!.status,
|
|
||||||
),
|
|
||||||
).then((val) {
|
|
||||||
if (val == true) _getStatus();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _expandDrawer() {
|
|
||||||
_drawerAnimationController.animateTo(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _collapseDrawer() {
|
|
||||||
_drawerAnimationController.animateTo(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _closeDrawer() {
|
|
||||||
_autoResize();
|
|
||||||
rootScaffoldKey.currentState!.closeDrawer();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _autoResize() {
|
|
||||||
if (AppTheme.isExtraLargeScreen(context)) {
|
|
||||||
_expandDrawer();
|
|
||||||
} else if (AppTheme.isLargeScreen(context)) {
|
|
||||||
_collapseDrawer();
|
|
||||||
} else {
|
|
||||||
_drawerAnimationController.value = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
final AuthProvider auth = Get.find();
|
|
||||||
if (auth.isAuthorized.value) _getStatus();
|
|
||||||
Future.delayed(Duration.zero, () => _autoResize());
|
|
||||||
_drawerAnimationController.addListener(() {
|
|
||||||
if (_drawerAnimation.value > 180 && _isCollapsed) {
|
|
||||||
setState(() => _isCollapsed = false);
|
|
||||||
} else if (_drawerAnimation.value < 180 && !_isCollapsed) {
|
|
||||||
setState(() => _isCollapsed = true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_drawerAnimationController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return AnimatedBuilder(
|
|
||||||
animation: _drawerAnimation,
|
|
||||||
builder: (context, child) {
|
|
||||||
return Drawer(
|
|
||||||
width: _drawerAnimation.value,
|
|
||||||
backgroundColor:
|
|
||||||
AppTheme.isLargeScreen(context) ? Colors.transparent : null,
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: SafeArea(
|
|
||||||
bottom: false,
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
_buildUserInfo().paddingSymmetric(vertical: 8),
|
|
||||||
const Divider(thickness: 0.3, height: 1),
|
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: Wrap(
|
|
||||||
runSpacing: 8,
|
|
||||||
spacing: 8,
|
|
||||||
alignment: WrapAlignment.spaceAround,
|
|
||||||
children: AppNavigation.destinations
|
|
||||||
.map(
|
|
||||||
(e) => Tooltip(
|
|
||||||
message: e.label,
|
|
||||||
child: InkWell(
|
|
||||||
borderRadius:
|
|
||||||
const BorderRadius.all(Radius.circular(8)),
|
|
||||||
child: Icon(
|
|
||||||
e.icon,
|
|
||||||
size: 22,
|
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
|
||||||
).paddingAll(16),
|
|
||||||
onTap: () {
|
|
||||||
AppRouter.instance.goNamed(e.page);
|
|
||||||
_closeDrawer();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
).paddingSymmetric(vertical: 8, horizontal: 12),
|
|
||||||
),
|
|
||||||
const Divider(thickness: 0.3, height: 1),
|
|
||||||
Expanded(
|
|
||||||
child: Material(
|
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: AppNavigationRegion(
|
|
||||||
isCollapsed: _isCollapsed,
|
|
||||||
onSelected: () {
|
|
||||||
_closeDrawer();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Divider(thickness: 0.3, height: 1),
|
|
||||||
Column(
|
|
||||||
children: [
|
|
||||||
if (_isCollapsed)
|
|
||||||
Tooltip(
|
|
||||||
message: 'expand'.tr,
|
|
||||||
child: InkWell(
|
|
||||||
child: const Icon(Icons.chevron_right, size: 20)
|
|
||||||
.paddingSymmetric(
|
|
||||||
horizontal: 28,
|
|
||||||
vertical: 10,
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
_expandDrawer();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
ListTile(
|
|
||||||
minTileHeight: 0,
|
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 20,
|
|
||||||
),
|
|
||||||
leading:
|
|
||||||
const Icon(Icons.chevron_left, size: 20).paddingAll(2),
|
|
||||||
title: Text('collapse'.tr),
|
|
||||||
onTap: () {
|
|
||||||
_collapseDrawer();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).paddingOnly(
|
|
||||||
top: 8,
|
|
||||||
bottom: math.max(8, MediaQuery.of(context).padding.bottom),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
65
lib/widgets/navigation/app_navigation_rail.dart
Normal file
65
lib/widgets/navigation/app_navigation_rail.dart
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:solian/router.dart';
|
||||||
|
import 'package:solian/widgets/navigation/app_navigation.dart';
|
||||||
|
|
||||||
|
class AppNavigationRail extends StatefulWidget {
|
||||||
|
final int initialIndex;
|
||||||
|
|
||||||
|
const AppNavigationRail({super.key, this.initialIndex = 0});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AppNavigationRail> createState() => _AppNavigationRailState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AppNavigationRailState extends State<AppNavigationRail> {
|
||||||
|
int? _currentIndex = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
if (widget.initialIndex >= 0) {
|
||||||
|
_currentIndex = widget.initialIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return NavigationRail(
|
||||||
|
selectedIndex: _currentIndex,
|
||||||
|
labelType: NavigationRailLabelType.selected,
|
||||||
|
groupAlignment: -1,
|
||||||
|
destinations: AppNavigation.destinations
|
||||||
|
.sublist(0, AppNavigation.destinations.length - 1)
|
||||||
|
.map(
|
||||||
|
(x) => NavigationRailDestination(
|
||||||
|
icon: x.icon,
|
||||||
|
label: Text(x.label),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
trailing: Expanded(
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: IconButton(
|
||||||
|
icon: AppNavigation.destinations.last.icon,
|
||||||
|
tooltip: AppNavigation.destinations.last.label,
|
||||||
|
onPressed: () {
|
||||||
|
setState(() => _currentIndex = null);
|
||||||
|
AppRouter.instance.goNamed(AppNavigation.destinations.last.page);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onDestinationSelected: (idx) {
|
||||||
|
setState(() => _currentIndex = idx);
|
||||||
|
AppRouter.instance.goNamed(AppNavigation.destinations[idx].page);
|
||||||
|
},
|
||||||
|
).paddingOnly(
|
||||||
|
top: max(16, MediaQuery.of(context).padding.top),
|
||||||
|
bottom: max(16, MediaQuery.of(context).padding.bottom),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,230 +0,0 @@
|
|||||||
import 'package:animations/animations.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:solian/models/realm.dart';
|
|
||||||
import 'package:solian/providers/auth.dart';
|
|
||||||
import 'package:solian/providers/content/channel.dart';
|
|
||||||
import 'package:solian/providers/content/realm.dart';
|
|
||||||
import 'package:solian/providers/navigation.dart';
|
|
||||||
import 'package:solian/services.dart';
|
|
||||||
import 'package:solian/widgets/account/account_avatar.dart';
|
|
||||||
import 'package:solian/widgets/auto_cache_image.dart';
|
|
||||||
import 'package:solian/widgets/channel/channel_list.dart';
|
|
||||||
|
|
||||||
class AppNavigationRegion extends StatefulWidget {
|
|
||||||
final bool isCollapsed;
|
|
||||||
final Function onSelected;
|
|
||||||
|
|
||||||
const AppNavigationRegion({
|
|
||||||
super.key,
|
|
||||||
this.isCollapsed = false,
|
|
||||||
required this.onSelected,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<AppNavigationRegion> createState() => _AppNavigationRegionState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AppNavigationRegionState extends State<AppNavigationRegion> {
|
|
||||||
bool _isTryingExit = false;
|
|
||||||
|
|
||||||
void _focusRealm(Realm item) {
|
|
||||||
setState(
|
|
||||||
() => Get.find<NavigationStateProvider>().focusedRealm.value = item,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _unFocusRealm() {
|
|
||||||
setState(
|
|
||||||
() => Get.find<NavigationStateProvider>().focusedRealm.value = null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildRealmFocusAvatar() {
|
|
||||||
final focusedRealm = Get.find<NavigationStateProvider>().focusedRealm.value;
|
|
||||||
return GestureDetector(
|
|
||||||
child: MouseRegion(
|
|
||||||
child: AnimatedSwitcher(
|
|
||||||
switchInCurve: Curves.fastOutSlowIn,
|
|
||||||
switchOutCurve: Curves.fastOutSlowIn,
|
|
||||||
duration: const Duration(milliseconds: 300),
|
|
||||||
transitionBuilder: (child, animation) {
|
|
||||||
return ScaleTransition(
|
|
||||||
scale: animation,
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: _isTryingExit
|
|
||||||
? CircleAvatar(
|
|
||||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
|
||||||
child: const Icon(
|
|
||||||
Icons.arrow_back,
|
|
||||||
color: Colors.white,
|
|
||||||
size: 16,
|
|
||||||
),
|
|
||||||
).paddingSymmetric(
|
|
||||||
vertical: 8,
|
|
||||||
)
|
|
||||||
: _buildEntryAvatar(focusedRealm!),
|
|
||||||
),
|
|
||||||
onEnter: (_) => setState(() => _isTryingExit = true),
|
|
||||||
onExit: (_) => setState(() => _isTryingExit = false),
|
|
||||||
),
|
|
||||||
onTap: () => _unFocusRealm(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildEntryAvatar(Realm item) {
|
|
||||||
return Hero(
|
|
||||||
tag: Key('region-realm-avatar-${item.id}'),
|
|
||||||
child: (item.avatar?.isNotEmpty ?? false)
|
|
||||||
? AccountAvatar(content: item.avatar)
|
|
||||||
: CircleAvatar(
|
|
||||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
|
||||||
child: const Icon(
|
|
||||||
Icons.workspaces,
|
|
||||||
color: Colors.white,
|
|
||||||
size: 16,
|
|
||||||
),
|
|
||||||
).paddingSymmetric(
|
|
||||||
vertical: 8,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildEntry(BuildContext context, Realm item) {
|
|
||||||
const padding = EdgeInsets.symmetric(horizontal: 20, vertical: 8);
|
|
||||||
|
|
||||||
if (widget.isCollapsed) {
|
|
||||||
return InkWell(
|
|
||||||
child: _buildEntryAvatar(item).paddingSymmetric(vertical: 8),
|
|
||||||
onTap: () => _focusRealm(item),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ListTile(
|
|
||||||
minTileHeight: 0,
|
|
||||||
leading: _buildEntryAvatar(item),
|
|
||||||
contentPadding: padding,
|
|
||||||
title: Text(item.name),
|
|
||||||
subtitle: Text(
|
|
||||||
item.description,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
onTap: () => _focusRealm(item),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final RealmProvider realms = Get.find();
|
|
||||||
final ChannelProvider channels = Get.find();
|
|
||||||
final AuthProvider auth = Get.find();
|
|
||||||
final NavigationStateProvider navState = Get.find();
|
|
||||||
|
|
||||||
return Obx(
|
|
||||||
() => PageTransitionSwitcher(
|
|
||||||
transitionBuilder: (child, animation, secondaryAnimation) {
|
|
||||||
return SharedAxisTransition(
|
|
||||||
animation: animation,
|
|
||||||
secondaryAnimation: secondaryAnimation,
|
|
||||||
transitionType: SharedAxisTransitionType.horizontal,
|
|
||||||
child: Material(
|
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: navState.focusedRealm.value == null
|
|
||||||
? widget.isCollapsed
|
|
||||||
? CustomScrollView(
|
|
||||||
slivers: [
|
|
||||||
const SliverPadding(padding: EdgeInsets.only(top: 16)),
|
|
||||||
SliverList.builder(
|
|
||||||
itemCount: realms.availableRealms.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final element = realms.availableRealms[index];
|
|
||||||
return Tooltip(
|
|
||||||
message: element.name,
|
|
||||||
child: _buildEntry(context, element),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
: CustomScrollView(
|
|
||||||
slivers: [
|
|
||||||
SliverList.builder(
|
|
||||||
itemCount: realms.availableRealms.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final element = realms.availableRealms[index];
|
|
||||||
return _buildEntry(context, element);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
: Column(
|
|
||||||
children: [
|
|
||||||
if (!widget.isCollapsed &&
|
|
||||||
(navState.focusedRealm.value!.banner?.isNotEmpty ??
|
|
||||||
false))
|
|
||||||
AspectRatio(
|
|
||||||
aspectRatio: 16 / 7,
|
|
||||||
child: AutoCacheImage(
|
|
||||||
ServiceFinder.buildUrl(
|
|
||||||
'uc',
|
|
||||||
'/attachments/${navState.focusedRealm.value!.banner}',
|
|
||||||
),
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (widget.isCollapsed)
|
|
||||||
Tooltip(
|
|
||||||
message: navState.focusedRealm.value!.name,
|
|
||||||
child: _buildRealmFocusAvatar().paddingOnly(
|
|
||||||
top: 24,
|
|
||||||
bottom: 8,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
ListTile(
|
|
||||||
minTileHeight: 0,
|
|
||||||
tileColor:
|
|
||||||
Theme.of(context).colorScheme.surfaceContainerLow,
|
|
||||||
leading: _buildRealmFocusAvatar(),
|
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 20, vertical: 8),
|
|
||||||
title: Text(navState.focusedRealm.value!.name),
|
|
||||||
subtitle: Text(
|
|
||||||
navState.focusedRealm.value!.description,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Obx(
|
|
||||||
() => ChannelListWidget(
|
|
||||||
useReplace: true,
|
|
||||||
channels: channels.availableChannels
|
|
||||||
.where((x) =>
|
|
||||||
x.realm?.id == navState.focusedRealm.value?.id)
|
|
||||||
.toList(),
|
|
||||||
isCollapsed: widget.isCollapsed,
|
|
||||||
selfId: auth.userProfile.value!['id'],
|
|
||||||
noCategory: true,
|
|
||||||
onSelected: (_) => widget.onSelected(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
92
lib/widgets/navigation/realm_switcher.dart
Normal file
92
lib/widgets/navigation/realm_switcher.dart
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:solian/models/realm.dart';
|
||||||
|
import 'package:solian/providers/content/realm.dart';
|
||||||
|
import 'package:solian/providers/navigation.dart';
|
||||||
|
import 'package:solian/widgets/account/account_avatar.dart';
|
||||||
|
|
||||||
|
class RealmSwitcher extends StatelessWidget {
|
||||||
|
const RealmSwitcher({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final realms = Get.find<RealmProvider>();
|
||||||
|
final navState = Get.find<NavigationStateProvider>();
|
||||||
|
|
||||||
|
return Obx(() {
|
||||||
|
return DropdownButtonHideUnderline(
|
||||||
|
child: DropdownButton2<Realm?>(
|
||||||
|
iconStyleData: const IconStyleData(iconSize: 0),
|
||||||
|
isExpanded: true,
|
||||||
|
hint: Text(
|
||||||
|
'Realm Region',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Theme.of(context).hintColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
items: [null, ...realms.availableRealms]
|
||||||
|
.map((Realm? item) => DropdownMenuItem<Realm?>(
|
||||||
|
value: item,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
if (item != null)
|
||||||
|
AccountAvatar(
|
||||||
|
content: item.avatar,
|
||||||
|
radius: 14,
|
||||||
|
fallbackWidget: const Icon(
|
||||||
|
Icons.workspaces,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
CircleAvatar(
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).colorScheme.primary,
|
||||||
|
radius: 14,
|
||||||
|
child: const Icon(
|
||||||
|
Icons.public,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
item?.name ?? 'global'.tr,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
value: navState.focusedRealm.value,
|
||||||
|
onChanged: (Realm? value) {
|
||||||
|
navState.focusedRealm.value = value;
|
||||||
|
},
|
||||||
|
buttonStyleData: ButtonStyleData(
|
||||||
|
height: 48,
|
||||||
|
width: max(200, MediaQuery.of(context).size.width * 0.4),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(16)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
menuItemStyleData: const MenuItemStyleData(
|
||||||
|
height: 48,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -29,7 +29,7 @@ class _PostEditorThumbnailDialogState extends State<PostEditorThumbnailDialog> {
|
|||||||
_attachmentController.text = value.toString();
|
_attachmentController.text = value.toString();
|
||||||
});
|
});
|
||||||
|
|
||||||
widget.controller.thumbnail.value = value;
|
widget.controller.thumbnail.value = value.isEmpty ? null : value;
|
||||||
},
|
},
|
||||||
initialAttachments: const [],
|
initialAttachments: const [],
|
||||||
onRemove: (_) {},
|
onRemove: (_) {},
|
||||||
@ -91,7 +91,8 @@ class _PostEditorThumbnailDialogState extends State<PostEditorThumbnailDialog> {
|
|||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
widget.controller.thumbnail.value = _attachmentController.text;
|
final text = _attachmentController.text;
|
||||||
|
widget.controller.thumbnail.value = text.isEmpty ? null : text;
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
child: Text('confirm'.tr),
|
child: Text('confirm'.tr),
|
||||||
|
@ -12,6 +12,7 @@ import 'package:solian/platform.dart';
|
|||||||
import 'package:solian/providers/auth.dart';
|
import 'package:solian/providers/auth.dart';
|
||||||
import 'package:solian/router.dart';
|
import 'package:solian/router.dart';
|
||||||
import 'package:solian/screens/posts/post_editor.dart';
|
import 'package:solian/screens/posts/post_editor.dart';
|
||||||
|
import 'package:solian/widgets/reports/abuse_report.dart';
|
||||||
|
|
||||||
class PostAction extends StatefulWidget {
|
class PostAction extends StatefulWidget {
|
||||||
final Post item;
|
final Post item;
|
||||||
@ -149,6 +150,23 @@ class _PostActionState extends State<PostAction> {
|
|||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
ListTile(
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
leading: const Icon(Icons.flag),
|
||||||
|
title: Text('report'.tr),
|
||||||
|
onTap: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AbuseReportDialog(
|
||||||
|
resourceId: 'post:${widget.item.id}',
|
||||||
|
),
|
||||||
|
).then((status) {
|
||||||
|
if (status == true) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
if (!widget.noReact)
|
if (!widget.noReact)
|
||||||
ListTile(
|
ListTile(
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
@ -18,6 +18,7 @@ import 'package:solian/widgets/link_expansion.dart';
|
|||||||
import 'package:solian/widgets/markdown_text_content.dart';
|
import 'package:solian/widgets/markdown_text_content.dart';
|
||||||
import 'package:solian/widgets/posts/post_tags.dart';
|
import 'package:solian/widgets/posts/post_tags.dart';
|
||||||
import 'package:solian/widgets/posts/post_quick_action.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:solian/widgets/sized_container.dart';
|
||||||
import 'package:timeago/timeago.dart' show format;
|
import 'package:timeago/timeago.dart' show format;
|
||||||
|
|
||||||
@ -69,360 +70,6 @@ class _PostItemState extends State<PostItem> {
|
|||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDate() {
|
|
||||||
if (widget.isFullDate) {
|
|
||||||
return Text(DateFormat('y/M/d HH:mm')
|
|
||||||
.format(item.publishedAt?.toLocal() ?? DateTime.now()));
|
|
||||||
} else {
|
|
||||||
return Text(
|
|
||||||
format(
|
|
||||||
item.publishedAt?.toLocal() ?? DateTime.now(),
|
|
||||||
locale: 'en_short',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildThumbnail() {
|
|
||||||
if (widget.item.body['thumbnail'] == null) return const SizedBox.shrink();
|
|
||||||
final border = BorderSide(
|
|
||||||
color: Theme.of(context).dividerColor,
|
|
||||||
width: 0.3,
|
|
||||||
);
|
|
||||||
return Container(
|
|
||||||
decoration: BoxDecoration(border: Border(top: border, bottom: border)),
|
|
||||||
child: AspectRatio(
|
|
||||||
aspectRatio: 16 / 9,
|
|
||||||
child: AttachmentSelfContainedEntry(
|
|
||||||
rid: widget.item.body['thumbnail'],
|
|
||||||
parentId: 'p${item.id}-thumbnail',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildHeader() {
|
|
||||||
return Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
if (widget.isCompact)
|
|
||||||
AccountAvatar(
|
|
||||||
content: item.author.avatar,
|
|
||||||
radius: 10,
|
|
||||||
).paddingOnly(left: 2, top: 1),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
item.author.nick,
|
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
_buildDate().paddingOnly(left: 4),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if (item.body['title'] != null)
|
|
||||||
Text(
|
|
||||||
item.body['title'],
|
|
||||||
style: Theme.of(context)
|
|
||||||
.textTheme
|
|
||||||
.bodyMedium!
|
|
||||||
.copyWith(fontSize: 15),
|
|
||||||
),
|
|
||||||
if (item.body['description'] != null)
|
|
||||||
Text(
|
|
||||||
item.body['description'],
|
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).paddingOnly(left: widget.isCompact ? 6 : 12),
|
|
||||||
),
|
|
||||||
if (widget.item.type == 'article')
|
|
||||||
Badge(
|
|
||||||
label: Text('article'.tr),
|
|
||||||
).paddingOnly(top: 3),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildHeaderDivider() {
|
|
||||||
if (item.body['description'] != null || item.body['title'] != null) {
|
|
||||||
return const Divider(thickness: 0.3, height: 1).paddingSymmetric(
|
|
||||||
vertical: 8,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildFooter() {
|
|
||||||
List<String> labels = List.empty(growable: true);
|
|
||||||
if (widget.item.editedAt != null) {
|
|
||||||
labels.add('postEdited'.trParams({
|
|
||||||
'date': DateFormat('yy/M/d HH:mm').format(item.editedAt!.toLocal()),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
if (widget.item.realm != null) {
|
|
||||||
labels.add('postInRealm'.trParams({
|
|
||||||
'realm': widget.item.realm!.alias,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Widget> widgets = List.empty(growable: true);
|
|
||||||
|
|
||||||
if (widget.item.tags?.isNotEmpty ?? false) {
|
|
||||||
widgets.add(PostTagsList(tags: widget.item.tags!));
|
|
||||||
}
|
|
||||||
if (labels.isNotEmpty) {
|
|
||||||
widgets.add(Text(
|
|
||||||
labels.join(' · '),
|
|
||||||
textAlign: TextAlign.left,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: _unFocusColor,
|
|
||||||
),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if (widget.item.pinnedAt != null) {
|
|
||||||
widgets.add(Text(
|
|
||||||
'postPinned'.tr,
|
|
||||||
style: TextStyle(fontSize: 12, color: _unFocusColor),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (widgets.isEmpty) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
} else {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: widgets,
|
|
||||||
).paddingOnly(top: 4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildReply(BuildContext context) {
|
|
||||||
return OpenContainer(
|
|
||||||
tappable: widget.isClickable || widget.isOverrideEmbedClickable,
|
|
||||||
closedBuilder: (_, openContainer) => Column(
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
FaIcon(
|
|
||||||
FontAwesomeIcons.reply,
|
|
||||||
size: 16,
|
|
||||||
color: _unFocusColor,
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
'postRepliedNotify'.trParams(
|
|
||||||
{'username': '@${widget.item.replyTo!.author.name}'},
|
|
||||||
),
|
|
||||||
style: TextStyle(color: _unFocusColor),
|
|
||||||
).paddingOnly(left: 6),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).paddingOnly(left: 12),
|
|
||||||
Card(
|
|
||||||
elevation: 1,
|
|
||||||
child: PostItem(
|
|
||||||
item: widget.item.replyTo!,
|
|
||||||
isCompact: true,
|
|
||||||
attachmentParent: widget.item.id.toString(),
|
|
||||||
).paddingSymmetric(vertical: 8),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
openBuilder: (_, __) => TitleShell(
|
|
||||||
title: 'postDetail'.tr,
|
|
||||||
child: PostDetailScreen(
|
|
||||||
id: widget.item.replyTo!.id.toString(),
|
|
||||||
post: widget.item.replyTo!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
closedElevation: 0,
|
|
||||||
openElevation: 0,
|
|
||||||
closedColor:
|
|
||||||
widget.backgroundColor ?? Theme.of(context).colorScheme.surface,
|
|
||||||
openColor: Theme.of(context).colorScheme.surface,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildRepost(BuildContext context) {
|
|
||||||
return OpenContainer(
|
|
||||||
tappable: widget.isClickable || widget.isOverrideEmbedClickable,
|
|
||||||
closedBuilder: (_, openContainer) => Column(
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
FaIcon(
|
|
||||||
FontAwesomeIcons.retweet,
|
|
||||||
size: 16,
|
|
||||||
color: _unFocusColor,
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
'postRepostedNotify'.trParams(
|
|
||||||
{'username': '@${widget.item.repostTo!.author.name}'},
|
|
||||||
),
|
|
||||||
style: TextStyle(color: _unFocusColor),
|
|
||||||
).paddingOnly(left: 6),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).paddingOnly(left: 12),
|
|
||||||
Card(
|
|
||||||
elevation: 1,
|
|
||||||
child: PostItem(
|
|
||||||
item: widget.item.repostTo!,
|
|
||||||
isCompact: true,
|
|
||||||
attachmentParent: widget.item.id.toString(),
|
|
||||||
).paddingSymmetric(vertical: 8),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
openBuilder: (_, __) => TitleShell(
|
|
||||||
title: 'postDetail'.tr,
|
|
||||||
child: PostDetailScreen(
|
|
||||||
id: widget.item.repostTo!.id.toString(),
|
|
||||||
post: widget.item.repostTo!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
closedElevation: 0,
|
|
||||||
openElevation: 0,
|
|
||||||
closedColor:
|
|
||||||
widget.backgroundColor ?? Theme.of(context).colorScheme.surface,
|
|
||||||
openColor: Theme.of(context).colorScheme.surface,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildAttachments() {
|
|
||||||
final List<String> attachments = item.body['attachments'] is List
|
|
||||||
? List.from(item.body['attachments']?.whereType<String>())
|
|
||||||
: List.empty();
|
|
||||||
|
|
||||||
if (attachments.length > 3) {
|
|
||||||
return AttachmentList(
|
|
||||||
parentId: widget.item.id.toString(),
|
|
||||||
attachmentsId: attachments,
|
|
||||||
autoload: false,
|
|
||||||
isGrid: true,
|
|
||||||
).paddingOnly(left: 36, top: 4, bottom: 4);
|
|
||||||
} else if (attachments.length > 1 || AppTheme.isLargeScreen(context)) {
|
|
||||||
return AttachmentList(
|
|
||||||
parentId: widget.item.id.toString(),
|
|
||||||
attachmentsId: attachments,
|
|
||||||
autoload: false,
|
|
||||||
isColumn: true,
|
|
||||||
).paddingOnly(left: 60, right: 24);
|
|
||||||
} else {
|
|
||||||
return AttachmentList(
|
|
||||||
flatMaxHeight: MediaQuery.of(context).size.width,
|
|
||||||
parentId: widget.item.id.toString(),
|
|
||||||
attachmentsId: attachments,
|
|
||||||
autoload: false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildFeaturedReply() {
|
|
||||||
if ((widget.item.metric?.replyCount ?? 0) == 0) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
final List<String> attachments = item.body['attachments'] is List
|
|
||||||
? List.from(item.body['attachments']?.whereType<String>())
|
|
||||||
: List.empty();
|
|
||||||
final unFocusColor =
|
|
||||||
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
|
||||||
return FutureBuilder(
|
|
||||||
future: Get.find<PostProvider>().listPostFeaturedReply(
|
|
||||||
widget.item.id.toString(),
|
|
||||||
),
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
if (!snapshot.hasData || snapshot.data!.isEmpty) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
return Container(
|
|
||||||
constraints: const BoxConstraints(maxWidth: 480),
|
|
||||||
child: Card(
|
|
||||||
margin: EdgeInsets.zero,
|
|
||||||
child: Column(
|
|
||||||
children: snapshot.data!
|
|
||||||
.map(
|
|
||||||
(x) => Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
AccountAvatar(content: x.author.avatar, radius: 10),
|
|
||||||
const Gap(6),
|
|
||||||
Text(
|
|
||||||
x.author.nick,
|
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
const Gap(6),
|
|
||||||
Text(
|
|
||||||
format(
|
|
||||||
x.publishedAt?.toLocal() ?? DateTime.now(),
|
|
||||||
locale: 'en_short',
|
|
||||||
),
|
|
||||||
).paddingOnly(top: 0.5),
|
|
||||||
const Gap(8),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
MarkdownTextContent(
|
|
||||||
content: x.body['content'],
|
|
||||||
parentId: 'p${item.id}-featured-reply${x.id}',
|
|
||||||
),
|
|
||||||
if (x.body['attachments'] is List &&
|
|
||||||
x.body['attachments'].length > 0)
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.file_copy,
|
|
||||||
size: 15,
|
|
||||||
color: unFocusColor,
|
|
||||||
).paddingOnly(right: 5),
|
|
||||||
Text(
|
|
||||||
'attachmentHint'.trParams(
|
|
||||||
{
|
|
||||||
'count': x.body['attachments'].length
|
|
||||||
.toString()
|
|
||||||
},
|
|
||||||
),
|
|
||||||
style: TextStyle(color: unFocusColor),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).paddingSymmetric(horizontal: 12, vertical: 8),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.animate()
|
|
||||||
.fadeIn(
|
|
||||||
duration: 300.ms,
|
|
||||||
curve: Curves.easeIn,
|
|
||||||
)
|
|
||||||
.paddingOnly(
|
|
||||||
top: (attachments.length == 1 && !AppTheme.isLargeScreen(context))
|
|
||||||
? 10
|
|
||||||
: 6,
|
|
||||||
left:
|
|
||||||
(attachments.length == 1 && !AppTheme.isLargeScreen(context))
|
|
||||||
? 24
|
|
||||||
: 60,
|
|
||||||
right: 16,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
double _contentHeight = 0;
|
double _contentHeight = 0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -436,9 +83,15 @@ class _PostItemState extends State<PostItem> {
|
|||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_buildThumbnail().paddingOnly(bottom: 8),
|
_PostThumbnail(
|
||||||
_buildHeader().paddingSymmetric(horizontal: 12),
|
rid: item.body['thumbnail'],
|
||||||
_buildHeaderDivider().paddingSymmetric(horizontal: 12),
|
parentId: widget.item.id.toString(),
|
||||||
|
).paddingOnly(bottom: 8),
|
||||||
|
_PostHeaderWidget(
|
||||||
|
isCompact: widget.isCompact,
|
||||||
|
item: item,
|
||||||
|
).paddingSymmetric(horizontal: 12),
|
||||||
|
_PostHeaderDividerWidget(item: item).paddingSymmetric(horizontal: 12),
|
||||||
Stack(
|
Stack(
|
||||||
children: [
|
children: [
|
||||||
SizedContainer(
|
SizedContainer(
|
||||||
@ -448,10 +101,14 @@ class _PostItemState extends State<PostItem> {
|
|||||||
onChange: (size) {
|
onChange: (size) {
|
||||||
setState(() => _contentHeight = size.height);
|
setState(() => _contentHeight = size.height);
|
||||||
},
|
},
|
||||||
child: MarkdownTextContent(
|
child: SingleChildScrollView(
|
||||||
parentId: 'p${item.id}',
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
content: item.body['content'],
|
child: MarkdownTextContent(
|
||||||
isSelectable: widget.isContentSelectable,
|
parentId: 'p${item.id}',
|
||||||
|
content: item.body['content'],
|
||||||
|
isAutoWarp: item.type == 'story',
|
||||||
|
isSelectable: widget.isContentSelectable,
|
||||||
|
),
|
||||||
).paddingOnly(
|
).paddingOnly(
|
||||||
left: 16,
|
left: 16,
|
||||||
right: 12,
|
right: 12,
|
||||||
@ -489,7 +146,7 @@ class _PostItemState extends State<PostItem> {
|
|||||||
right: 8,
|
right: 8,
|
||||||
top: 4,
|
top: 4,
|
||||||
),
|
),
|
||||||
_buildFooter().paddingOnly(left: 16),
|
_PostFooterWidget(item: item).paddingOnly(left: 16),
|
||||||
if (attachments.isNotEmpty)
|
if (attachments.isNotEmpty)
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
@ -515,7 +172,10 @@ class _PostItemState extends State<PostItem> {
|
|||||||
closedBuilder: (_, openContainer) => Column(
|
closedBuilder: (_, openContainer) => Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_buildThumbnail().paddingOnly(bottom: 4),
|
_PostThumbnail(
|
||||||
|
rid: item.body['thumbnail'],
|
||||||
|
parentId: widget.item.id.toString(),
|
||||||
|
).paddingOnly(bottom: 4),
|
||||||
Row(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@ -537,8 +197,11 @@ class _PostItemState extends State<PostItem> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_buildHeader(),
|
_PostHeaderWidget(
|
||||||
_buildHeaderDivider(),
|
isCompact: widget.isCompact,
|
||||||
|
item: item,
|
||||||
|
),
|
||||||
|
_PostHeaderDividerWidget(item: item),
|
||||||
Stack(
|
Stack(
|
||||||
children: [
|
children: [
|
||||||
SizedContainer(
|
SizedContainer(
|
||||||
@ -549,13 +212,17 @@ class _PostItemState extends State<PostItem> {
|
|||||||
onChange: (size) {
|
onChange: (size) {
|
||||||
setState(() => _contentHeight = size.height);
|
setState(() => _contentHeight = size.height);
|
||||||
},
|
},
|
||||||
child: MarkdownTextContent(
|
child: SingleChildScrollView(
|
||||||
parentId: 'p${item.id}-embed',
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
content: item.body['content'],
|
child: MarkdownTextContent(
|
||||||
isSelectable: widget.isContentSelectable,
|
parentId: 'p${item.id}-embed',
|
||||||
isLargeText: item.type == 'article' &&
|
content: item.body['content'],
|
||||||
widget.isFullContent,
|
isAutoWarp: item.type == 'story',
|
||||||
).paddingOnly(left: 12, right: 8),
|
isSelectable: widget.isContentSelectable,
|
||||||
|
isLargeText: item.type == 'article' &&
|
||||||
|
widget.isFullContent,
|
||||||
|
).paddingOnly(left: 12, right: 8),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (_contentHeight >= 320 && !widget.isFullContent)
|
if (_contentHeight >= 320 && !widget.isFullContent)
|
||||||
@ -569,10 +236,14 @@ class _PostItemState extends State<PostItem> {
|
|||||||
begin: Alignment.bottomCenter,
|
begin: Alignment.bottomCenter,
|
||||||
end: Alignment.topCenter,
|
end: Alignment.topCenter,
|
||||||
colors: [
|
colors: [
|
||||||
Theme.of(context).colorScheme.surface,
|
(widget.backgroundColor ??
|
||||||
Theme.of(context)
|
Theme.of(context)
|
||||||
.colorScheme
|
.colorScheme
|
||||||
.surface
|
.surface),
|
||||||
|
(widget.backgroundColor ??
|
||||||
|
Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.surface)
|
||||||
.withOpacity(0),
|
.withOpacity(0),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -586,15 +257,33 @@ class _PostItemState extends State<PostItem> {
|
|||||||
Container(
|
Container(
|
||||||
constraints: const BoxConstraints(maxWidth: 480),
|
constraints: const BoxConstraints(maxWidth: 480),
|
||||||
padding: const EdgeInsets.only(top: 4),
|
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)
|
if (widget.item.repostTo != null && widget.isShowEmbed)
|
||||||
Container(
|
Container(
|
||||||
constraints: const BoxConstraints(maxWidth: 480),
|
constraints: const BoxConstraints(maxWidth: 480),
|
||||||
padding: const EdgeInsets.only(top: 4),
|
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'])
|
LinkExpansion(content: item.body['content'])
|
||||||
.paddingOnly(top: 4),
|
.paddingOnly(top: 4),
|
||||||
],
|
],
|
||||||
@ -610,8 +299,8 @@ class _PostItemState extends State<PostItem> {
|
|||||||
right: 16,
|
right: 16,
|
||||||
left: 16,
|
left: 16,
|
||||||
),
|
),
|
||||||
_buildAttachments(),
|
_PostAttachmentWidget(item: item),
|
||||||
if (widget.showFeaturedReply) _buildFeaturedReply(),
|
if (widget.showFeaturedReply) _PostFeaturedReplyWidget(item: item),
|
||||||
if (widget.isShowReply || widget.isReactable)
|
if (widget.isShowReply || widget.isReactable)
|
||||||
PostQuickAction(
|
PostQuickAction(
|
||||||
isShowReply: widget.isShowReply,
|
isShowReply: widget.isShowReply,
|
||||||
@ -654,6 +343,400 @@ class _PostItemState extends State<PostItem> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _PostFeaturedReplyWidget extends StatelessWidget {
|
||||||
|
final Post item;
|
||||||
|
|
||||||
|
const _PostFeaturedReplyWidget({required this.item});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final isLargeScreen = AppTheme.isLargeScreen(context);
|
||||||
|
final unFocusColor =
|
||||||
|
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
||||||
|
|
||||||
|
if ((item.metric?.replyCount ?? 0) == 0) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<String> attachments = item.body['attachments'] is List
|
||||||
|
? List.from(item.body['attachments']?.whereType<String>())
|
||||||
|
: List.empty();
|
||||||
|
|
||||||
|
return FutureBuilder(
|
||||||
|
future:
|
||||||
|
Get.find<PostProvider>().listPostFeaturedReply(item.id.toString()),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (!snapshot.hasData || snapshot.data!.isEmpty) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 480),
|
||||||
|
child: Card(
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
|
child: Column(
|
||||||
|
children: snapshot.data!
|
||||||
|
.map(
|
||||||
|
(reply) => ClipRRect(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
child: OpenContainer(
|
||||||
|
closedBuilder: (_, openContainer) => Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
AccountAvatar(
|
||||||
|
content: reply.author.avatar,
|
||||||
|
radius: 10,
|
||||||
|
),
|
||||||
|
const Gap(6),
|
||||||
|
Text(
|
||||||
|
reply.author.nick,
|
||||||
|
style:
|
||||||
|
const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const Gap(6),
|
||||||
|
Text(
|
||||||
|
format(
|
||||||
|
reply.publishedAt?.toLocal() ?? DateTime.now(),
|
||||||
|
locale: 'en_short',
|
||||||
|
),
|
||||||
|
).paddingOnly(top: 0.5),
|
||||||
|
const Gap(8),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
MarkdownTextContent(
|
||||||
|
isAutoWarp: reply.type == 'story',
|
||||||
|
content: reply.body['content'],
|
||||||
|
parentId:
|
||||||
|
'p${item.id}-featured-reply${reply.id}',
|
||||||
|
),
|
||||||
|
if (reply.body['attachments'] is List &&
|
||||||
|
reply.body['attachments'].isNotEmpty)
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.file_copy,
|
||||||
|
size: 15,
|
||||||
|
color: unFocusColor,
|
||||||
|
).paddingOnly(right: 5),
|
||||||
|
Text(
|
||||||
|
'attachmentHint'.trParams(
|
||||||
|
{
|
||||||
|
'count': reply
|
||||||
|
.body['attachments'].length
|
||||||
|
.toString(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
style: TextStyle(color: unFocusColor),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).paddingSymmetric(horizontal: 12, vertical: 8),
|
||||||
|
openBuilder: (_, __) => TitleShell(
|
||||||
|
title: 'postDetail'.tr,
|
||||||
|
child: PostDetailScreen(
|
||||||
|
id: reply.id.toString(),
|
||||||
|
post: reply,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
closedElevation: 0,
|
||||||
|
openElevation: 0,
|
||||||
|
closedColor:
|
||||||
|
Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
openColor: Theme.of(context).colorScheme.surface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.animate()
|
||||||
|
.fadeIn(
|
||||||
|
duration: 300.ms,
|
||||||
|
curve: Curves.easeIn,
|
||||||
|
)
|
||||||
|
.paddingOnly(
|
||||||
|
top: (attachments.length == 1 && !isLargeScreen) ? 10 : 6,
|
||||||
|
left: (attachments.length == 1 && !isLargeScreen) ? 24 : 60,
|
||||||
|
right: 16,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PostAttachmentWidget extends StatelessWidget {
|
||||||
|
final Post item;
|
||||||
|
|
||||||
|
const _PostAttachmentWidget({required this.item});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final isLargeScreen = AppTheme.isLargeScreen(context);
|
||||||
|
|
||||||
|
final List<String> attachments = item.body['attachments'] is List
|
||||||
|
? List.from(item.body['attachments']?.whereType<String>())
|
||||||
|
: List.empty();
|
||||||
|
|
||||||
|
if (attachments.length > 3) {
|
||||||
|
return AttachmentList(
|
||||||
|
parentId: item.id.toString(),
|
||||||
|
attachmentsId: attachments,
|
||||||
|
autoload: false,
|
||||||
|
isGrid: true,
|
||||||
|
).paddingOnly(left: 36, top: 4, bottom: 4);
|
||||||
|
} else if (attachments.length > 1 || isLargeScreen) {
|
||||||
|
return AttachmentList(
|
||||||
|
parentId: item.id.toString(),
|
||||||
|
attachmentsId: attachments,
|
||||||
|
autoload: false,
|
||||||
|
isColumn: true,
|
||||||
|
).paddingOnly(left: 60, right: 24, top: 4, bottom: 4);
|
||||||
|
} else {
|
||||||
|
return AttachmentList(
|
||||||
|
flatMaxHeight: MediaQuery.of(context).size.width,
|
||||||
|
parentId: item.id.toString(),
|
||||||
|
attachmentsId: attachments,
|
||||||
|
autoload: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PostEmbedWidget extends StatelessWidget {
|
||||||
|
final bool isClickable;
|
||||||
|
final bool isOverrideEmbedClickable;
|
||||||
|
final Post item;
|
||||||
|
final String username;
|
||||||
|
final String hintText;
|
||||||
|
final IconData icon;
|
||||||
|
final String id;
|
||||||
|
|
||||||
|
const _PostEmbedWidget({
|
||||||
|
required this.isClickable,
|
||||||
|
required this.isOverrideEmbedClickable,
|
||||||
|
required this.item,
|
||||||
|
required this.username,
|
||||||
|
required this.hintText,
|
||||||
|
required this.icon,
|
||||||
|
required this.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final unFocusColor =
|
||||||
|
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
||||||
|
|
||||||
|
return OpenContainer(
|
||||||
|
tappable: isClickable || isOverrideEmbedClickable,
|
||||||
|
closedBuilder: (_, openContainer) => Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
FaIcon(
|
||||||
|
icon,
|
||||||
|
size: 16,
|
||||||
|
color: unFocusColor,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
hintText.trParams(
|
||||||
|
{'username': '@$username'},
|
||||||
|
),
|
||||||
|
style: TextStyle(color: unFocusColor),
|
||||||
|
).paddingOnly(left: 6),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).paddingOnly(left: 12),
|
||||||
|
Card(
|
||||||
|
elevation: 1,
|
||||||
|
child: PostItem(
|
||||||
|
item: item,
|
||||||
|
isCompact: true,
|
||||||
|
attachmentParent: id,
|
||||||
|
).paddingSymmetric(vertical: 8),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
openBuilder: (_, __) => TitleShell(
|
||||||
|
title: 'postDetail'.tr,
|
||||||
|
child: PostDetailScreen(
|
||||||
|
id: id,
|
||||||
|
post: item,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
closedElevation: 0,
|
||||||
|
openElevation: 0,
|
||||||
|
closedColor: Theme.of(context).colorScheme.surface,
|
||||||
|
openColor: Theme.of(context).colorScheme.surface,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PostHeaderDividerWidget extends StatelessWidget {
|
||||||
|
final Post item;
|
||||||
|
|
||||||
|
const _PostHeaderDividerWidget({
|
||||||
|
required this.item,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (item.body['description'] != null || item.body['title'] != null) {
|
||||||
|
return const Divider(thickness: 0.3, height: 1).paddingSymmetric(
|
||||||
|
vertical: 8,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PostFooterWidget extends StatelessWidget {
|
||||||
|
final Post item;
|
||||||
|
|
||||||
|
const _PostFooterWidget({required this.item});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final unFocusColor =
|
||||||
|
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
||||||
|
|
||||||
|
List<String> labels = List.empty(growable: true);
|
||||||
|
if (item.editedAt != null) {
|
||||||
|
labels.add('postEdited'.trParams({
|
||||||
|
'date': DateFormat('yy/M/d HH:mm').format(item.editedAt!.toLocal()),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
if (item.realm != null) {
|
||||||
|
labels.add('postInRealm'.trParams({
|
||||||
|
'realm': item.realm!.alias,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> widgets = List.empty(growable: true);
|
||||||
|
|
||||||
|
if (item.tags?.isNotEmpty ?? false) {
|
||||||
|
widgets.add(PostTagsList(tags: item.tags!));
|
||||||
|
}
|
||||||
|
if (labels.isNotEmpty) {
|
||||||
|
widgets.add(Text(
|
||||||
|
labels.join(' · '),
|
||||||
|
textAlign: TextAlign.left,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: unFocusColor,
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if (item.pinnedAt != null) {
|
||||||
|
widgets.add(Text(
|
||||||
|
'postPinned'.tr,
|
||||||
|
style: TextStyle(fontSize: 12, color: unFocusColor),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (widgets.isEmpty) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
} else {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: widgets,
|
||||||
|
).paddingOnly(top: 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PostHeaderWidget extends StatelessWidget {
|
||||||
|
final bool isCompact;
|
||||||
|
final Post item;
|
||||||
|
|
||||||
|
const _PostHeaderWidget({
|
||||||
|
required this.isCompact,
|
||||||
|
required this.item,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (isCompact)
|
||||||
|
AccountAvatar(
|
||||||
|
content: item.author.avatar,
|
||||||
|
radius: 10,
|
||||||
|
).paddingOnly(left: 2, top: 1),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
item.author.nick,
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
RelativeDate(item.publishedAt?.toLocal() ?? DateTime.now())
|
||||||
|
.paddingOnly(left: 4),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (item.body['title'] != null)
|
||||||
|
Text(
|
||||||
|
item.body['title'],
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodyMedium!
|
||||||
|
.copyWith(fontSize: 15),
|
||||||
|
),
|
||||||
|
if (item.body['description'] != null)
|
||||||
|
Text(
|
||||||
|
item.body['description'],
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).paddingOnly(left: isCompact ? 6 : 12),
|
||||||
|
),
|
||||||
|
if (item.type == 'article')
|
||||||
|
Badge(
|
||||||
|
label: Text('article'.tr),
|
||||||
|
).paddingOnly(top: 3),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PostThumbnail extends StatelessWidget {
|
||||||
|
final String parentId;
|
||||||
|
final String? rid;
|
||||||
|
|
||||||
|
const _PostThumbnail({required this.parentId, required this.rid});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (rid?.isEmpty ?? true) return const SizedBox.shrink();
|
||||||
|
final border = BorderSide(
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
width: 0.3,
|
||||||
|
);
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(border: Border(top: border, bottom: border)),
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: 16 / 9,
|
||||||
|
child: AttachmentSelfContainedEntry(
|
||||||
|
rid: rid!,
|
||||||
|
parentId: 'p$parentId-thumbnail',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
typedef _OnWidgetSizeChange = void Function(Size size);
|
typedef _OnWidgetSizeChange = void Function(Size size);
|
||||||
|
|
||||||
class _MeasureSizeRenderObject extends RenderProxyBox {
|
class _MeasureSizeRenderObject extends RenderProxyBox {
|
||||||
|
@ -27,7 +27,7 @@ class PostTagsList extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
AppRouter.instance.pushNamed('feedSearch', queryParameters: {
|
AppRouter.instance.pushNamed('postSearch', queryParameters: {
|
||||||
'tag': x.alias,
|
'tag': x.alias,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
23
lib/widgets/relative_date.dart
Normal file
23
lib/widgets/relative_date.dart
Normal 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',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
107
lib/widgets/reports/abuse_report.dart
Normal file
107
lib/widgets/reports/abuse_report.dart
Normal 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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -8,7 +8,10 @@ class EmptyPagePlaceholder extends StatelessWidget {
|
|||||||
return Material(
|
return Material(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
color: Theme.of(context).colorScheme.surface,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Image.asset('assets/logo.png', width: 80, height: 80),
|
child: ClipRRect(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||||
|
child: Image.asset('assets/logo.png', width: 80, height: 80),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class SidebarPlaceholder extends StatelessWidget {
|
|
||||||
const SidebarPlaceholder({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Material(
|
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: const Center(
|
|
||||||
child: Icon(Icons.menu_open, size: 50),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -17,6 +17,7 @@ import flutter_local_notifications
|
|||||||
import flutter_secure_storage_macos
|
import flutter_secure_storage_macos
|
||||||
import flutter_webrtc
|
import flutter_webrtc
|
||||||
import gal
|
import gal
|
||||||
|
import in_app_review
|
||||||
import livekit_client
|
import livekit_client
|
||||||
import macos_window_utils
|
import macos_window_utils
|
||||||
import media_kit_libs_macos_video
|
import media_kit_libs_macos_video
|
||||||
@ -46,6 +47,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
||||||
FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin"))
|
FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin"))
|
||||||
GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin"))
|
GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin"))
|
||||||
|
InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin"))
|
||||||
LiveKitPlugin.register(with: registry.registrar(forPlugin: "LiveKitPlugin"))
|
LiveKitPlugin.register(with: registry.registrar(forPlugin: "LiveKitPlugin"))
|
||||||
MacOSWindowUtilsPlugin.register(with: registry.registrar(forPlugin: "MacOSWindowUtilsPlugin"))
|
MacOSWindowUtilsPlugin.register(with: registry.registrar(forPlugin: "MacOSWindowUtilsPlugin"))
|
||||||
MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin"))
|
MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin"))
|
||||||
|
@ -158,7 +158,9 @@ PODS:
|
|||||||
- GoogleUtilities/UserDefaults (8.0.2):
|
- GoogleUtilities/UserDefaults (8.0.2):
|
||||||
- GoogleUtilities/Logger
|
- GoogleUtilities/Logger
|
||||||
- GoogleUtilities/Privacy
|
- GoogleUtilities/Privacy
|
||||||
- livekit_client (2.2.5):
|
- in_app_review (0.2.0):
|
||||||
|
- FlutterMacOS
|
||||||
|
- livekit_client (2.2.6):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- WebRTC-SDK (= 125.6422.04)
|
- WebRTC-SDK (= 125.6422.04)
|
||||||
- macos_window_utils (1.0.0):
|
- macos_window_utils (1.0.0):
|
||||||
@ -234,6 +236,7 @@ DEPENDENCIES:
|
|||||||
- flutter_webrtc (from `Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos`)
|
- flutter_webrtc (from `Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos`)
|
||||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||||
- gal (from `Flutter/ephemeral/.symlinks/plugins/gal/darwin`)
|
- gal (from `Flutter/ephemeral/.symlinks/plugins/gal/darwin`)
|
||||||
|
- in_app_review (from `Flutter/ephemeral/.symlinks/plugins/in_app_review/macos`)
|
||||||
- livekit_client (from `Flutter/ephemeral/.symlinks/plugins/livekit_client/macos`)
|
- livekit_client (from `Flutter/ephemeral/.symlinks/plugins/livekit_client/macos`)
|
||||||
- macos_window_utils (from `Flutter/ephemeral/.symlinks/plugins/macos_window_utils/macos`)
|
- macos_window_utils (from `Flutter/ephemeral/.symlinks/plugins/macos_window_utils/macos`)
|
||||||
- media_kit_libs_macos_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos`)
|
- media_kit_libs_macos_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos`)
|
||||||
@ -299,6 +302,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: Flutter/ephemeral
|
:path: Flutter/ephemeral
|
||||||
gal:
|
gal:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/gal/darwin
|
:path: Flutter/ephemeral/.symlinks/plugins/gal/darwin
|
||||||
|
in_app_review:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/in_app_review/macos
|
||||||
livekit_client:
|
livekit_client:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/livekit_client/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/livekit_client/macos
|
||||||
macos_window_utils:
|
macos_window_utils:
|
||||||
@ -336,7 +341,7 @@ SPEC CHECKSUMS:
|
|||||||
connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db
|
connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db
|
||||||
desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898
|
desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898
|
||||||
device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720
|
device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720
|
||||||
file_selector_macos: 54fdab7caa3ac3fc43c9fac4d7d8d231277f8cf2
|
file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d
|
||||||
Firebase: 9f574c08c2396885b5e7e100ed4293d956218af9
|
Firebase: 9f574c08c2396885b5e7e100ed4293d956218af9
|
||||||
firebase_analytics: a2d0d907566e4a48e27745317f05b4b7db85edd9
|
firebase_analytics: a2d0d907566e4a48e27745317f05b4b7db85edd9
|
||||||
firebase_core: c55630cdb8a01cf49eae741dd4bc8c93bdd546b8
|
firebase_core: c55630cdb8a01cf49eae741dd4bc8c93bdd546b8
|
||||||
@ -359,7 +364,8 @@ SPEC CHECKSUMS:
|
|||||||
GoogleAppMeasurement: 6e49ffac7d3f2c3ded9cc663f912a13b67bbd0de
|
GoogleAppMeasurement: 6e49ffac7d3f2c3ded9cc663f912a13b67bbd0de
|
||||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||||
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
|
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
|
||||||
livekit_client: be04a950a4b84b9dbc87507ffad5154fe75fa067
|
in_app_review: a850789fad746e89bce03d4aeee8078b45a53fd0
|
||||||
|
livekit_client: 98d09566e3a936b3402be8091ec3845556d36800
|
||||||
macos_window_utils: 933f91f64805e2eb91a5bd057cf97cd097276663
|
macos_window_utils: 933f91f64805e2eb91a5bd057cf97cd097276663
|
||||||
media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82
|
media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82
|
||||||
media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5
|
media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5
|
||||||
@ -377,7 +383,7 @@ SPEC CHECKSUMS:
|
|||||||
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||||
sqlite3: 0bb0e6389d824e40296f531b858a2a0b71c0d2fb
|
sqlite3: 0bb0e6389d824e40296f531b858a2a0b71c0d2fb
|
||||||
sqlite3_flutter_libs: 5ca46c1a04eddfbeeb5b16566164aa7ad1616e7b
|
sqlite3_flutter_libs: 5ca46c1a04eddfbeeb5b16566164aa7ad1616e7b
|
||||||
url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399
|
url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404
|
||||||
wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269
|
wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269
|
||||||
WebRTC-SDK: c3d69a87e7185fad3568f6f3cff7c9ac5890acf3
|
WebRTC-SDK: c3d69a87e7185fad3568f6f3cff7c9ac5890acf3
|
||||||
|
|
||||||
|
@ -12,8 +12,6 @@
|
|||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.device.audio-input</key>
|
<key>com.apple.security.device.audio-input</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.device.bluetooth</key>
|
|
||||||
<true/>
|
|
||||||
<key>com.apple.security.device.camera</key>
|
<key>com.apple.security.device.camera</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.files.user-selected.read-only</key>
|
<key>com.apple.security.files.user-selected.read-only</key>
|
||||||
|
@ -47,10 +47,21 @@
|
|||||||
<string>MainMenu</string>
|
<string>MainMenu</string>
|
||||||
<key>NSPrincipalClass</key>
|
<key>NSPrincipalClass</key>
|
||||||
<string>NSApplication</string>
|
<string>NSApplication</string>
|
||||||
|
<key>CFBundleLocalizations</key>
|
||||||
|
<array>
|
||||||
|
<string>zh_CN</string>
|
||||||
|
<string>en</string>
|
||||||
|
</array>
|
||||||
<key>NSUserActivityTypes</key>
|
<key>NSUserActivityTypes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>INStartCallIntent</string>
|
<string>INStartCallIntent</string>
|
||||||
<string>INSendMessageIntent</string>
|
<string>INSendMessageIntent</string>
|
||||||
</array>
|
</array>
|
||||||
|
<key>NSCameraUsageDescription</key>
|
||||||
|
<string>Allow you take photo/video for your message or post</string>
|
||||||
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
|
<string>Allow you record audio for your message or post</string>
|
||||||
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
|
<string>Allow you add photo to your message or post</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
@ -10,8 +10,6 @@
|
|||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.device.audio-input</key>
|
<key>com.apple.security.device.audio-input</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.device.bluetooth</key>
|
|
||||||
<true/>
|
|
||||||
<key>com.apple.security.device.camera</key>
|
<key>com.apple.security.device.camera</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.files.user-selected.read-only</key>
|
<key>com.apple.security.files.user-selected.read-only</key>
|
||||||
|
52
pubspec.lock
52
pubspec.lock
@ -22,6 +22,14 @@ packages:
|
|||||||
description: dart
|
description: dart
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.3.2"
|
version: "0.3.2"
|
||||||
|
action_slider:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: action_slider
|
||||||
|
sha256: fad0720cde9bf06c12594c15da17dba087556a3285875a91aee3d3a64a3072e2
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.7.0"
|
||||||
analyzer:
|
analyzer:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -330,10 +338,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: desktop_drop
|
name: desktop_drop
|
||||||
sha256: d55a010fe46c8e8fcff4ea4b451a9ff84a162217bdb3b2a0aa1479776205e15d
|
sha256: "03abf1c0443afdd1d65cf8fa589a2f01c67a11da56bbb06f6ea1de79d5628e94"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.4.4"
|
version: "0.5.0"
|
||||||
device_info_plus:
|
device_info_plus:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -466,10 +474,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: file_selector_macos
|
name: file_selector_macos
|
||||||
sha256: f42eacb83b318e183b1ae24eead1373ab1334084404c8c16e0354f9a3e55d385
|
sha256: cb284e267f8e2a45a904b5c094d2ba51d0aabfc20b1538ab786d9ef7dc2bf75c
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.9.4"
|
version: "0.9.4+1"
|
||||||
file_selector_platform_interface:
|
file_selector_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -639,10 +647,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_app_update
|
name: flutter_app_update
|
||||||
sha256: "2b83278d5cc807f543e623d5b466216316104335a4918d9cc4556f39985fe84a"
|
sha256: "3650f57571e9f05d51f008f3fc9d556351910348f8011de7734b56fa74ccfee6"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.0"
|
version: "3.1.1"
|
||||||
flutter_background_service:
|
flutter_background_service:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -743,10 +751,10 @@ packages:
|
|||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: flutter_launcher_icons
|
name: flutter_launcher_icons
|
||||||
sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea"
|
sha256: a38f2f1b3c373d42bf08bd17d60e20d3c73abce7727607b4d085ec7d5acaa294
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.13.1"
|
version: "0.14.0"
|
||||||
flutter_lints:
|
flutter_lints:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@ -759,10 +767,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_local_notifications
|
name: flutter_local_notifications
|
||||||
sha256: c500d5d9e7e553f06b61877ca6b9c8b92c570a4c8db371038702e8ce57f8a50f
|
sha256: "49eeef364fddb71515bc78d5a8c51435a68bccd6e4d68e25a942c5e47761ae71"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "17.2.2"
|
version: "17.2.3"
|
||||||
flutter_local_notifications_linux:
|
flutter_local_notifications_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1117,6 +1125,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.1+1"
|
version: "0.2.1+1"
|
||||||
|
in_app_review:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: in_app_review
|
||||||
|
sha256: "99869244d09adc76af16bf8fd731dd13cef58ecafd5917847589c49f378cbb30"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.9"
|
||||||
|
in_app_review_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: in_app_review_platform_interface
|
||||||
|
sha256: fed2c755f2125caa9ae10495a3c163aa7fab5af3585a9c62ef4a6920c5b45f10
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.5"
|
||||||
infinite_scroll_pagination:
|
infinite_scroll_pagination:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -1201,10 +1225,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: livekit_client
|
name: livekit_client
|
||||||
sha256: "5df9b6f153b5f2c59fbf116b41e54597dfe8b2340b6630f7d8869887a9e58f44"
|
sha256: "449f1f4f7688cc0d27a466d5b78c8973ec4bf2bbe93f79441f4fd118ecea61d7"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.5"
|
version: "2.2.6"
|
||||||
logging:
|
logging:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -2070,10 +2094,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_macos
|
name: url_launcher_macos
|
||||||
sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de"
|
sha256: "769549c999acdb42b8bcfa7c43d72bf79a382ca7441ab18a808e101149daf672"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.0"
|
version: "3.2.1"
|
||||||
url_launcher_platform_interface:
|
url_launcher_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -2,7 +2,7 @@ name: solian
|
|||||||
description: "The Solar Network App"
|
description: "The Solar Network App"
|
||||||
publish_to: "none"
|
publish_to: "none"
|
||||||
|
|
||||||
version: 1.2.2+2
|
version: 1.3.6+2
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.3.4 <4.0.0"
|
sdk: ">=3.3.4 <4.0.0"
|
||||||
@ -43,7 +43,7 @@ dependencies:
|
|||||||
protocol_handler: ^0.2.0
|
protocol_handler: ^0.2.0
|
||||||
markdown: ^7.2.2
|
markdown: ^7.2.2
|
||||||
pasteboard: ^0.3.0
|
pasteboard: ^0.3.0
|
||||||
desktop_drop: ^0.4.4
|
desktop_drop: ^0.5.0
|
||||||
badges: ^3.1.2
|
badges: ^3.1.2
|
||||||
flutter_card_swiper: ^7.0.1
|
flutter_card_swiper: ^7.0.1
|
||||||
dismissible_page: ^1.0.2
|
dismissible_page: ^1.0.2
|
||||||
@ -82,13 +82,15 @@ dependencies:
|
|||||||
flutter_local_notifications: ^17.2.2
|
flutter_local_notifications: ^17.2.2
|
||||||
flutter_app_update: ^3.1.0
|
flutter_app_update: ^3.1.0
|
||||||
version: ^3.0.2
|
version: ^3.0.2
|
||||||
|
action_slider: ^0.7.0
|
||||||
|
in_app_review: ^2.0.9
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
flutter_lints: ^4.0.0
|
flutter_lints: ^4.0.0
|
||||||
flutter_launcher_icons: ^0.13.1
|
flutter_launcher_icons: ^0.14.0
|
||||||
|
|
||||||
build_runner: ^2.4.12
|
build_runner: ^2.4.12
|
||||||
flutter_native_splash: ^2.4.1
|
flutter_native_splash: ^2.4.1
|
||||||
|
Reference in New Issue
Block a user