Compare commits
46 Commits
Author | SHA1 | Date | |
---|---|---|---|
107379d9fe | |||
0d807b8708 | |||
ac1b3fe15c | |||
5853de32a2 | |||
eac1be365e | |||
3fb1d7a6d4 | |||
0480b5244f | |||
56fb92c6b9 | |||
b3267f0026 | |||
88587c10da | |||
9012566dbf | |||
6e00a99803 | |||
aa17a5d52a | |||
ebeffbe1aa | |||
d22eac5c10 | |||
e5381dd5e0 | |||
1c26944a05 | |||
df787f02a1 | |||
db43b7dca5 | |||
59c4d667f6 | |||
063c087089 | |||
48e3b510cf | |||
77288713e1 | |||
1abc65f8fa | |||
a6b17f2c05 | |||
d8dd4060c0 | |||
c8e131c1ab | |||
f4621dd2b4 | |||
6e442c144e | |||
8bbd964026 | |||
0b8a5a3303 | |||
65c6083640 | |||
ad7a34ec18 | |||
6c32d76f78 | |||
2aa699547c | |||
1f4aa8916d | |||
e2c2e41f89 | |||
0f2b854e45 | |||
c21ca5573c | |||
1809f2557d | |||
1fc84099fe | |||
f8755f5220 | |||
4041d6dc4e | |||
cc1071d86e | |||
e334b862df | |||
32c33a963a |
5
.vscode/settings.json
vendored
Normal file
5
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"cSpell.words": [
|
||||||
|
"annvisery"
|
||||||
|
]
|
||||||
|
}
|
@ -54,6 +54,7 @@
|
|||||||
"about": "About",
|
"about": "About",
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
|
"insert": "Insert",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
"settingsNotificationBgService": "Background notification service",
|
"settingsNotificationBgService": "Background notification service",
|
||||||
"settingsNotificationBgServiceDesc": "A notification service is always installed on the device, so that some devices that do not support push notifications can receive notifications in the background. When this feature is enabled, push notifications will not be registered with the server, and you will always appear to be online in the eyes of others (except for invisible). You may need to turn off power and traffic optimization in the settings.",
|
"settingsNotificationBgServiceDesc": "A notification service is always installed on the device, so that some devices that do not support push notifications can receive notifications in the background. When this feature is enabled, push notifications will not be registered with the server, and you will always appear to be online in the eyes of others (except for invisible). You may need to turn off power and traffic optimization in the settings.",
|
||||||
@ -140,7 +141,7 @@
|
|||||||
"clear": "Clear",
|
"clear": "Clear",
|
||||||
"pinPost": "Pin this post",
|
"pinPost": "Pin this post",
|
||||||
"unpinPost": "Unpin this post",
|
"unpinPost": "Unpin this post",
|
||||||
"postRestoreFromLocal": "Restore from local",
|
"postRestoreFromLocal": "Restored",
|
||||||
"postAutoSaveAt": "Auto saved at @date",
|
"postAutoSaveAt": "Auto saved at @date",
|
||||||
"postCategoriesAndTags": "Categories n' Tags",
|
"postCategoriesAndTags": "Categories n' Tags",
|
||||||
"postPublishDate": "Publish Date",
|
"postPublishDate": "Publish Date",
|
||||||
@ -366,8 +367,9 @@
|
|||||||
"bsPreparingData": "Preparing User Data",
|
"bsPreparingData": "Preparing User Data",
|
||||||
"bsRegisteringPushNotify": "Enabling Push Notifications",
|
"bsRegisteringPushNotify": "Enabling Push Notifications",
|
||||||
"bsDismissibleErrorHint": "Click anywhere to ignore this error",
|
"bsDismissibleErrorHint": "Click anywhere to ignore this error",
|
||||||
|
"bsContinuable": "Click anywhere to continue",
|
||||||
"postShareContent": "@content\n\n@username on the Solar Network\nCheck it out: @link",
|
"postShareContent": "@content\n\n@username on the Solar Network\nCheck it out: @link",
|
||||||
"postShareSubject": "@username posted a post on the Solar Network",
|
"postShareSubject": "@title by @username on Solar Network",
|
||||||
"themeColor": "Global Theme Color",
|
"themeColor": "Global Theme Color",
|
||||||
"themeColorRed": "Modern Red",
|
"themeColorRed": "Modern Red",
|
||||||
"themeColorBlue": "Classic Blue",
|
"themeColorBlue": "Classic Blue",
|
||||||
@ -477,5 +479,20 @@
|
|||||||
"agedTheme": "Old school style theme",
|
"agedTheme": "Old school style theme",
|
||||||
"agedThemeDesc": "Downgrade the global theme to Material Design 2. Unexpected issues may occur. For experimental use only.",
|
"agedThemeDesc": "Downgrade the global theme to Material Design 2. Unexpected issues may occur. For experimental use only.",
|
||||||
"appBackgroundImage": "Global background image",
|
"appBackgroundImage": "Global background image",
|
||||||
"appBackgroundImageDesc": "The global background image will be displayed on all pages"
|
"appBackgroundImageDesc": "The global background image will be displayed on all pages",
|
||||||
|
"authPreferences": "Auth preferences",
|
||||||
|
"authPreferencesDesc": "Set the security behavior of your account",
|
||||||
|
"authMaximumAuthSteps": "Maximum authentication steps",
|
||||||
|
"authMaximumAuthStepsDesc": "The maximum number of authentication steps when logging in, higher value is more secure, lower value is more convenient; default is 2",
|
||||||
|
"auditLog": "Audit log",
|
||||||
|
"shareImage": "Share as image",
|
||||||
|
"shareImageFooter": "Only on the Solar Network",
|
||||||
|
"fileSavedAt": "File saved at @path",
|
||||||
|
"showIp": "Show IP Address",
|
||||||
|
"shotOn": "Shot on @device",
|
||||||
|
"unread": "Unread",
|
||||||
|
"searchTook": "Took @time",
|
||||||
|
"searchResult": "@count Matches",
|
||||||
|
"happyBirthday": "Happy birthday @name!",
|
||||||
|
"happyBirthdayDesc": "Today is your @count birthday"
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
"about": "关于",
|
"about": "关于",
|
||||||
"edit": "编辑",
|
"edit": "编辑",
|
||||||
"delete": "删除",
|
"delete": "删除",
|
||||||
|
"insert": "插入",
|
||||||
"settings": "设置",
|
"settings": "设置",
|
||||||
"settingsNotificationBgService": "常驻通知服务",
|
"settingsNotificationBgService": "常驻通知服务",
|
||||||
"settingsNotificationBgServiceDesc": "在设备常驻一个通知服务,使得部分不支持推送通知的设备可以在后台收到通知;启用该功能的情况下不会向服务器注册推送通知,并且你会始终在他人眼中成为在线(隐身除外);可能需要在设置中关闭电量与流量优化。",
|
"settingsNotificationBgServiceDesc": "在设备常驻一个通知服务,使得部分不支持推送通知的设备可以在后台收到通知;启用该功能的情况下不会向服务器注册推送通知,并且你会始终在他人眼中成为在线(隐身除外);可能需要在设置中关闭电量与流量优化。",
|
||||||
@ -362,8 +363,9 @@
|
|||||||
"bsPreparingData": "正在准备用户资料",
|
"bsPreparingData": "正在准备用户资料",
|
||||||
"bsRegisteringPushNotify": "正在启用推送通知",
|
"bsRegisteringPushNotify": "正在启用推送通知",
|
||||||
"bsDismissibleErrorHint": "点击任意地方忽略此错误",
|
"bsDismissibleErrorHint": "点击任意地方忽略此错误",
|
||||||
|
"bsContinuable": "点击任意处继续",
|
||||||
"postShareContent": "@content\n\n@username 在 Solar Network\n原帖地址:@link",
|
"postShareContent": "@content\n\n@username 在 Solar Network\n原帖地址:@link",
|
||||||
"postShareSubject": "@username 在 Solar Network 上发布了一篇帖子",
|
"postShareSubject": "@username 在 Solar Network 发表的 @title",
|
||||||
"themeColor": "全局主题色",
|
"themeColor": "全局主题色",
|
||||||
"themeColorRed": "现代红",
|
"themeColorRed": "现代红",
|
||||||
"themeColorBlue": "经典蓝",
|
"themeColorBlue": "经典蓝",
|
||||||
@ -473,5 +475,20 @@
|
|||||||
"agedTheme": "过时主题",
|
"agedTheme": "过时主题",
|
||||||
"agedThemeDesc": "将全局主题降级为 Material Design 2,可能发生意料之外的问题,仅供实验使用",
|
"agedThemeDesc": "将全局主题降级为 Material Design 2,可能发生意料之外的问题,仅供实验使用",
|
||||||
"appBackgroundImage": "全局背景图片",
|
"appBackgroundImage": "全局背景图片",
|
||||||
"appBackgroundImageDesc": "全局背景图片将会在所有页面中展示"
|
"appBackgroundImageDesc": "全局背景图片将会在所有页面中展示",
|
||||||
|
"authPreferences": "安全偏好设置",
|
||||||
|
"authPreferencesDesc": "调整账号的安全行为模式",
|
||||||
|
"authMaximumAuthSteps": "最大认证步数",
|
||||||
|
"authMaximumAuthStepsDesc": "登陆时最多的验证步数,值越高则越安全,反之则会相对方便;默认设置为 2",
|
||||||
|
"auditLog": "活动日志",
|
||||||
|
"shareImage": "分享图片",
|
||||||
|
"shareImageFooter": "上 Solar Network 看更多有趣帖子",
|
||||||
|
"fileSavedAt": "文件保存于 @path",
|
||||||
|
"showIp": "显示 IP 地址",
|
||||||
|
"shotOn": "由 @device 拍摄",
|
||||||
|
"unread": "未读",
|
||||||
|
"searchTook": "耗时 @time",
|
||||||
|
"searchResult": "匹配到 @count 条结果",
|
||||||
|
"happyBirthday": "生日快乐,@name!",
|
||||||
|
"happyBirthdayDesc": "今天是你的第 @count 个生日"
|
||||||
}
|
}
|
||||||
|
@ -38,6 +38,8 @@ PODS:
|
|||||||
- file_picker (0.0.1):
|
- file_picker (0.0.1):
|
||||||
- DKImagePickerController/PhotoGallery
|
- DKImagePickerController/PhotoGallery
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- file_saver (0.0.1):
|
||||||
|
- Flutter
|
||||||
- Firebase/Analytics (11.2.0):
|
- Firebase/Analytics (11.2.0):
|
||||||
- Firebase/Core
|
- Firebase/Core
|
||||||
- Firebase/Core (11.2.0):
|
- Firebase/Core (11.2.0):
|
||||||
@ -166,6 +168,9 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- flutter_secure_storage (6.0.0):
|
- flutter_secure_storage (6.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- flutter_udid (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
- SAMKeychain
|
||||||
- flutter_webrtc (0.11.3):
|
- flutter_webrtc (0.11.3):
|
||||||
- Flutter
|
- Flutter
|
||||||
- WebRTC-SDK (= 125.6422.04)
|
- WebRTC-SDK (= 125.6422.04)
|
||||||
@ -259,6 +264,7 @@ PODS:
|
|||||||
- PromisesObjC (= 2.4.0)
|
- PromisesObjC (= 2.4.0)
|
||||||
- protocol_handler_ios (0.0.1):
|
- protocol_handler_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- SAMKeychain (1.5.3)
|
||||||
- screen_brightness_ios (0.1.0):
|
- screen_brightness_ios (0.1.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- SDWebImage (5.19.7):
|
- SDWebImage (5.19.7):
|
||||||
@ -304,6 +310,7 @@ DEPENDENCIES:
|
|||||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`)
|
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`)
|
||||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||||
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
||||||
|
- file_saver (from `.symlinks/plugins/file_saver/ios`)
|
||||||
- firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`)
|
- firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`)
|
||||||
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
|
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
|
||||||
- firebase_crashlytics (from `.symlinks/plugins/firebase_crashlytics/ios`)
|
- firebase_crashlytics (from `.symlinks/plugins/firebase_crashlytics/ios`)
|
||||||
@ -316,6 +323,7 @@ DEPENDENCIES:
|
|||||||
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
||||||
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
||||||
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
||||||
|
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
|
||||||
- flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
|
- flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
|
||||||
- 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`)
|
||||||
@ -364,6 +372,7 @@ SPEC REPOS:
|
|||||||
- nanopb
|
- nanopb
|
||||||
- PromisesObjC
|
- PromisesObjC
|
||||||
- PromisesSwift
|
- PromisesSwift
|
||||||
|
- SAMKeychain
|
||||||
- SDWebImage
|
- SDWebImage
|
||||||
- sqlite3
|
- sqlite3
|
||||||
- SwiftyGif
|
- SwiftyGif
|
||||||
@ -377,6 +386,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/device_info_plus/ios"
|
:path: ".symlinks/plugins/device_info_plus/ios"
|
||||||
file_picker:
|
file_picker:
|
||||||
:path: ".symlinks/plugins/file_picker/ios"
|
:path: ".symlinks/plugins/file_picker/ios"
|
||||||
|
file_saver:
|
||||||
|
:path: ".symlinks/plugins/file_saver/ios"
|
||||||
firebase_analytics:
|
firebase_analytics:
|
||||||
:path: ".symlinks/plugins/firebase_analytics/ios"
|
:path: ".symlinks/plugins/firebase_analytics/ios"
|
||||||
firebase_core:
|
firebase_core:
|
||||||
@ -401,6 +412,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
||||||
flutter_secure_storage:
|
flutter_secure_storage:
|
||||||
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
||||||
|
flutter_udid:
|
||||||
|
:path: ".symlinks/plugins/flutter_udid/ios"
|
||||||
flutter_webrtc:
|
flutter_webrtc:
|
||||||
:path: ".symlinks/plugins/flutter_webrtc/ios"
|
:path: ".symlinks/plugins/flutter_webrtc/ios"
|
||||||
gal:
|
gal:
|
||||||
@ -449,11 +462,12 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/wakelock_plus/ios"
|
:path: ".symlinks/plugins/wakelock_plus/ios"
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db
|
connectivity_plus: 4c41c08fc6d7c91f63bc7aec70ffe3730b04f563
|
||||||
device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d
|
device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
|
||||||
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
||||||
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||||
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
|
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
|
||||||
|
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
|
||||||
Firebase: 98e6bf5278170668a7983e12971a66b2cd57fc8c
|
Firebase: 98e6bf5278170668a7983e12971a66b2cd57fc8c
|
||||||
firebase_analytics: fbc57838bdb94eef1e0ff504f127d974ff2981ad
|
firebase_analytics: fbc57838bdb94eef1e0ff504f127d974ff2981ad
|
||||||
firebase_core: 2bedc3136ec7c7b8561c6123ed0239387b53f2af
|
firebase_core: 2bedc3136ec7c7b8561c6123ed0239387b53f2af
|
||||||
@ -480,6 +494,7 @@ SPEC CHECKSUMS:
|
|||||||
flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086
|
flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086
|
||||||
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
|
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
|
||||||
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
|
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
|
||||||
|
flutter_udid: a2482c67a61b9c806ef59dd82ed8d007f1b7ac04
|
||||||
flutter_webrtc: 75b868e4f9e817c7a9a42ca4b6169063de4eec9f
|
flutter_webrtc: 75b868e4f9e817c7a9a42ca4b6169063de4eec9f
|
||||||
gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1
|
gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1
|
||||||
GoogleAppMeasurement: 76d4f8b36b03bd8381fa9a7fe2cc7f99c0a2e93a
|
GoogleAppMeasurement: 76d4f8b36b03bd8381fa9a7fe2cc7f99c0a2e93a
|
||||||
@ -493,7 +508,7 @@ SPEC CHECKSUMS:
|
|||||||
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
|
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
|
||||||
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
|
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
|
||||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||||
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
|
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
|
||||||
pasteboard: 982969ebaa7c78af3e6cc7761e8f5e77565d9ce0
|
pasteboard: 982969ebaa7c78af3e6cc7761e8f5e77565d9ce0
|
||||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||||
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
||||||
@ -501,9 +516,10 @@ SPEC CHECKSUMS:
|
|||||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||||
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
|
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
|
||||||
protocol_handler_ios: a5db8abc38526ee326988b808be621e5fd568990
|
protocol_handler_ios: a5db8abc38526ee326988b808be621e5fd568990
|
||||||
|
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||||
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
|
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
|
||||||
SDWebImage: 8a6b7b160b4d710e2a22b6900e25301075c34cb3
|
SDWebImage: 8a6b7b160b4d710e2a22b6900e25301075c34cb3
|
||||||
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
|
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
|
||||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||||
sqflite_darwin: a553b1fd6fe66f53bbb0fe5b4f5bab93f08d7a13
|
sqflite_darwin: a553b1fd6fe66f53bbb0fe5b4f5bab93f08d7a13
|
||||||
sqlite3: 0bb0e6389d824e40296f531b858a2a0b71c0d2fb
|
sqlite3: 0bb0e6389d824e40296f531b858a2a0b71c0d2fb
|
||||||
|
@ -59,6 +59,7 @@
|
|||||||
ignoresPersistentStateOnLaunch = "NO"
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
debugDocumentVersioning = "YES"
|
debugDocumentVersioning = "YES"
|
||||||
debugServiceExtension = "internal"
|
debugServiceExtension = "internal"
|
||||||
|
showGraphicsOverview = "Yes"
|
||||||
allowLocationSimulation = "YES">
|
allowLocationSimulation = "YES">
|
||||||
<BuildableProductRunnable
|
<BuildableProductRunnable
|
||||||
runnableDebuggingMode = "0">
|
runnableDebuggingMode = "0">
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
|
|
||||||
|
import 'package:confetti/confetti.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';
|
||||||
@ -10,9 +11,11 @@ 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/exceptions/request.dart';
|
||||||
import 'package:solian/exts.dart';
|
import 'package:solian/exts.dart';
|
||||||
|
import 'package:solian/models/account.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/realm.dart';
|
import 'package:solian/providers/content/realm.dart';
|
||||||
|
import 'package:solian/providers/notifications.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';
|
||||||
import 'package:solian/providers/websocket.dart';
|
import 'package:solian/providers/websocket.dart';
|
||||||
@ -22,6 +25,11 @@ import 'package:solian/widgets/sized_container.dart';
|
|||||||
import 'package:flutter_app_update/flutter_app_update.dart';
|
import 'package:flutter_app_update/flutter_app_update.dart';
|
||||||
import 'package:version/version.dart';
|
import 'package:version/version.dart';
|
||||||
|
|
||||||
|
enum BootstrapperSpecialState {
|
||||||
|
userBirthday,
|
||||||
|
appAnniversary,
|
||||||
|
}
|
||||||
|
|
||||||
class BootstrapperShell extends StatefulWidget {
|
class BootstrapperShell extends StatefulWidget {
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
|
||||||
@ -42,6 +50,9 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
|
|||||||
|
|
||||||
int _periodCursor = 0;
|
int _periodCursor = 0;
|
||||||
|
|
||||||
|
// Special state is some special event triggered after bootstrapping
|
||||||
|
BootstrapperSpecialState? _specialState;
|
||||||
|
|
||||||
final Completer _bootCompleter = Completer();
|
final Completer _bootCompleter = Completer();
|
||||||
|
|
||||||
void _requestRating() async {
|
void _requestRating() async {
|
||||||
@ -198,11 +209,26 @@ 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<NotificationProvider>().fetchNotification(),
|
||||||
if (auth.isAuthorized.isTrue)
|
if (auth.isAuthorized.isTrue)
|
||||||
Get.find<RelationshipProvider>().refreshRelativeList(),
|
Get.find<RelationshipProvider>().refreshRelativeList(),
|
||||||
if (auth.isAuthorized.isTrue)
|
if (auth.isAuthorized.isTrue)
|
||||||
Get.find<RealmProvider>().refreshAvailableRealms(),
|
Get.find<RealmProvider>().refreshAvailableRealms(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
if (auth.isAuthorized.isTrue && auth.userProfile.value != null) {
|
||||||
|
final account = Account.fromJson(auth.userProfile.value!);
|
||||||
|
if (account.profile?.birthday != null) {
|
||||||
|
final birthDate = account.profile!.birthday!.toLocal();
|
||||||
|
final isBirthday = birthDate.day == DateTime.now().day;
|
||||||
|
if (isBirthday) {
|
||||||
|
setState(
|
||||||
|
() => _specialState = BootstrapperSpecialState.userBirthday,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
context.showErrorDialog(e);
|
context.showErrorDialog(e);
|
||||||
}
|
}
|
||||||
@ -214,7 +240,7 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
|
|||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
if (auth.isAuthorized.isTrue) {
|
if (auth.isAuthorized.isTrue) {
|
||||||
try {
|
try {
|
||||||
Get.find<WebSocketProvider>().registerPushNotifications();
|
Get.find<NotificationProvider>().registerPushNotifications();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
context.showSnackbar(
|
context.showSnackbar(
|
||||||
'pushNotifyRegisterFailed'.trParams({'reason': err.toString()}),
|
'pushNotifyRegisterFailed'.trParams({'reason': err.toString()}),
|
||||||
@ -352,8 +378,142 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
} else if (_specialState != null) {
|
||||||
|
return GestureDetector(
|
||||||
|
child: RootContainer(
|
||||||
|
child: switch (_specialState) {
|
||||||
|
BootstrapperSpecialState.appAnniversary => const Placeholder(),
|
||||||
|
_ => _BirthdaySpecialScreen(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
setState(() => _specialState = null);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return widget.child;
|
return widget.child;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _BirthdaySpecialScreen extends StatefulWidget {
|
||||||
|
const _BirthdaySpecialScreen();
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_BirthdaySpecialScreen> createState() => _BirthdaySpecialScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BirthdaySpecialScreenState extends State<_BirthdaySpecialScreen> {
|
||||||
|
late final ConfettiController _confettiController =
|
||||||
|
ConfettiController(duration: const Duration(seconds: 10));
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
_confettiController.play();
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_confettiController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Color get _unFocusColor =>
|
||||||
|
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
||||||
|
|
||||||
|
String _toOrdinal(int num) {
|
||||||
|
if (num >= 11 && num <= 13) {
|
||||||
|
return '${num}th';
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (num % 10) {
|
||||||
|
case 1:
|
||||||
|
return '${num}st';
|
||||||
|
case 2:
|
||||||
|
return '${num}nd';
|
||||||
|
case 3:
|
||||||
|
return '${num}rd';
|
||||||
|
default:
|
||||||
|
return '${num}th';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final AuthProvider auth = Get.find();
|
||||||
|
final account = Account.fromJson(auth.userProfile.value!);
|
||||||
|
|
||||||
|
final birthDate = account.profile!.birthday!.toLocal();
|
||||||
|
final birthdayCount = DateTime.now().difference(birthDate).inDays ~/ 365;
|
||||||
|
|
||||||
|
return Stack(
|
||||||
|
children: <Widget>[
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: ConfettiWidget(
|
||||||
|
confettiController: _confettiController,
|
||||||
|
blastDirectionality: BlastDirectionality.explosive,
|
||||||
|
shouldLoop: true,
|
||||||
|
colors: const [
|
||||||
|
Colors.green,
|
||||||
|
Colors.blue,
|
||||||
|
Colors.pink,
|
||||||
|
Colors.orange,
|
||||||
|
Colors.purple
|
||||||
|
],
|
||||||
|
maxBlastForce: 30,
|
||||||
|
minBlastForce: 15,
|
||||||
|
emissionFrequency: 0.05,
|
||||||
|
numberOfParticles: 20,
|
||||||
|
gravity: 0.2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Align(
|
||||||
|
child: CenteredContainer(
|
||||||
|
maxWidth: 320,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'🎂',
|
||||||
|
style: TextStyle(fontSize: 60),
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
Text(
|
||||||
|
'happyBirthday'.trParams({
|
||||||
|
'name': account.profile?.firstName != null
|
||||||
|
? [
|
||||||
|
account.profile?.firstName,
|
||||||
|
account.profile?.lastName
|
||||||
|
].join(' ')
|
||||||
|
: '@${account.name}',
|
||||||
|
}),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'happyBirthdayDesc'.trParams({
|
||||||
|
'count': _toOrdinal(birthdayCount),
|
||||||
|
}),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
Text(
|
||||||
|
'bsContinuable'.tr,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: _unFocusColor,
|
||||||
|
),
|
||||||
|
).paddingOnly(bottom: 5),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -43,14 +43,17 @@ class PostEditorController extends GetxController {
|
|||||||
|
|
||||||
RxBool isRestoreFromLocal = false.obs;
|
RxBool isRestoreFromLocal = false.obs;
|
||||||
Rx<DateTime?> lastSaveTime = Rx(null);
|
Rx<DateTime?> lastSaveTime = Rx(null);
|
||||||
Timer? _saveTimer;
|
Future? _saveFuture;
|
||||||
|
|
||||||
PostEditorController() {
|
PostEditorController() {
|
||||||
SharedPreferences.getInstance().then((inst) {
|
SharedPreferences.getInstance().then((inst) {
|
||||||
_prefs = inst;
|
_prefs = inst;
|
||||||
_saveTimer = Timer.periodic(
|
});
|
||||||
const Duration(seconds: 3),
|
contentController.addListener(() {
|
||||||
(Timer t) {
|
contentLength.value = contentController.text.length;
|
||||||
|
_saveFuture ??= Future.delayed(
|
||||||
|
const Duration(seconds: 1),
|
||||||
|
() {
|
||||||
if (isNotEmpty) {
|
if (isNotEmpty) {
|
||||||
localSave();
|
localSave();
|
||||||
lastSaveTime.value = DateTime.now();
|
lastSaveTime.value = DateTime.now();
|
||||||
@ -59,12 +62,10 @@ class PostEditorController extends GetxController {
|
|||||||
localClear();
|
localClear();
|
||||||
lastSaveTime.value = null;
|
lastSaveTime.value = null;
|
||||||
}
|
}
|
||||||
|
_saveFuture = null;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
contentController.addListener(() {
|
|
||||||
contentLength.value = contentController.text.length;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> editOverview(BuildContext context) {
|
Future<void> editOverview(BuildContext context) {
|
||||||
@ -124,6 +125,21 @@ class PostEditorController extends GetxController {
|
|||||||
onRemove: (String value) {
|
onRemove: (String value) {
|
||||||
attachments.remove(value);
|
attachments.remove(value);
|
||||||
},
|
},
|
||||||
|
onInsert: (String str) {
|
||||||
|
final text = contentController.text;
|
||||||
|
final selection = contentController.selection;
|
||||||
|
final newText = text.replaceRange(
|
||||||
|
selection.start,
|
||||||
|
selection.end,
|
||||||
|
str,
|
||||||
|
);
|
||||||
|
contentController.value = TextEditingValue(
|
||||||
|
text: newText,
|
||||||
|
selection: TextSelection.collapsed(
|
||||||
|
offset: selection.baseOffset + str.length,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -355,8 +371,6 @@ class PostEditorController extends GetxController {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_saveTimer?.cancel();
|
|
||||||
|
|
||||||
titleController.dispose();
|
titleController.dispose();
|
||||||
descriptionController.dispose();
|
descriptionController.dispose();
|
||||||
contentController.dispose();
|
contentController.dispose();
|
||||||
|
@ -18,6 +18,7 @@ import 'package:solian/providers/database/services/messages.dart';
|
|||||||
import 'package:solian/providers/last_read.dart';
|
import 'package:solian/providers/last_read.dart';
|
||||||
import 'package:solian/providers/link_expander.dart';
|
import 'package:solian/providers/link_expander.dart';
|
||||||
import 'package:solian/providers/navigation.dart';
|
import 'package:solian/providers/navigation.dart';
|
||||||
|
import 'package:solian/providers/notifications.dart';
|
||||||
import 'package:solian/providers/stickers.dart';
|
import 'package:solian/providers/stickers.dart';
|
||||||
import 'package:solian/providers/subscription.dart';
|
import 'package:solian/providers/subscription.dart';
|
||||||
import 'package:solian/providers/theme_switcher.dart';
|
import 'package:solian/providers/theme_switcher.dart';
|
||||||
@ -138,11 +139,12 @@ class SolianApp extends StatelessWidget {
|
|||||||
Get.put(NavigationStateProvider());
|
Get.put(NavigationStateProvider());
|
||||||
|
|
||||||
Get.lazyPut(() => AuthProvider());
|
Get.lazyPut(() => AuthProvider());
|
||||||
|
Get.lazyPut(() => WebSocketProvider());
|
||||||
Get.lazyPut(() => RelationshipProvider());
|
Get.lazyPut(() => RelationshipProvider());
|
||||||
Get.lazyPut(() => PostProvider());
|
Get.lazyPut(() => PostProvider());
|
||||||
Get.lazyPut(() => StickerProvider());
|
Get.lazyPut(() => StickerProvider());
|
||||||
Get.lazyPut(() => AttachmentProvider());
|
Get.lazyPut(() => AttachmentProvider());
|
||||||
Get.lazyPut(() => WebSocketProvider());
|
Get.lazyPut(() => NotificationProvider());
|
||||||
Get.lazyPut(() => StatusProvider());
|
Get.lazyPut(() => StatusProvider());
|
||||||
Get.lazyPut(() => ChannelProvider());
|
Get.lazyPut(() => ChannelProvider());
|
||||||
Get.lazyPut(() => RealmProvider());
|
Get.lazyPut(() => RealmProvider());
|
||||||
@ -154,6 +156,6 @@ class SolianApp extends StatelessWidget {
|
|||||||
Get.lazyPut(() => LastReadProvider());
|
Get.lazyPut(() => LastReadProvider());
|
||||||
Get.lazyPut(() => SubscriptionProvider());
|
Get.lazyPut(() => SubscriptionProvider());
|
||||||
|
|
||||||
Get.find<WebSocketProvider>().requestPermissions();
|
Get.find<NotificationProvider>().requestPermissions();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
38
lib/models/audit_log.dart
Normal file
38
lib/models/audit_log.dart
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
import 'package:solian/models/account.dart';
|
||||||
|
|
||||||
|
part 'audit_log.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class AuditEvent {
|
||||||
|
int id;
|
||||||
|
DateTime createdAt;
|
||||||
|
DateTime updatedAt;
|
||||||
|
DateTime? deletedAt;
|
||||||
|
String type;
|
||||||
|
String target;
|
||||||
|
String location;
|
||||||
|
String ipAddress;
|
||||||
|
String userAgent;
|
||||||
|
Account account;
|
||||||
|
int accountId;
|
||||||
|
|
||||||
|
AuditEvent({
|
||||||
|
required this.id,
|
||||||
|
required this.createdAt,
|
||||||
|
required this.updatedAt,
|
||||||
|
required this.deletedAt,
|
||||||
|
required this.type,
|
||||||
|
required this.target,
|
||||||
|
required this.location,
|
||||||
|
required this.ipAddress,
|
||||||
|
required this.userAgent,
|
||||||
|
required this.account,
|
||||||
|
required this.accountId,
|
||||||
|
});
|
||||||
|
|
||||||
|
static AuditEvent fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$AuditEventFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$AuditEventToJson(this);
|
||||||
|
}
|
38
lib/models/audit_log.g.dart
Normal file
38
lib/models/audit_log.g.dart
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'audit_log.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
AuditEvent _$AuditEventFromJson(Map<String, dynamic> json) => AuditEvent(
|
||||||
|
id: (json['id'] as num).toInt(),
|
||||||
|
createdAt: DateTime.parse(json['created_at'] as String),
|
||||||
|
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||||
|
deletedAt: json['deleted_at'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['deleted_at'] as String),
|
||||||
|
type: json['type'] as String,
|
||||||
|
target: json['target'] as String,
|
||||||
|
location: json['location'] as String,
|
||||||
|
ipAddress: json['ip_address'] as String,
|
||||||
|
userAgent: json['user_agent'] as String,
|
||||||
|
account: Account.fromJson(json['account'] as Map<String, dynamic>),
|
||||||
|
accountId: (json['account_id'] as num).toInt(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$AuditEventToJson(AuditEvent instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'created_at': instance.createdAt.toIso8601String(),
|
||||||
|
'updated_at': instance.updatedAt.toIso8601String(),
|
||||||
|
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||||
|
'type': instance.type,
|
||||||
|
'target': instance.target,
|
||||||
|
'location': instance.location,
|
||||||
|
'ip_address': instance.ipAddress,
|
||||||
|
'user_agent': instance.userAgent,
|
||||||
|
'account': instance.account.toJson(),
|
||||||
|
'account_id': instance.accountId,
|
||||||
|
};
|
@ -1,18 +1,29 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
part 'notification.g.dart';
|
part 'notification.g.dart';
|
||||||
|
|
||||||
|
const Map<String, IconData> NotificationTopicIcons = {
|
||||||
|
'passport.security.alert': Icons.gpp_maybe,
|
||||||
|
'interactive.subscription': Icons.subscriptions,
|
||||||
|
'interactive.feedback': Icons.add_reaction,
|
||||||
|
'messaging.callStart': Icons.call_received,
|
||||||
|
};
|
||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class Notification {
|
class Notification {
|
||||||
int id;
|
int id;
|
||||||
DateTime createdAt;
|
DateTime createdAt;
|
||||||
DateTime updatedAt;
|
DateTime updatedAt;
|
||||||
DateTime? deletedAt;
|
DateTime? deletedAt;
|
||||||
|
DateTime? readAt;
|
||||||
|
String topic;
|
||||||
String title;
|
String title;
|
||||||
String? subtitle;
|
String? subtitle;
|
||||||
String body;
|
String body;
|
||||||
String? avatar;
|
String? avatar;
|
||||||
String? picture;
|
String? picture;
|
||||||
|
Map<String, dynamic>? metadata;
|
||||||
int? senderId;
|
int? senderId;
|
||||||
int accountId;
|
int accountId;
|
||||||
|
|
||||||
@ -21,11 +32,14 @@ class Notification {
|
|||||||
required this.createdAt,
|
required this.createdAt,
|
||||||
required this.updatedAt,
|
required this.updatedAt,
|
||||||
required this.deletedAt,
|
required this.deletedAt,
|
||||||
|
required this.readAt,
|
||||||
|
required this.topic,
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.subtitle,
|
required this.subtitle,
|
||||||
required this.body,
|
required this.body,
|
||||||
required this.avatar,
|
required this.avatar,
|
||||||
required this.picture,
|
required this.picture,
|
||||||
|
required this.metadata,
|
||||||
required this.senderId,
|
required this.senderId,
|
||||||
required this.accountId,
|
required this.accountId,
|
||||||
});
|
});
|
||||||
|
@ -13,11 +13,16 @@ Notification _$NotificationFromJson(Map<String, dynamic> json) => Notification(
|
|||||||
deletedAt: json['deleted_at'] == null
|
deletedAt: json['deleted_at'] == null
|
||||||
? null
|
? null
|
||||||
: DateTime.parse(json['deleted_at'] as String),
|
: DateTime.parse(json['deleted_at'] as String),
|
||||||
|
readAt: json['read_at'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['read_at'] as String),
|
||||||
|
topic: json['topic'] as String,
|
||||||
title: json['title'] as String,
|
title: json['title'] as String,
|
||||||
subtitle: json['subtitle'] as String?,
|
subtitle: json['subtitle'] as String?,
|
||||||
body: json['body'] as String,
|
body: json['body'] as String,
|
||||||
avatar: json['avatar'] as String?,
|
avatar: json['avatar'] as String?,
|
||||||
picture: json['picture'] as String?,
|
picture: json['picture'] as String?,
|
||||||
|
metadata: json['metadata'] as Map<String, dynamic>?,
|
||||||
senderId: (json['sender_id'] as num?)?.toInt(),
|
senderId: (json['sender_id'] as num?)?.toInt(),
|
||||||
accountId: (json['account_id'] as num).toInt(),
|
accountId: (json['account_id'] as num).toInt(),
|
||||||
);
|
);
|
||||||
@ -28,11 +33,14 @@ Map<String, dynamic> _$NotificationToJson(Notification instance) =>
|
|||||||
'created_at': instance.createdAt.toIso8601String(),
|
'created_at': instance.createdAt.toIso8601String(),
|
||||||
'updated_at': instance.updatedAt.toIso8601String(),
|
'updated_at': instance.updatedAt.toIso8601String(),
|
||||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||||
|
'read_at': instance.readAt?.toIso8601String(),
|
||||||
|
'topic': instance.topic,
|
||||||
'title': instance.title,
|
'title': instance.title,
|
||||||
'subtitle': instance.subtitle,
|
'subtitle': instance.subtitle,
|
||||||
'body': instance.body,
|
'body': instance.body,
|
||||||
'avatar': instance.avatar,
|
'avatar': instance.avatar,
|
||||||
'picture': instance.picture,
|
'picture': instance.picture,
|
||||||
|
'metadata': instance.metadata,
|
||||||
'sender_id': instance.senderId,
|
'sender_id': instance.senderId,
|
||||||
'account_id': instance.accountId,
|
'account_id': instance.accountId,
|
||||||
};
|
};
|
||||||
|
@ -24,6 +24,7 @@ class Post {
|
|||||||
String? alias;
|
String? alias;
|
||||||
String? areaAlias;
|
String? areaAlias;
|
||||||
dynamic body;
|
dynamic body;
|
||||||
|
int visibility;
|
||||||
List<Tag>? tags;
|
List<Tag>? tags;
|
||||||
List<Category>? categories;
|
List<Category>? categories;
|
||||||
List<Post>? replies;
|
List<Post>? replies;
|
||||||
@ -55,6 +56,7 @@ class Post {
|
|||||||
required this.areaAlias,
|
required this.areaAlias,
|
||||||
required this.type,
|
required this.type,
|
||||||
required this.body,
|
required this.body,
|
||||||
|
required this.visibility,
|
||||||
required this.tags,
|
required this.tags,
|
||||||
required this.categories,
|
required this.categories,
|
||||||
required this.replies,
|
required this.replies,
|
||||||
|
@ -20,6 +20,7 @@ Post _$PostFromJson(Map<String, dynamic> json) => Post(
|
|||||||
areaAlias: json['area_alias'] as String?,
|
areaAlias: json['area_alias'] as String?,
|
||||||
type: json['type'] as String,
|
type: json['type'] as String,
|
||||||
body: json['body'],
|
body: json['body'],
|
||||||
|
visibility: (json['visibility'] as num).toInt(),
|
||||||
tags: (json['tags'] as List<dynamic>?)
|
tags: (json['tags'] as List<dynamic>?)
|
||||||
?.map((e) => Tag.fromJson(e as Map<String, dynamic>))
|
?.map((e) => Tag.fromJson(e as Map<String, dynamic>))
|
||||||
.toList(),
|
.toList(),
|
||||||
@ -67,6 +68,7 @@ Map<String, dynamic> _$PostToJson(Post instance) => <String, dynamic>{
|
|||||||
'alias': instance.alias,
|
'alias': instance.alias,
|
||||||
'area_alias': instance.areaAlias,
|
'area_alias': instance.areaAlias,
|
||||||
'body': instance.body,
|
'body': instance.body,
|
||||||
|
'visibility': instance.visibility,
|
||||||
'tags': instance.tags?.map((e) => e.toJson()).toList(),
|
'tags': instance.tags?.map((e) => e.toJson()).toList(),
|
||||||
'categories': instance.categories?.map((e) => e.toJson()).toList(),
|
'categories': instance.categories?.map((e) => e.toJson()).toList(),
|
||||||
'replies': instance.replies?.map((e) => e.toJson()).toList(),
|
'replies': instance.replies?.map((e) => e.toJson()).toList(),
|
||||||
|
@ -11,6 +11,7 @@ import 'package:solian/exceptions/request.dart';
|
|||||||
import 'package:solian/exceptions/unauthorized.dart';
|
import 'package:solian/exceptions/unauthorized.dart';
|
||||||
import 'package:solian/models/auth.dart';
|
import 'package:solian/models/auth.dart';
|
||||||
import 'package:solian/providers/database/database.dart';
|
import 'package:solian/providers/database/database.dart';
|
||||||
|
import 'package:solian/providers/notifications.dart';
|
||||||
import 'package:solian/providers/websocket.dart';
|
import 'package:solian/providers/websocket.dart';
|
||||||
import 'package:solian/services.dart';
|
import 'package:solian/services.dart';
|
||||||
|
|
||||||
@ -174,7 +175,7 @@ class AuthProvider extends GetConnect {
|
|||||||
);
|
);
|
||||||
|
|
||||||
Get.find<WebSocketProvider>().connect();
|
Get.find<WebSocketProvider>().connect();
|
||||||
Get.find<WebSocketProvider>().notifyPrefetch();
|
Get.find<NotificationProvider>().fetchNotification();
|
||||||
|
|
||||||
return credentials!;
|
return credentials!;
|
||||||
}
|
}
|
||||||
@ -184,8 +185,8 @@ class AuthProvider extends GetConnect {
|
|||||||
userProfile.value = null;
|
userProfile.value = null;
|
||||||
|
|
||||||
Get.find<WebSocketProvider>().disconnect();
|
Get.find<WebSocketProvider>().disconnect();
|
||||||
Get.find<WebSocketProvider>().notifications.clear();
|
Get.find<NotificationProvider>().notifications.clear();
|
||||||
Get.find<WebSocketProvider>().notificationUnread.value = 0;
|
Get.find<NotificationProvider>().notificationUnread.value = 0;
|
||||||
|
|
||||||
AppDatabase.removeDatabase();
|
AppDatabase.removeDatabase();
|
||||||
autoStopBackgroundNotificationService();
|
autoStopBackgroundNotificationService();
|
||||||
|
@ -23,6 +23,21 @@ class AttachmentProvider extends GetConnect {
|
|||||||
|
|
||||||
final Map<String, Attachment> _cachedResponses = {};
|
final Map<String, Attachment> _cachedResponses = {};
|
||||||
|
|
||||||
|
List<Attachment?> listMetadataFromCache(List<String> rid) {
|
||||||
|
if (rid.isEmpty) return List.empty();
|
||||||
|
|
||||||
|
List<Attachment?> result = List.filled(rid.length, null);
|
||||||
|
for (var idx = 0; idx < rid.length; idx++) {
|
||||||
|
if (_cachedResponses.containsKey(rid[idx])) {
|
||||||
|
result[idx] = _cachedResponses[rid[idx]];
|
||||||
|
} else {
|
||||||
|
result[idx] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
Future<List<Attachment?>> listMetadata(
|
Future<List<Attachment?>> listMetadata(
|
||||||
List<String> rid, {
|
List<String> rid, {
|
||||||
noCache = false,
|
noCache = false,
|
||||||
|
@ -44,9 +44,33 @@ class PostProvider extends GetxController {
|
|||||||
final queries = [
|
final queries = [
|
||||||
'take=${10}',
|
'take=${10}',
|
||||||
'offset=$page',
|
'offset=$page',
|
||||||
|
'truncate=false',
|
||||||
];
|
];
|
||||||
final client = await auth.configureClient('interactive');
|
final client = await auth.configureClient('interactive');
|
||||||
final resp = await client.get('/posts/drafts?${queries.join('&')}');
|
final resp = await client.get(
|
||||||
|
'/posts/drafts?${queries.join('&')}',
|
||||||
|
);
|
||||||
|
if (resp.statusCode != 200) {
|
||||||
|
throw RequestException(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Response> searchPost(String probe, int page,
|
||||||
|
{String? realm, String? author, tag, category, int take = 10}) async {
|
||||||
|
final queries = [
|
||||||
|
'probe=$probe',
|
||||||
|
'take=$take',
|
||||||
|
'offset=$page',
|
||||||
|
if (tag != null) 'tag=$tag',
|
||||||
|
if (category != null) 'category=$category',
|
||||||
|
if (author != null) 'author=$author',
|
||||||
|
if (realm != null) 'realm=$realm',
|
||||||
|
];
|
||||||
|
final AuthProvider auth = Get.find();
|
||||||
|
final client = await auth.configureClient('co');
|
||||||
|
final resp = await client.get('/posts/search?${queries.join('&')}');
|
||||||
if (resp.statusCode != 200) {
|
if (resp.statusCode != 200) {
|
||||||
throw RequestException(resp);
|
throw RequestException(resp);
|
||||||
}
|
}
|
||||||
|
@ -299,53 +299,71 @@ typedef $$LocalMessageEventTableTableUpdateCompanionBuilder
|
|||||||
});
|
});
|
||||||
|
|
||||||
class $$LocalMessageEventTableTableFilterComposer
|
class $$LocalMessageEventTableTableFilterComposer
|
||||||
extends FilterComposer<_$AppDatabase, $LocalMessageEventTableTable> {
|
extends Composer<_$AppDatabase, $LocalMessageEventTableTable> {
|
||||||
$$LocalMessageEventTableTableFilterComposer(super.$state);
|
$$LocalMessageEventTableTableFilterComposer({
|
||||||
ColumnFilters<int> get id => $state.composableBuilder(
|
required super.$db,
|
||||||
column: $state.table.id,
|
required super.$table,
|
||||||
builder: (column, joinBuilders) =>
|
super.joinBuilder,
|
||||||
ColumnFilters(column, joinBuilders: joinBuilders));
|
super.$addJoinBuilderToRootComposer,
|
||||||
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
|
});
|
||||||
|
ColumnFilters<int> get id => $composableBuilder(
|
||||||
|
column: $table.id, builder: (column) => ColumnFilters(column));
|
||||||
|
|
||||||
ColumnFilters<int> get channelId => $state.composableBuilder(
|
ColumnFilters<int> get channelId => $composableBuilder(
|
||||||
column: $state.table.channelId,
|
column: $table.channelId, builder: (column) => ColumnFilters(column));
|
||||||
builder: (column, joinBuilders) =>
|
|
||||||
ColumnFilters(column, joinBuilders: joinBuilders));
|
|
||||||
|
|
||||||
ColumnWithTypeConverterFilters<Event?, Event, String> get data =>
|
ColumnWithTypeConverterFilters<Event?, Event, String> get data =>
|
||||||
$state.composableBuilder(
|
$composableBuilder(
|
||||||
column: $state.table.data,
|
column: $table.data,
|
||||||
builder: (column, joinBuilders) => ColumnWithTypeConverterFilters(
|
builder: (column) => ColumnWithTypeConverterFilters(column));
|
||||||
column,
|
|
||||||
joinBuilders: joinBuilders));
|
|
||||||
|
|
||||||
ColumnFilters<DateTime> get createdAt => $state.composableBuilder(
|
ColumnFilters<DateTime> get createdAt => $composableBuilder(
|
||||||
column: $state.table.createdAt,
|
column: $table.createdAt, builder: (column) => ColumnFilters(column));
|
||||||
builder: (column, joinBuilders) =>
|
|
||||||
ColumnFilters(column, joinBuilders: joinBuilders));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class $$LocalMessageEventTableTableOrderingComposer
|
class $$LocalMessageEventTableTableOrderingComposer
|
||||||
extends OrderingComposer<_$AppDatabase, $LocalMessageEventTableTable> {
|
extends Composer<_$AppDatabase, $LocalMessageEventTableTable> {
|
||||||
$$LocalMessageEventTableTableOrderingComposer(super.$state);
|
$$LocalMessageEventTableTableOrderingComposer({
|
||||||
ColumnOrderings<int> get id => $state.composableBuilder(
|
required super.$db,
|
||||||
column: $state.table.id,
|
required super.$table,
|
||||||
builder: (column, joinBuilders) =>
|
super.joinBuilder,
|
||||||
ColumnOrderings(column, joinBuilders: joinBuilders));
|
super.$addJoinBuilderToRootComposer,
|
||||||
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
|
});
|
||||||
|
ColumnOrderings<int> get id => $composableBuilder(
|
||||||
|
column: $table.id, builder: (column) => ColumnOrderings(column));
|
||||||
|
|
||||||
ColumnOrderings<int> get channelId => $state.composableBuilder(
|
ColumnOrderings<int> get channelId => $composableBuilder(
|
||||||
column: $state.table.channelId,
|
column: $table.channelId, builder: (column) => ColumnOrderings(column));
|
||||||
builder: (column, joinBuilders) =>
|
|
||||||
ColumnOrderings(column, joinBuilders: joinBuilders));
|
|
||||||
|
|
||||||
ColumnOrderings<String> get data => $state.composableBuilder(
|
ColumnOrderings<String> get data => $composableBuilder(
|
||||||
column: $state.table.data,
|
column: $table.data, builder: (column) => ColumnOrderings(column));
|
||||||
builder: (column, joinBuilders) =>
|
|
||||||
ColumnOrderings(column, joinBuilders: joinBuilders));
|
|
||||||
|
|
||||||
ColumnOrderings<DateTime> get createdAt => $state.composableBuilder(
|
ColumnOrderings<DateTime> get createdAt => $composableBuilder(
|
||||||
column: $state.table.createdAt,
|
column: $table.createdAt, builder: (column) => ColumnOrderings(column));
|
||||||
builder: (column, joinBuilders) =>
|
}
|
||||||
ColumnOrderings(column, joinBuilders: joinBuilders));
|
|
||||||
|
class $$LocalMessageEventTableTableAnnotationComposer
|
||||||
|
extends Composer<_$AppDatabase, $LocalMessageEventTableTable> {
|
||||||
|
$$LocalMessageEventTableTableAnnotationComposer({
|
||||||
|
required super.$db,
|
||||||
|
required super.$table,
|
||||||
|
super.joinBuilder,
|
||||||
|
super.$addJoinBuilderToRootComposer,
|
||||||
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
|
});
|
||||||
|
GeneratedColumn<int> get id =>
|
||||||
|
$composableBuilder(column: $table.id, builder: (column) => column);
|
||||||
|
|
||||||
|
GeneratedColumn<int> get channelId =>
|
||||||
|
$composableBuilder(column: $table.channelId, builder: (column) => column);
|
||||||
|
|
||||||
|
GeneratedColumnWithTypeConverter<Event?, String> get data =>
|
||||||
|
$composableBuilder(column: $table.data, builder: (column) => column);
|
||||||
|
|
||||||
|
GeneratedColumn<DateTime> get createdAt =>
|
||||||
|
$composableBuilder(column: $table.createdAt, builder: (column) => column);
|
||||||
}
|
}
|
||||||
|
|
||||||
class $$LocalMessageEventTableTableTableManager extends RootTableManager<
|
class $$LocalMessageEventTableTableTableManager extends RootTableManager<
|
||||||
@ -354,6 +372,7 @@ class $$LocalMessageEventTableTableTableManager extends RootTableManager<
|
|||||||
LocalMessageEventTableData,
|
LocalMessageEventTableData,
|
||||||
$$LocalMessageEventTableTableFilterComposer,
|
$$LocalMessageEventTableTableFilterComposer,
|
||||||
$$LocalMessageEventTableTableOrderingComposer,
|
$$LocalMessageEventTableTableOrderingComposer,
|
||||||
|
$$LocalMessageEventTableTableAnnotationComposer,
|
||||||
$$LocalMessageEventTableTableCreateCompanionBuilder,
|
$$LocalMessageEventTableTableCreateCompanionBuilder,
|
||||||
$$LocalMessageEventTableTableUpdateCompanionBuilder,
|
$$LocalMessageEventTableTableUpdateCompanionBuilder,
|
||||||
(
|
(
|
||||||
@ -368,10 +387,15 @@ class $$LocalMessageEventTableTableTableManager extends RootTableManager<
|
|||||||
: super(TableManagerState(
|
: super(TableManagerState(
|
||||||
db: db,
|
db: db,
|
||||||
table: table,
|
table: table,
|
||||||
filteringComposer: $$LocalMessageEventTableTableFilterComposer(
|
createFilteringComposer: () =>
|
||||||
ComposerState(db, table)),
|
$$LocalMessageEventTableTableFilterComposer(
|
||||||
orderingComposer: $$LocalMessageEventTableTableOrderingComposer(
|
$db: db, $table: table),
|
||||||
ComposerState(db, table)),
|
createOrderingComposer: () =>
|
||||||
|
$$LocalMessageEventTableTableOrderingComposer(
|
||||||
|
$db: db, $table: table),
|
||||||
|
createComputedFieldComposer: () =>
|
||||||
|
$$LocalMessageEventTableTableAnnotationComposer(
|
||||||
|
$db: db, $table: table),
|
||||||
updateCompanionCallback: ({
|
updateCompanionCallback: ({
|
||||||
Value<int> id = const Value.absent(),
|
Value<int> id = const Value.absent(),
|
||||||
Value<int> channelId = const Value.absent(),
|
Value<int> channelId = const Value.absent(),
|
||||||
@ -410,6 +434,7 @@ typedef $$LocalMessageEventTableTableProcessedTableManager
|
|||||||
LocalMessageEventTableData,
|
LocalMessageEventTableData,
|
||||||
$$LocalMessageEventTableTableFilterComposer,
|
$$LocalMessageEventTableTableFilterComposer,
|
||||||
$$LocalMessageEventTableTableOrderingComposer,
|
$$LocalMessageEventTableTableOrderingComposer,
|
||||||
|
$$LocalMessageEventTableTableAnnotationComposer,
|
||||||
$$LocalMessageEventTableTableCreateCompanionBuilder,
|
$$LocalMessageEventTableTableCreateCompanionBuilder,
|
||||||
$$LocalMessageEventTableTableUpdateCompanionBuilder,
|
$$LocalMessageEventTableTableUpdateCompanionBuilder,
|
||||||
(
|
(
|
||||||
|
@ -4,19 +4,19 @@ import 'package:intl/intl.dart';
|
|||||||
class ExperienceProvider extends GetxController {
|
class ExperienceProvider extends GetxController {
|
||||||
static List<int> experienceToLevelRequirements = [
|
static List<int> experienceToLevelRequirements = [
|
||||||
0, // Level 0
|
0, // Level 0
|
||||||
100, // Level 1
|
1000, // Level 1
|
||||||
400, // Level 2
|
4000, // Level 2
|
||||||
900, // Level 3
|
9000, // Level 3
|
||||||
1600, // Level 4
|
16000, // Level 4
|
||||||
2500, // Level 5
|
25000, // Level 5
|
||||||
3600, // Level 6
|
36000, // Level 6
|
||||||
4900, // Level 7
|
49000, // Level 7
|
||||||
6400, // Level 8
|
64000, // Level 8
|
||||||
8100, // Level 9
|
81000, // Level 9
|
||||||
10000, // Level 10
|
100000, // Level 10
|
||||||
12100, // Level 11
|
121000, // Level 11
|
||||||
14400, // Level 12
|
144000, // Level 12
|
||||||
36800 // Level 13
|
368000 // Level 13
|
||||||
];
|
];
|
||||||
|
|
||||||
static List<String> levelLabelMapping =
|
static List<String> levelLabelMapping =
|
||||||
@ -35,7 +35,7 @@ class ExperienceProvider extends GetxController {
|
|||||||
final idx = experienceToLevelRequirements.indexOf(exp);
|
final idx = experienceToLevelRequirements.indexOf(exp);
|
||||||
if (idx + 1 >= experienceToLevelRequirements.length) return 1;
|
if (idx + 1 >= experienceToLevelRequirements.length) return 1;
|
||||||
final nextExp = experienceToLevelRequirements[idx + 1];
|
final nextExp = experienceToLevelRequirements[idx + 1];
|
||||||
return exp / nextExp;
|
return (experience - exp).abs() / (exp - nextExp).abs();
|
||||||
}
|
}
|
||||||
|
|
||||||
static String calcLevelUpProgressLevel(int experience) {
|
static String calcLevelUpProgressLevel(int experience) {
|
||||||
@ -43,9 +43,9 @@ class ExperienceProvider extends GetxController {
|
|||||||
.firstWhere((x) => x <= experience);
|
.firstWhere((x) => x <= experience);
|
||||||
final idx = experienceToLevelRequirements.indexOf(exp);
|
final idx = experienceToLevelRequirements.indexOf(exp);
|
||||||
if (idx + 1 >= experienceToLevelRequirements.length) return 'Infinity';
|
if (idx + 1 >= experienceToLevelRequirements.length) return 'Infinity';
|
||||||
final nextExp = experienceToLevelRequirements[idx + 1];
|
final nextExp = exp - experienceToLevelRequirements[idx + 1];
|
||||||
final formatter =
|
final formatter =
|
||||||
NumberFormat.compactCurrency(symbol: '', decimalDigits: 1);
|
NumberFormat.compactCurrency(symbol: '', decimalDigits: 1);
|
||||||
return '${formatter.format(exp)}/${formatter.format(nextExp)}';
|
return '${formatter.format((exp - experience).abs())}/${formatter.format(nextExp.abs())}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
175
lib/providers/notifications.dart
Normal file
175
lib/providers/notifications.dart
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
import 'dart:developer';
|
||||||
|
|
||||||
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||||
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||||
|
import 'package:flutter_udid/flutter_udid.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:solian/exceptions/request.dart';
|
||||||
|
import 'package:solian/models/notification.dart';
|
||||||
|
import 'package:solian/models/pagination.dart';
|
||||||
|
import 'package:solian/platform.dart';
|
||||||
|
import 'package:solian/providers/auth.dart';
|
||||||
|
|
||||||
|
class NotificationProvider extends GetxController {
|
||||||
|
RxBool isBusy = false.obs;
|
||||||
|
|
||||||
|
RxInt notificationUnread = 0.obs;
|
||||||
|
RxList<Notification> notifications =
|
||||||
|
List<Notification>.empty(growable: true).obs;
|
||||||
|
|
||||||
|
Future<void> fetchNotification() async {
|
||||||
|
final AuthProvider auth = Get.find();
|
||||||
|
if (auth.isAuthorized.isFalse) return;
|
||||||
|
|
||||||
|
final client = await auth.configureClient('auth');
|
||||||
|
|
||||||
|
final resp = await client.get('/notifications?skip=0&take=100');
|
||||||
|
if (resp.statusCode == 200) {
|
||||||
|
final result = PaginationResult.fromJson(resp.body);
|
||||||
|
final data = result.data?.map((x) => Notification.fromJson(x)).toList();
|
||||||
|
if (data != null) {
|
||||||
|
notifications.addAll(data);
|
||||||
|
notificationUnread.value = data.where((x) => x.readAt == null).length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> markAllRead() async {
|
||||||
|
final AuthProvider auth = Get.find();
|
||||||
|
if (auth.isAuthorized.isFalse) return;
|
||||||
|
|
||||||
|
isBusy.value = true;
|
||||||
|
|
||||||
|
final NotificationProvider nty = Get.find();
|
||||||
|
|
||||||
|
List<int> markList = List.empty(growable: true);
|
||||||
|
for (final element in nty.notifications) {
|
||||||
|
if (element.id <= 0) continue;
|
||||||
|
if (element.readAt != null) continue;
|
||||||
|
markList.add(element.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (markList.isNotEmpty) {
|
||||||
|
final client = await auth.configureClient('auth');
|
||||||
|
await client.put('/notifications/read', {'messages': markList});
|
||||||
|
}
|
||||||
|
|
||||||
|
nty.notifications.value = nty.notifications.map((x) {
|
||||||
|
x.readAt = DateTime.now();
|
||||||
|
return x;
|
||||||
|
}).toList();
|
||||||
|
nty.notifications.refresh();
|
||||||
|
|
||||||
|
isBusy.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> markOneRead(Notification element, int index) async {
|
||||||
|
final AuthProvider auth = Get.find();
|
||||||
|
if (auth.isAuthorized.isFalse) return;
|
||||||
|
|
||||||
|
final NotificationProvider nty = Get.find();
|
||||||
|
|
||||||
|
if (element.id <= 0) {
|
||||||
|
nty.notifications.removeAt(index);
|
||||||
|
return;
|
||||||
|
} else if (element.readAt != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isBusy.value = true;
|
||||||
|
|
||||||
|
final client = await auth.configureClient('auth');
|
||||||
|
|
||||||
|
await client.put('/notifications/read/${element.id}', {});
|
||||||
|
|
||||||
|
nty.notifications[0].readAt = DateTime.now();
|
||||||
|
nty.notifications.refresh();
|
||||||
|
|
||||||
|
isBusy.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void requestPermissions() {
|
||||||
|
try {
|
||||||
|
FirebaseMessaging.instance.requestPermission(
|
||||||
|
alert: true,
|
||||||
|
announcement: true,
|
||||||
|
carPlay: true,
|
||||||
|
badge: true,
|
||||||
|
sound: true);
|
||||||
|
} catch (_) {
|
||||||
|
// When firebase isn't initialized (background service)
|
||||||
|
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
|
||||||
|
FlutterLocalNotificationsPlugin();
|
||||||
|
flutterLocalNotificationsPlugin
|
||||||
|
.resolvePlatformSpecificImplementation<
|
||||||
|
AndroidFlutterLocalNotificationsPlugin>()
|
||||||
|
?.requestNotificationsPermission();
|
||||||
|
flutterLocalNotificationsPlugin
|
||||||
|
.resolvePlatformSpecificImplementation<
|
||||||
|
IOSFlutterLocalNotificationsPlugin>()
|
||||||
|
?.requestPermissions(
|
||||||
|
alert: true,
|
||||||
|
badge: true,
|
||||||
|
sound: true,
|
||||||
|
);
|
||||||
|
flutterLocalNotificationsPlugin
|
||||||
|
.resolvePlatformSpecificImplementation<
|
||||||
|
MacOSFlutterLocalNotificationsPlugin>()
|
||||||
|
?.requestPermissions(
|
||||||
|
alert: true,
|
||||||
|
badge: true,
|
||||||
|
sound: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> registerPushNotifications() async {
|
||||||
|
if (PlatformInfo.isWeb) return;
|
||||||
|
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
if (prefs.getBool('service_background_notification') == true) {
|
||||||
|
log('Background notification service has been enabled, skip register push notifications');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final AuthProvider auth = Get.find();
|
||||||
|
if (auth.isAuthorized.isFalse) return;
|
||||||
|
|
||||||
|
late final String? token;
|
||||||
|
late final String provider;
|
||||||
|
var deviceUuid = await _getDeviceUuid();
|
||||||
|
|
||||||
|
if (deviceUuid == null || deviceUuid.isEmpty) {
|
||||||
|
log("Unable to active push notifications, couldn't get device uuid");
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
log('Device UUID is $deviceUuid');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PlatformInfo.isIOS || PlatformInfo.isMacOS) {
|
||||||
|
provider = 'apple';
|
||||||
|
token = await FirebaseMessaging.instance.getAPNSToken();
|
||||||
|
} else {
|
||||||
|
provider = 'firebase';
|
||||||
|
token = await FirebaseMessaging.instance.getToken();
|
||||||
|
}
|
||||||
|
log('Device Push Token is $token');
|
||||||
|
|
||||||
|
final client = await auth.configureClient('auth');
|
||||||
|
|
||||||
|
final resp = await client.post('/notifications/subscribe', {
|
||||||
|
'provider': provider,
|
||||||
|
'device_token': token,
|
||||||
|
'device_id': deviceUuid,
|
||||||
|
});
|
||||||
|
if (resp.statusCode != 200 && resp.statusCode != 400) {
|
||||||
|
throw RequestException(resp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> _getDeviceUuid() async {
|
||||||
|
if (PlatformInfo.isWeb) return null;
|
||||||
|
return await FlutterUdid.consistentUdid;
|
||||||
|
}
|
||||||
|
}
|
@ -3,17 +3,11 @@ import 'dart:convert';
|
|||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:device_info_plus/device_info_plus.dart';
|
|
||||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
|
||||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
|
||||||
import 'package:solian/exceptions/request.dart';
|
|
||||||
import 'package:solian/models/notification.dart';
|
import 'package:solian/models/notification.dart';
|
||||||
import 'package:solian/models/packet.dart';
|
import 'package:solian/models/packet.dart';
|
||||||
import 'package:solian/models/pagination.dart';
|
|
||||||
import 'package:solian/platform.dart';
|
|
||||||
import 'package:solian/providers/auth.dart';
|
import 'package:solian/providers/auth.dart';
|
||||||
|
import 'package:solian/providers/notifications.dart';
|
||||||
import 'package:solian/services.dart';
|
import 'package:solian/services.dart';
|
||||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||||
|
|
||||||
@ -21,56 +15,10 @@ class WebSocketProvider extends GetxController {
|
|||||||
RxBool isConnected = false.obs;
|
RxBool isConnected = false.obs;
|
||||||
RxBool isConnecting = false.obs;
|
RxBool isConnecting = false.obs;
|
||||||
|
|
||||||
RxInt notificationUnread = 0.obs;
|
|
||||||
RxList<Notification> notifications =
|
|
||||||
List<Notification>.empty(growable: true).obs;
|
|
||||||
|
|
||||||
WebSocketChannel? websocket;
|
WebSocketChannel? websocket;
|
||||||
|
|
||||||
StreamController<NetworkPackage> stream = StreamController.broadcast();
|
StreamController<NetworkPackage> stream = StreamController.broadcast();
|
||||||
|
|
||||||
@override
|
|
||||||
onInit() {
|
|
||||||
notifyPrefetch();
|
|
||||||
|
|
||||||
super.onInit();
|
|
||||||
}
|
|
||||||
|
|
||||||
void requestPermissions() {
|
|
||||||
try {
|
|
||||||
FirebaseMessaging.instance.requestPermission(
|
|
||||||
alert: true,
|
|
||||||
announcement: true,
|
|
||||||
carPlay: true,
|
|
||||||
badge: true,
|
|
||||||
sound: true);
|
|
||||||
} catch (_) {
|
|
||||||
// When firebase isn't initialized (background service)
|
|
||||||
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
|
|
||||||
FlutterLocalNotificationsPlugin();
|
|
||||||
flutterLocalNotificationsPlugin
|
|
||||||
.resolvePlatformSpecificImplementation<
|
|
||||||
AndroidFlutterLocalNotificationsPlugin>()
|
|
||||||
?.requestNotificationsPermission();
|
|
||||||
flutterLocalNotificationsPlugin
|
|
||||||
.resolvePlatformSpecificImplementation<
|
|
||||||
IOSFlutterLocalNotificationsPlugin>()
|
|
||||||
?.requestPermissions(
|
|
||||||
alert: true,
|
|
||||||
badge: true,
|
|
||||||
sound: true,
|
|
||||||
);
|
|
||||||
flutterLocalNotificationsPlugin
|
|
||||||
.resolvePlatformSpecificImplementation<
|
|
||||||
MacOSFlutterLocalNotificationsPlugin>()
|
|
||||||
?.requestPermissions(
|
|
||||||
alert: true,
|
|
||||||
badge: true,
|
|
||||||
sound: true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> connect({noRetry = false}) async {
|
Future<void> connect({noRetry = false}) async {
|
||||||
if (isConnected.value) {
|
if (isConnected.value) {
|
||||||
return;
|
return;
|
||||||
@ -119,8 +67,9 @@ class WebSocketProvider extends GetxController {
|
|||||||
log('Websocket incoming message: ${packet.method} ${packet.message}');
|
log('Websocket incoming message: ${packet.method} ${packet.message}');
|
||||||
stream.sink.add(packet);
|
stream.sink.add(packet);
|
||||||
if (packet.method == 'notifications.new') {
|
if (packet.method == 'notifications.new') {
|
||||||
notifications.add(Notification.fromJson(packet.payload!));
|
final NotificationProvider nty = Get.find();
|
||||||
notificationUnread.value++;
|
nty.notifications.add(Notification.fromJson(packet.payload!));
|
||||||
|
nty.notificationUnread.value++;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDone: () {
|
onDone: () {
|
||||||
@ -133,95 +82,4 @@ class WebSocketProvider extends GetxController {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> notifyPrefetch() async {
|
|
||||||
final AuthProvider auth = Get.find();
|
|
||||||
if (auth.isAuthorized.isFalse) return;
|
|
||||||
|
|
||||||
final client = await auth.configureClient('auth');
|
|
||||||
|
|
||||||
final resp = await client.get('/notifications?skip=0&take=100');
|
|
||||||
if (resp.statusCode == 200) {
|
|
||||||
final result = PaginationResult.fromJson(resp.body);
|
|
||||||
final data = result.data?.map((x) => Notification.fromJson(x)).toList();
|
|
||||||
if (data != null) {
|
|
||||||
notifications.addAll(data);
|
|
||||||
notificationUnread.value = data.length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> registerPushNotifications() async {
|
|
||||||
if (PlatformInfo.isWeb) return;
|
|
||||||
|
|
||||||
final prefs = await SharedPreferences.getInstance();
|
|
||||||
if (prefs.getBool('service_background_notification') == true) {
|
|
||||||
log('Background notification service has been enabled, skip register push notifications');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final AuthProvider auth = Get.find();
|
|
||||||
if (auth.isAuthorized.isFalse) return;
|
|
||||||
|
|
||||||
late final String? token;
|
|
||||||
late final String provider;
|
|
||||||
final deviceUuid = await _getDeviceUuid();
|
|
||||||
|
|
||||||
if (deviceUuid == null || deviceUuid.isEmpty) {
|
|
||||||
log("Unable to active push notifications, couldn't get device uuid");
|
|
||||||
} else {
|
|
||||||
log('Device UUID is $deviceUuid');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (PlatformInfo.isIOS || PlatformInfo.isMacOS) {
|
|
||||||
provider = 'apple';
|
|
||||||
token = await FirebaseMessaging.instance.getAPNSToken();
|
|
||||||
} else {
|
|
||||||
provider = 'firebase';
|
|
||||||
token = await FirebaseMessaging.instance.getToken();
|
|
||||||
}
|
|
||||||
log('Device Push Token is $token');
|
|
||||||
|
|
||||||
final client = await auth.configureClient('auth');
|
|
||||||
|
|
||||||
final resp = await client.post('/notifications/subscribe', {
|
|
||||||
'provider': provider,
|
|
||||||
'device_token': token,
|
|
||||||
'device_id': deviceUuid,
|
|
||||||
});
|
|
||||||
if (resp.statusCode != 200 && resp.statusCode != 400) {
|
|
||||||
throw RequestException(resp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<String?> _getDeviceUuid() async {
|
|
||||||
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
|
|
||||||
if (PlatformInfo.isWeb) {
|
|
||||||
final webInfo = await deviceInfo.webBrowserInfo;
|
|
||||||
return webInfo.vendor! +
|
|
||||||
webInfo.userAgent! +
|
|
||||||
webInfo.hardwareConcurrency.toString();
|
|
||||||
}
|
|
||||||
if (PlatformInfo.isAndroid) {
|
|
||||||
final androidInfo = await deviceInfo.androidInfo;
|
|
||||||
return androidInfo.id;
|
|
||||||
}
|
|
||||||
if (PlatformInfo.isIOS) {
|
|
||||||
final iosInfo = await deviceInfo.iosInfo;
|
|
||||||
return iosInfo.identifierForVendor!;
|
|
||||||
}
|
|
||||||
if (PlatformInfo.isLinux) {
|
|
||||||
final linuxInfo = await deviceInfo.linuxInfo;
|
|
||||||
return linuxInfo.machineId!;
|
|
||||||
}
|
|
||||||
if (PlatformInfo.isWindows) {
|
|
||||||
final windowsInfo = await deviceInfo.windowsInfo;
|
|
||||||
return windowsInfo.deviceId;
|
|
||||||
}
|
|
||||||
if (PlatformInfo.isMacOS) {
|
|
||||||
final macosInfo = await deviceInfo.macOsInfo;
|
|
||||||
return macosInfo.systemGUID;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -2,11 +2,14 @@ import 'package:animations/animations.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/bootstrapper.dart';
|
import 'package:solian/bootstrapper.dart';
|
||||||
|
import 'package:solian/models/post.dart';
|
||||||
import 'package:solian/models/realm.dart';
|
import 'package:solian/models/realm.dart';
|
||||||
import 'package:solian/screens/about.dart';
|
import 'package:solian/screens/about.dart';
|
||||||
import 'package:solian/screens/account.dart';
|
import 'package:solian/screens/account.dart';
|
||||||
|
import 'package:solian/screens/account/audit_log.dart';
|
||||||
import 'package:solian/screens/account/friend.dart';
|
import 'package:solian/screens/account/friend.dart';
|
||||||
import 'package:solian/screens/account/preferences/notifications.dart';
|
import 'package:solian/screens/account/preferences/notifications.dart';
|
||||||
|
import 'package:solian/screens/account/preferences/security.dart';
|
||||||
import 'package:solian/screens/account/profile_edit.dart';
|
import 'package:solian/screens/account/profile_edit.dart';
|
||||||
import 'package:solian/screens/account/profile_page.dart';
|
import 'package:solian/screens/account/profile_page.dart';
|
||||||
import 'package:solian/screens/auth/signin.dart';
|
import 'package:solian/screens/auth/signin.dart';
|
||||||
@ -16,9 +19,9 @@ import 'package:solian/screens/channel/channel_detail.dart';
|
|||||||
import 'package:solian/screens/channel/channel_organize.dart';
|
import 'package:solian/screens/channel/channel_organize.dart';
|
||||||
import 'package:solian/screens/chat.dart';
|
import 'package:solian/screens/chat.dart';
|
||||||
import 'package:solian/screens/dashboard.dart';
|
import 'package:solian/screens/dashboard.dart';
|
||||||
import 'package:solian/screens/feed/search.dart';
|
import 'package:solian/screens/posts/post_search.dart';
|
||||||
import 'package:solian/screens/posts/post_detail.dart';
|
import 'package:solian/screens/posts/post_detail.dart';
|
||||||
import 'package:solian/screens/feed/draft_box.dart';
|
import 'package:solian/screens/posts/draft_box.dart';
|
||||||
import 'package:solian/screens/realms.dart';
|
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';
|
||||||
@ -94,7 +97,7 @@ abstract class AppRouter {
|
|||||||
name: 'postSearch',
|
name: 'postSearch',
|
||||||
builder: (context, state) => TitleShell(
|
builder: (context, state) => TitleShell(
|
||||||
state: state,
|
state: state,
|
||||||
child: FeedSearchScreen(
|
child: PostSearchScreen(
|
||||||
tag: state.uri.queryParameters['tag'],
|
tag: state.uri.queryParameters['tag'],
|
||||||
category: state.uri.queryParameters['category'],
|
category: state.uri.queryParameters['category'],
|
||||||
),
|
),
|
||||||
@ -107,6 +110,7 @@ abstract class AppRouter {
|
|||||||
state: state,
|
state: state,
|
||||||
child: PostDetailScreen(
|
child: PostDetailScreen(
|
||||||
id: state.pathParameters['id']!,
|
id: state.pathParameters['id']!,
|
||||||
|
post: state.extra as Post?,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -264,6 +268,22 @@ abstract class AppRouter {
|
|||||||
child: const NotificationPreferencesScreen(),
|
child: const NotificationPreferencesScreen(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/account/preferences/auth',
|
||||||
|
name: 'authPreferences',
|
||||||
|
builder: (context, state) => TitleShell(
|
||||||
|
state: state,
|
||||||
|
child: const AuthPreferencesScreen(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/account/audit',
|
||||||
|
name: 'auditLog',
|
||||||
|
builder: (context, state) => TitleShell(
|
||||||
|
state: state,
|
||||||
|
child: const AuditLogScreen(),
|
||||||
|
),
|
||||||
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/account/view/:name',
|
path: '/account/view/:name',
|
||||||
name: 'accountProfilePage',
|
name: 'accountProfilePage',
|
||||||
|
@ -129,6 +129,24 @@ class _AccountScreenState extends State<AccountScreen> {
|
|||||||
AppRouter.instance.pushNamed('settings');
|
AppRouter.instance.pushNamed('settings');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
if (auth.isAuthorized.value)
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.event_repeat),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
|
||||||
|
title: Text('auditLog'.tr),
|
||||||
|
onTap: () {
|
||||||
|
AppRouter.instance.pushNamed('auditLog');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (auth.isAuthorized.value)
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.lock),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
|
||||||
|
title: Text('authPreferences'.tr),
|
||||||
|
onTap: () {
|
||||||
|
AppRouter.instance.pushNamed('authPreferences');
|
||||||
|
},
|
||||||
|
),
|
||||||
if (auth.isAuthorized.value)
|
if (auth.isAuthorized.value)
|
||||||
ListTile(
|
ListTile(
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
|
||||||
|
154
lib/screens/account/audit_log.dart
Normal file
154
lib/screens/account/audit_log.dart
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
import 'package:marquee/marquee.dart';
|
||||||
|
import 'package:solian/exceptions/request.dart';
|
||||||
|
import 'package:solian/exts.dart';
|
||||||
|
import 'package:solian/models/audit_log.dart';
|
||||||
|
import 'package:solian/models/pagination.dart';
|
||||||
|
import 'package:solian/providers/auth.dart';
|
||||||
|
import 'package:solian/widgets/relative_date.dart';
|
||||||
|
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||||
|
import 'package:timeline_tile/timeline_tile.dart';
|
||||||
|
|
||||||
|
class AuditLogScreen extends StatefulWidget {
|
||||||
|
const AuditLogScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AuditLogScreen> createState() => _AuditLogScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AuditLogScreenState extends State<AuditLogScreen> {
|
||||||
|
bool _isBusy = true;
|
||||||
|
|
||||||
|
final List<AuditEvent> _events = List.empty(growable: true);
|
||||||
|
|
||||||
|
Future<void> _getEvents() async {
|
||||||
|
if (!_isBusy) setState(() => _isBusy = true);
|
||||||
|
|
||||||
|
final AuthProvider auth = Get.find();
|
||||||
|
final client = await auth.configureClient('id');
|
||||||
|
final resp =
|
||||||
|
await client.get('/users/me/events?take=15&offset=${_events.length}');
|
||||||
|
if (resp.statusCode != 200) {
|
||||||
|
context.showErrorDialog(RequestException(resp));
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = PaginationResult.fromJson(resp.body);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_events.addAll(
|
||||||
|
result.data?.map((x) => AuditEvent.fromJson(x)).toList() ??
|
||||||
|
List.empty(),
|
||||||
|
);
|
||||||
|
_isBusy = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _showIp = false;
|
||||||
|
|
||||||
|
String _censorIpAddress(String ip) {
|
||||||
|
List<String> parts = ip.split('.');
|
||||||
|
|
||||||
|
if (parts.length == 4) {
|
||||||
|
String censoredPart1 = '*' * parts[1].length;
|
||||||
|
String censoredPart2 = '*' * parts[2].length;
|
||||||
|
String censoredPart3 = '*' * parts[3].length;
|
||||||
|
|
||||||
|
return '${parts[0]}.$censoredPart1.$censoredPart2.$censoredPart3';
|
||||||
|
} else {
|
||||||
|
return '***.***.***.***';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_getEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
CheckboxListTile(
|
||||||
|
value: _showIp,
|
||||||
|
title: Text('showIp'.tr),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
secondary: const Icon(Icons.alternate_email),
|
||||||
|
tileColor:
|
||||||
|
Theme.of(context).colorScheme.surfaceContainer.withOpacity(0.5),
|
||||||
|
onChanged: (val) {
|
||||||
|
setState(() => _showIp = val ?? false);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: RefreshIndicator(
|
||||||
|
onRefresh: () {
|
||||||
|
_events.clear();
|
||||||
|
return _getEvents();
|
||||||
|
},
|
||||||
|
child: InfiniteList(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
itemCount: _events.length,
|
||||||
|
isLoading: _isBusy,
|
||||||
|
onFetchData: () {
|
||||||
|
_getEvents();
|
||||||
|
},
|
||||||
|
itemBuilder: (context, idx) {
|
||||||
|
final element = _events[idx];
|
||||||
|
return TimelineTile(
|
||||||
|
isFirst: idx == 0,
|
||||||
|
isLast: _events.length - 1 == idx,
|
||||||
|
alignment: TimelineAlign.start,
|
||||||
|
indicatorStyle: IndicatorStyle(width: 15),
|
||||||
|
endChild: Container(
|
||||||
|
child: Card(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
element.type,
|
||||||
|
style: GoogleFonts.robotoMono(fontSize: 15),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
_showIp
|
||||||
|
? element.ipAddress
|
||||||
|
: _censorIpAddress(element.ipAddress),
|
||||||
|
style: GoogleFonts.sourceCodePro(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: 20,
|
||||||
|
width: double.maxFinite,
|
||||||
|
child: Marquee(
|
||||||
|
text: element.userAgent,
|
||||||
|
velocity: 25,
|
||||||
|
startAfter: Duration(milliseconds: 500),
|
||||||
|
pauseAfterRound: Duration(milliseconds: 3000),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
RelativeDate(element.createdAt),
|
||||||
|
const Gap(6),
|
||||||
|
Text('·'),
|
||||||
|
const Gap(6),
|
||||||
|
RelativeDate(element.createdAt, isFull: true),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).paddingSymmetric(horizontal: 12, vertical: 8),
|
||||||
|
).paddingOnly(left: 16),
|
||||||
|
),
|
||||||
|
).paddingSymmetric(horizontal: 18);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,14 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_animate/flutter_animate.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:solian/providers/websocket.dart';
|
import 'package:solian/models/notification.dart';
|
||||||
import 'package:solian/providers/auth.dart';
|
import 'package:solian/models/post.dart';
|
||||||
import 'package:solian/models/notification.dart' as notify;
|
import 'package:solian/providers/notifications.dart';
|
||||||
|
import 'package:solian/router.dart';
|
||||||
|
import 'package:solian/widgets/loading_indicator.dart';
|
||||||
|
import 'package:solian/widgets/markdown_text_content.dart';
|
||||||
|
import 'package:solian/widgets/posts/post_item.dart';
|
||||||
|
import 'package:solian/widgets/relative_date.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
class NotificationScreen extends StatefulWidget {
|
class NotificationScreen extends StatefulWidget {
|
||||||
@ -14,57 +19,9 @@ class NotificationScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _NotificationScreenState extends State<NotificationScreen> {
|
class _NotificationScreenState extends State<NotificationScreen> {
|
||||||
bool _isBusy = false;
|
|
||||||
|
|
||||||
Future<void> _markAllRead() async {
|
|
||||||
final AuthProvider auth = Get.find();
|
|
||||||
if (auth.isAuthorized.isFalse) return;
|
|
||||||
|
|
||||||
setState(() => _isBusy = true);
|
|
||||||
|
|
||||||
final WebSocketProvider provider = Get.find();
|
|
||||||
|
|
||||||
List<int> markList = List.empty(growable: true);
|
|
||||||
for (final element in provider.notifications) {
|
|
||||||
if (element.id <= 0) continue;
|
|
||||||
markList.add(element.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (markList.isNotEmpty) {
|
|
||||||
final client = await auth.configureClient('auth');
|
|
||||||
await client.put('/notifications/read', {'messages': markList});
|
|
||||||
}
|
|
||||||
|
|
||||||
provider.notifications.clear();
|
|
||||||
|
|
||||||
setState(() => _isBusy = false);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _markOneRead(notify.Notification element, int index) async {
|
|
||||||
final AuthProvider auth = Get.find();
|
|
||||||
if (auth.isAuthorized.isFalse) return;
|
|
||||||
|
|
||||||
final WebSocketProvider provider = Get.find();
|
|
||||||
|
|
||||||
if (element.id <= 0) {
|
|
||||||
provider.notifications.removeAt(index);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(() => _isBusy = true);
|
|
||||||
|
|
||||||
final client = await auth.configureClient('auth');
|
|
||||||
|
|
||||||
await client.put('/notifications/read/${element.id}', {});
|
|
||||||
|
|
||||||
provider.notifications.removeAt(index);
|
|
||||||
|
|
||||||
setState(() => _isBusy = false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final WebSocketProvider ws = Get.find();
|
final NotificationProvider nty = Get.find();
|
||||||
|
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: MediaQuery.of(context).size.height * 0.85,
|
height: MediaQuery.of(context).size.height * 0.85,
|
||||||
@ -77,71 +34,174 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||||||
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
|
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Obx(() {
|
child: Obx(() {
|
||||||
return CustomScrollView(
|
return RefreshIndicator(
|
||||||
slivers: [
|
onRefresh: () => nty.fetchNotification(),
|
||||||
if (_isBusy)
|
child: CustomScrollView(
|
||||||
|
slivers: [
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: const LinearProgressIndicator().animate().scaleX(),
|
child: LoadingIndicator(
|
||||||
),
|
isActive: nty.isBusy.value,
|
||||||
if (ws.notifications.isEmpty)
|
|
||||||
SliverToBoxAdapter(
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
|
||||||
color:
|
|
||||||
Theme.of(context).colorScheme.surfaceContainerHigh,
|
|
||||||
child: ListTile(
|
|
||||||
leading: const Icon(Icons.check),
|
|
||||||
title: Text('notifyEmpty'.tr),
|
|
||||||
subtitle: Text('notifyEmptyCaption'.tr),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (ws.notifications.isNotEmpty)
|
if (nty.notifications
|
||||||
SliverToBoxAdapter(
|
.where((x) => x.readAt == null)
|
||||||
child: Container(
|
.isEmpty)
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
SliverToBoxAdapter(
|
||||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
child: Container(
|
||||||
child: ListTile(
|
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||||
leading: const Icon(Icons.checklist),
|
color: Theme.of(context)
|
||||||
title: Text('notifyAllRead'.tr),
|
.colorScheme
|
||||||
onTap: _isBusy ? null : () => _markAllRead(),
|
.surfaceContainerHigh,
|
||||||
|
child: ListTile(
|
||||||
|
leading: const Icon(Icons.check),
|
||||||
|
title: Text('notifyEmpty'.tr),
|
||||||
|
subtitle: Text('notifyEmptyCaption'.tr),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (nty.notifications
|
||||||
|
.where((x) => x.readAt == null)
|
||||||
|
.isNotEmpty)
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||||
|
color:
|
||||||
|
Theme.of(context).colorScheme.secondaryContainer,
|
||||||
|
child: ListTile(
|
||||||
|
leading: const Icon(Icons.checklist),
|
||||||
|
title: Text('notifyAllRead'.tr),
|
||||||
|
onTap: nty.isBusy.value
|
||||||
|
? null
|
||||||
|
: () => nty.markAllRead(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SliverList.separated(
|
||||||
|
itemCount: nty.notifications.length,
|
||||||
|
itemBuilder: (BuildContext context, int index) {
|
||||||
|
var element = nty.notifications[index];
|
||||||
|
return ClipRect(
|
||||||
|
child: Dismissible(
|
||||||
|
direction: element.readAt == null
|
||||||
|
? DismissDirection.horizontal
|
||||||
|
: DismissDirection.none,
|
||||||
|
key: Key(const Uuid().v4()),
|
||||||
|
background: Container(
|
||||||
|
color: Colors.lightBlue,
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child:
|
||||||
|
const Icon(Icons.check, color: Colors.white),
|
||||||
|
),
|
||||||
|
secondaryBackground: Container(
|
||||||
|
color: Colors.lightBlue,
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child:
|
||||||
|
const Icon(Icons.check, color: Colors.white),
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 28,
|
||||||
|
vertical: 16,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Icon(NotificationTopicIcons[element.topic]),
|
||||||
|
const Gap(16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (element.readAt == null)
|
||||||
|
Badge(
|
||||||
|
label: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.new_releases_outlined,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSurface,
|
||||||
|
size: 12,
|
||||||
|
),
|
||||||
|
const Gap(4),
|
||||||
|
Text('unread'.tr),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
).paddingOnly(bottom: 4),
|
||||||
|
Text(
|
||||||
|
element.title,
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.titleMedium,
|
||||||
|
),
|
||||||
|
if (element.subtitle != null)
|
||||||
|
Text(
|
||||||
|
element.subtitle!,
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.titleSmall,
|
||||||
|
),
|
||||||
|
if (element.subtitle != null)
|
||||||
|
const Gap(4),
|
||||||
|
MarkdownTextContent(
|
||||||
|
content: element.body,
|
||||||
|
isAutoWarp: true,
|
||||||
|
isSelectable: true,
|
||||||
|
parentId:
|
||||||
|
'notification-${element.id}',
|
||||||
|
),
|
||||||
|
if ([
|
||||||
|
'interactive.feedback',
|
||||||
|
'interactive.subscription'
|
||||||
|
].contains(element.topic) &&
|
||||||
|
element.metadata?['related_post'] !=
|
||||||
|
null)
|
||||||
|
_PostRelatedNotificationWidget(
|
||||||
|
metadata: element.metadata!,
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
Opacity(
|
||||||
|
opacity: 0.75,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
RelativeDate(
|
||||||
|
element.createdAt,
|
||||||
|
style: TextStyle(fontSize: 12),
|
||||||
|
),
|
||||||
|
const Gap(4),
|
||||||
|
Text(
|
||||||
|
'·',
|
||||||
|
style: TextStyle(fontSize: 12),
|
||||||
|
),
|
||||||
|
const Gap(4),
|
||||||
|
RelativeDate(
|
||||||
|
element.createdAt,
|
||||||
|
style: TextStyle(fontSize: 12),
|
||||||
|
isFull: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onDismissed: (_) => nty.markOneRead(element, index),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
separatorBuilder: (_, __) =>
|
||||||
|
const Divider(thickness: 0.3, height: 0.3),
|
||||||
),
|
),
|
||||||
SliverList.separated(
|
],
|
||||||
itemCount: ws.notifications.length,
|
),
|
||||||
itemBuilder: (BuildContext context, int index) {
|
|
||||||
var element = ws.notifications[index];
|
|
||||||
return Dismissible(
|
|
||||||
key: Key(const Uuid().v4()),
|
|
||||||
background: Container(
|
|
||||||
color: Colors.lightBlue,
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: const Icon(Icons.check, color: Colors.white),
|
|
||||||
),
|
|
||||||
child: ListTile(
|
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 24,
|
|
||||||
vertical: 8,
|
|
||||||
),
|
|
||||||
title: Text(element.title),
|
|
||||||
subtitle: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
if (element.subtitle != null)
|
|
||||||
Text(element.subtitle!),
|
|
||||||
Text(element.body),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onDismissed: (_) => _markOneRead(element, index),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
separatorBuilder: (_, __) =>
|
|
||||||
const Divider(thickness: 0.3, height: 0.3),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@ -156,7 +216,7 @@ class NotificationButton extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final WebSocketProvider provider = Get.find();
|
final NotificationProvider nty = Get.find();
|
||||||
|
|
||||||
final button = IconButton(
|
final button = IconButton(
|
||||||
icon: const Icon(Icons.notifications),
|
icon: const Icon(Icons.notifications),
|
||||||
@ -166,16 +226,16 @@ class NotificationButton extends StatelessWidget {
|
|||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => const NotificationScreen(),
|
builder: (context) => const NotificationScreen(),
|
||||||
).then((_) => provider.notificationUnread.value = 0);
|
).then((_) => nty.notificationUnread.value = 0);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return Obx(() {
|
return Obx(() {
|
||||||
if (provider.notificationUnread.value > 0) {
|
if (nty.notificationUnread.value > 0) {
|
||||||
return Badge(
|
return Badge(
|
||||||
isLabelVisible: true,
|
isLabelVisible: true,
|
||||||
offset: const Offset(-8, 2),
|
offset: const Offset(-8, 2),
|
||||||
label: Text(provider.notificationUnread.value.toString()),
|
label: Text(nty.notificationUnread.value.toString()),
|
||||||
child: button,
|
child: button,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@ -184,3 +244,31 @@ class NotificationButton extends StatelessWidget {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _PostRelatedNotificationWidget extends StatelessWidget {
|
||||||
|
final Map<String, dynamic> metadata;
|
||||||
|
|
||||||
|
const _PostRelatedNotificationWidget({super.key, required this.metadata});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
child: Card(
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||||
|
child: PostItem(
|
||||||
|
item: Post.fromJson(metadata['related_post']),
|
||||||
|
isCompact: true,
|
||||||
|
).paddingAll(8),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
final data = Post.fromJson(metadata['related_post']);
|
||||||
|
Navigator.pop(context);
|
||||||
|
AppRouter.instance.pushNamed(
|
||||||
|
'postDetail',
|
||||||
|
pathParameters: {'id': data.id.toString()},
|
||||||
|
extra: data,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_animate/flutter_animate.dart';
|
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:get/get_connect/http/src/exceptions/exceptions.dart';
|
import 'package:get/get_connect/http/src/exceptions/exceptions.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:solian/exceptions/request.dart';
|
import 'package:solian/exceptions/request.dart';
|
||||||
import 'package:solian/exts.dart';
|
import 'package:solian/exts.dart';
|
||||||
import 'package:solian/providers/auth.dart';
|
import 'package:solian/providers/auth.dart';
|
||||||
|
import 'package:solian/widgets/loading_indicator.dart';
|
||||||
|
|
||||||
class NotificationPreferencesScreen extends StatefulWidget {
|
class NotificationPreferencesScreen extends StatefulWidget {
|
||||||
const NotificationPreferencesScreen({super.key});
|
const NotificationPreferencesScreen({super.key});
|
||||||
@ -59,10 +59,10 @@ class _NotificationPreferencesScreenState
|
|||||||
});
|
});
|
||||||
if (resp.statusCode != 200) {
|
if (resp.statusCode != 200) {
|
||||||
context.showErrorDialog(RequestException(resp));
|
context.showErrorDialog(RequestException(resp));
|
||||||
|
} else {
|
||||||
|
context.showSnackbar('preferencesApplied'.tr);
|
||||||
}
|
}
|
||||||
|
|
||||||
context.showSnackbar('preferencesApplied'.tr);
|
|
||||||
|
|
||||||
setState(() => _isBusy = false);
|
setState(() => _isBusy = false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,7 +76,7 @@ class _NotificationPreferencesScreenState
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
LoadingIndicator(isActive: _isBusy),
|
||||||
ListTile(
|
ListTile(
|
||||||
tileColor: Theme.of(context).colorScheme.surfaceContainer,
|
tileColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
118
lib/screens/account/preferences/security.dart
Normal file
118
lib/screens/account/preferences/security.dart
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:get/get_connect/http/src/exceptions/exceptions.dart';
|
||||||
|
import 'package:solian/exceptions/request.dart';
|
||||||
|
import 'package:solian/exts.dart';
|
||||||
|
import 'package:solian/providers/auth.dart';
|
||||||
|
import 'package:solian/widgets/loading_indicator.dart';
|
||||||
|
|
||||||
|
class AuthPreferencesScreen extends StatefulWidget {
|
||||||
|
const AuthPreferencesScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AuthPreferencesScreen> createState() => _AuthPreferencesScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AuthPreferencesScreenState extends State<AuthPreferencesScreen> {
|
||||||
|
bool _isBusy = true;
|
||||||
|
|
||||||
|
Map<String, dynamic> _config = {
|
||||||
|
'maximum_auth_steps': 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
Future<void> _getPreferences() async {
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
|
final auth = Get.find<AuthProvider>();
|
||||||
|
if (!auth.isAuthorized.value) throw UnauthorizedException();
|
||||||
|
|
||||||
|
final client = await auth.configureClient('id');
|
||||||
|
final resp = await client.get('/preferences/auth');
|
||||||
|
if (resp.statusCode != 200 && resp.statusCode != 404) {
|
||||||
|
context.showErrorDialog(RequestException(resp));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resp.statusCode == 200) {
|
||||||
|
_config = resp.body;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _savePreferences() async {
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
|
final auth = Get.find<AuthProvider>();
|
||||||
|
if (!auth.isAuthorized.value) throw UnauthorizedException();
|
||||||
|
|
||||||
|
final client = await auth.configureClient('id');
|
||||||
|
final resp = await client.put('/preferences/auth', _config);
|
||||||
|
if (resp.statusCode != 200) {
|
||||||
|
context.showErrorDialog(RequestException(resp));
|
||||||
|
} else {
|
||||||
|
context.showSnackbar('preferencesApplied'.tr);
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_getPreferences();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
LoadingIndicator(isActive: _isBusy),
|
||||||
|
ListTile(
|
||||||
|
tileColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
leading: const Icon(Icons.save),
|
||||||
|
title: Text('save'.tr),
|
||||||
|
enabled: !_isBusy,
|
||||||
|
onTap: () {
|
||||||
|
_savePreferences();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: ListView(
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
title: Text('authMaximumAuthSteps'.tr),
|
||||||
|
subtitle: Text('authMaximumAuthStepsDesc'.tr),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
trailing: SizedBox(
|
||||||
|
width: 60,
|
||||||
|
child: _isBusy
|
||||||
|
? null
|
||||||
|
: TextFormField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
isDense: true,
|
||||||
|
),
|
||||||
|
initialValue:
|
||||||
|
_config['maximum_auth_steps']?.toString() ?? '2',
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
inputFormatters: [
|
||||||
|
FilteringTextInputFormatter.digitsOnly
|
||||||
|
],
|
||||||
|
onTapOutside: (_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
onChanged: (value) {
|
||||||
|
_config['maximum_auth_steps'] =
|
||||||
|
int.tryParse(value) ?? 2;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,4 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_animate/flutter_animate.dart';
|
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:image_cropper/image_cropper.dart';
|
import 'package:image_cropper/image_cropper.dart';
|
||||||
@ -12,6 +11,7 @@ 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';
|
||||||
import 'package:solian/widgets/account/account_avatar.dart';
|
import 'package:solian/widgets/account/account_avatar.dart';
|
||||||
|
import 'package:solian/widgets/loading_indicator.dart';
|
||||||
|
|
||||||
class PersonalizeScreen extends StatefulWidget {
|
class PersonalizeScreen extends StatefulWidget {
|
||||||
const PersonalizeScreen({super.key});
|
const PersonalizeScreen({super.key});
|
||||||
@ -188,7 +188,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
|||||||
|
|
||||||
return ListView(
|
return ListView(
|
||||||
children: [
|
children: [
|
||||||
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
LoadingIndicator(isActive: _isBusy),
|
||||||
const Gap(24),
|
const Gap(24),
|
||||||
Stack(
|
Stack(
|
||||||
children: [
|
children: [
|
||||||
|
@ -348,7 +348,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
|||||||
detail: _userinfo,
|
detail: _userinfo,
|
||||||
profile: _userinfo!.profile,
|
profile: _userinfo!.profile,
|
||||||
extraWidgets: [
|
extraWidgets: [
|
||||||
if (_dailySignRecords.isNotEmpty)
|
if (_dailySignRecords.length > 1)
|
||||||
Card(
|
Card(
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: 180,
|
height: 180,
|
||||||
@ -588,8 +588,6 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
|||||||
color:
|
color:
|
||||||
Theme.of(context).colorScheme.surfaceContainerLow,
|
Theme.of(context).colorScheme.surfaceContainerLow,
|
||||||
child: PostListEntryWidget(
|
child: PostListEntryWidget(
|
||||||
backgroundColor:
|
|
||||||
Theme.of(context).colorScheme.surfaceContainerLow,
|
|
||||||
item: element,
|
item: element,
|
||||||
isClickable: true,
|
isClickable: true,
|
||||||
isNestedClickable: true,
|
isNestedClickable: true,
|
||||||
|
@ -8,8 +8,8 @@ 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/realm.dart';
|
import 'package:solian/providers/content/realm.dart';
|
||||||
|
import 'package:solian/providers/notifications.dart';
|
||||||
import 'package:solian/providers/relation.dart';
|
import 'package:solian/providers/relation.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';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
@ -178,7 +178,7 @@ class _SignInScreenState extends State<SignInScreen> {
|
|||||||
|
|
||||||
Get.find<RealmProvider>().refreshAvailableRealms();
|
Get.find<RealmProvider>().refreshAvailableRealms();
|
||||||
Get.find<RelationshipProvider>().refreshRelativeList();
|
Get.find<RelationshipProvider>().refreshRelativeList();
|
||||||
Get.find<WebSocketProvider>().registerPushNotifications();
|
Get.find<NotificationProvider>().registerPushNotifications();
|
||||||
autoConfigureBackgroundNotificationService();
|
autoConfigureBackgroundNotificationService();
|
||||||
autoStartBackgroundNotificationService();
|
autoStartBackgroundNotificationService();
|
||||||
|
|
||||||
|
@ -198,7 +198,7 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final ChatCallProvider ctrl = Get.find();
|
final ChatCallProvider ctrl = Get.find();
|
||||||
|
|
||||||
return RootContainer(
|
return ResponsiveRootContainer(
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: widget.hideAppBar
|
appBar: widget.hideAppBar
|
||||||
? null
|
? null
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_animate/flutter_animate.dart';
|
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:solian/exts.dart';
|
import 'package:solian/exts.dart';
|
||||||
import 'package:solian/models/channel.dart';
|
import 'package:solian/models/channel.dart';
|
||||||
@ -9,6 +8,7 @@ import 'package:solian/providers/content/channel.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/app_bar_title.dart';
|
import 'package:solian/widgets/app_bar_title.dart';
|
||||||
|
import 'package:solian/widgets/loading_indicator.dart';
|
||||||
import 'package:solian/widgets/root_container.dart';
|
import 'package:solian/widgets/root_container.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
@ -132,7 +132,7 @@ class _ChannelOrganizeScreenState extends State<ChannelOrganizeScreen> {
|
|||||||
top: false,
|
top: false,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
LoadingIndicator(isActive: _isBusy),
|
||||||
if (widget.edit != null)
|
if (widget.edit != null)
|
||||||
MaterialBanner(
|
MaterialBanner(
|
||||||
leading: const Icon(Icons.edit),
|
leading: const Icon(Icons.edit),
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_resizable_container/flutter_resizable_container.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';
|
||||||
@ -19,6 +20,7 @@ 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/loading_indicator.dart';
|
||||||
import 'package:solian/widgets/root_container.dart';
|
import 'package:solian/widgets/root_container.dart';
|
||||||
import 'package:solian/widgets/sidebar/empty_placeholder.dart';
|
import 'package:solian/widgets/sidebar/empty_placeholder.dart';
|
||||||
|
|
||||||
@ -41,14 +43,23 @@ class ChatListShell extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return RootContainer(
|
return RootContainer(
|
||||||
child: Row(
|
child: ResizableContainer(
|
||||||
|
direction: Axis.horizontal,
|
||||||
|
divider: ResizableDivider(
|
||||||
|
thickness: 0.3,
|
||||||
|
color: Theme.of(context).dividerColor.withOpacity(0.3),
|
||||||
|
),
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(
|
const ResizableChild(
|
||||||
width: 360,
|
minSize: 280,
|
||||||
|
maxSize: 520,
|
||||||
|
size: ResizableSize.pixels(360),
|
||||||
child: ChatList(),
|
child: ChatList(),
|
||||||
),
|
),
|
||||||
const VerticalDivider(thickness: 0.3, width: 0.3),
|
ResizableChild(
|
||||||
Expanded(child: child ?? const EmptyPagePlaceholder()),
|
minSize: 280,
|
||||||
|
child: child ?? const EmptyPagePlaceholder(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -280,26 +291,7 @@ class _ChatListState extends State<ChatList> {
|
|||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
const ChatCallCurrentIndicator(),
|
const ChatCallCurrentIndicator(),
|
||||||
if (_isBusy)
|
LoadingIndicator(isActive: _isBusy),
|
||||||
Container(
|
|
||||||
color: Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.surfaceContainerLow
|
|
||||||
.withOpacity(0.8),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
const SizedBox(
|
|
||||||
height: 16,
|
|
||||||
width: 16,
|
|
||||||
child: CircularProgressIndicator(strokeWidth: 2.5),
|
|
||||||
),
|
|
||||||
const Gap(8),
|
|
||||||
Text('loading'.tr)
|
|
||||||
],
|
|
||||||
).paddingSymmetric(vertical: 8),
|
|
||||||
),
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TabBarView(
|
child: TabBarView(
|
||||||
children: [
|
children: [
|
||||||
|
@ -20,7 +20,7 @@ import 'package:solian/providers/content/posts.dart';
|
|||||||
import 'package:solian/providers/daily_sign.dart';
|
import 'package:solian/providers/daily_sign.dart';
|
||||||
import 'package:solian/providers/database/services/messages.dart';
|
import 'package:solian/providers/database/services/messages.dart';
|
||||||
import 'package:solian/providers/last_read.dart';
|
import 'package:solian/providers/last_read.dart';
|
||||||
import 'package:solian/providers/websocket.dart';
|
import 'package:solian/providers/notifications.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';
|
||||||
@ -38,7 +38,7 @@ class DashboardScreen extends StatefulWidget {
|
|||||||
class _DashboardScreenState extends State<DashboardScreen> {
|
class _DashboardScreenState extends State<DashboardScreen> {
|
||||||
late final AuthProvider _auth = Get.find();
|
late final AuthProvider _auth = Get.find();
|
||||||
late final LastReadProvider _lastRead = Get.find();
|
late final LastReadProvider _lastRead = Get.find();
|
||||||
late final WebSocketProvider _ws = Get.find();
|
late final NotificationProvider _nty = Get.find();
|
||||||
late final PostProvider _posts = Get.find();
|
late final PostProvider _posts = Get.find();
|
||||||
late final DailySignProvider _dailySign = Get.find();
|
late final DailySignProvider _dailySign = Get.find();
|
||||||
|
|
||||||
@ -46,7 +46,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
|||||||
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
||||||
|
|
||||||
List<Notification> get _pendingNotifications =>
|
List<Notification> get _pendingNotifications =>
|
||||||
List<Notification>.from(_ws.notifications)
|
List<Notification>.from(_nty.notifications.where((x) => x.readAt == null))
|
||||||
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||||
|
|
||||||
List<Post>? _currentPosts;
|
List<Post>? _currentPosts;
|
||||||
@ -254,7 +254,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
|||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'notificationUnreadCount'.trParams({
|
'notificationUnreadCount'.trParams({
|
||||||
'count': _ws.notifications.length.toString(),
|
'count': _pendingNotifications.length.toString(),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -267,12 +267,12 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
|||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => const NotificationScreen(),
|
builder: (context) => const NotificationScreen(),
|
||||||
).then((_) => _ws.notificationUnread.value = 0);
|
).then((_) => _nty.notificationUnread.value = 0);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
).paddingOnly(left: 18, right: 18, bottom: 8),
|
).paddingOnly(left: 18, right: 18, bottom: 8),
|
||||||
if (_ws.notifications.isNotEmpty)
|
if (_pendingNotifications.isNotEmpty)
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 76,
|
height: 76,
|
||||||
child: ListView.separated(
|
child: ListView.separated(
|
||||||
@ -389,10 +389,11 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
|||||||
onUpdate: (_) {
|
onUpdate: (_) {
|
||||||
_pullPosts();
|
_pullPosts();
|
||||||
},
|
},
|
||||||
backgroundColor: Theme.of(context)
|
padding: EdgeInsets.symmetric(
|
||||||
.colorScheme
|
vertical: 8,
|
||||||
.surfaceContainerLow,
|
horizontal: 4,
|
||||||
).paddingAll(8),
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
).paddingSymmetric(horizontal: 8),
|
).paddingSymmetric(horizontal: 8),
|
||||||
@ -525,7 +526,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
|||||||
style: TextStyle(color: _unFocusColor, fontSize: 12),
|
style: TextStyle(color: _unFocusColor, fontSize: 12),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
).paddingAll(8),
|
).paddingOnly(left: 8, right: 8, top: 8, bottom: 50),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
@ -6,6 +7,7 @@ import 'package:get/get.dart';
|
|||||||
import 'package:solian/controllers/post_list_controller.dart';
|
import 'package:solian/controllers/post_list_controller.dart';
|
||||||
import 'package:solian/providers/auth.dart';
|
import 'package:solian/providers/auth.dart';
|
||||||
import 'package:solian/providers/navigation.dart';
|
import 'package:solian/providers/navigation.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/signin_required_overlay.dart';
|
import 'package:solian/widgets/account/signin_required_overlay.dart';
|
||||||
@ -87,76 +89,89 @@ class _ExploreScreenState extends State<ExploreScreen>
|
|||||||
|
|
||||||
final scrollProgress =
|
final scrollProgress =
|
||||||
(scrollOffset / colorChangeOffset).clamp(0.0, 1.0);
|
(scrollOffset / colorChangeOffset).clamp(0.0, 1.0);
|
||||||
final backgroundColor = Color.lerp(
|
final blurSigma = lerpDouble(0, 10, scrollProgress) ?? 0;
|
||||||
Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.surfaceContainerLow
|
|
||||||
.withOpacity(0),
|
|
||||||
Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.surfaceContainerLow
|
|
||||||
.withOpacity(0.9),
|
|
||||||
scrollProgress,
|
|
||||||
);
|
|
||||||
|
|
||||||
return SliverAppBar(
|
return SliverAppBar(
|
||||||
backgroundColor: backgroundColor,
|
flexibleSpace: ClipRRect(
|
||||||
flexibleSpace: SizedBox(
|
child: BackdropFilter(
|
||||||
height: 48,
|
filter: ImageFilter.blur(
|
||||||
child: const Row(
|
sigmaX: blurSigma,
|
||||||
children: [
|
sigmaY: blurSigma,
|
||||||
RealmSwitcher(),
|
),
|
||||||
],
|
child: ListView(
|
||||||
).paddingSymmetric(horizontal: 8),
|
padding: EdgeInsets.zero,
|
||||||
).paddingOnly(top: MediaQuery.of(context).padding.top),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
height: 48,
|
||||||
|
child: const Row(
|
||||||
|
children: [
|
||||||
|
RealmSwitcher(),
|
||||||
|
],
|
||||||
|
).paddingSymmetric(horizontal: 8),
|
||||||
|
).paddingSymmetric(vertical: 4),
|
||||||
|
TabBar(
|
||||||
|
controller: _tabController,
|
||||||
|
dividerHeight: scrollProgress > 0 ? 0 : 0.3,
|
||||||
|
tabAlignment: TabAlignment.fill,
|
||||||
|
tabs: [
|
||||||
|
Tab(
|
||||||
|
child: Row(
|
||||||
|
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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).paddingOnly(top: MediaQuery.of(context).padding.top),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expandedHeight: 104,
|
||||||
snap: true,
|
snap: true,
|
||||||
floating: true,
|
floating: true,
|
||||||
toolbarHeight: AppTheme.toolbarHeight(context),
|
toolbarHeight: AppTheme.toolbarHeight(context),
|
||||||
leading: AppBarLeadingButton.adaptive(context),
|
leading: AppBarLeadingButton.adaptive(context),
|
||||||
actions: [
|
actions: [
|
||||||
const BackgroundStateWidget(),
|
const BackgroundStateWidget(),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.search),
|
||||||
|
onPressed: () {
|
||||||
|
AppRouter.instance.pushNamed('postSearch');
|
||||||
|
},
|
||||||
|
),
|
||||||
const NotificationButton(),
|
const NotificationButton(),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: AppTheme.isLargeScreen(context) ? 8 : 16,
|
width: AppTheme.isLargeScreen(context) ? 8 : 16,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
bottom: TabBar(
|
|
||||||
controller: _tabController,
|
|
||||||
dividerHeight: 0.3,
|
|
||||||
tabAlignment: TabAlignment.fill,
|
|
||||||
tabs: [
|
|
||||||
Tab(
|
|
||||||
child: Row(
|
|
||||||
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),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -180,6 +195,12 @@ class _ExploreScreenState extends State<ExploreScreen>
|
|||||||
onRefresh: () => _postController.reloadAllOver(),
|
onRefresh: () => _postController.reloadAllOver(),
|
||||||
child: CustomScrollView(slivers: [
|
child: CustomScrollView(slivers: [
|
||||||
ControlledPostListWidget(
|
ControlledPostListWidget(
|
||||||
|
padding: AppTheme.isLargeScreen(context)
|
||||||
|
? EdgeInsets.symmetric(
|
||||||
|
horizontal: 4,
|
||||||
|
vertical: 8,
|
||||||
|
)
|
||||||
|
: EdgeInsets.zero,
|
||||||
controller: _postController.pagingController,
|
controller: _postController.pagingController,
|
||||||
onUpdate: () => _postController.reloadAllOver(),
|
onUpdate: () => _postController.reloadAllOver(),
|
||||||
),
|
),
|
||||||
@ -191,6 +212,9 @@ class _ExploreScreenState extends State<ExploreScreen>
|
|||||||
onRefresh: () => _postController.reloadAllOver(),
|
onRefresh: () => _postController.reloadAllOver(),
|
||||||
child: CustomScrollView(slivers: [
|
child: CustomScrollView(slivers: [
|
||||||
ControlledPostListWidget(
|
ControlledPostListWidget(
|
||||||
|
padding: AppTheme.isLargeScreen(context)
|
||||||
|
? EdgeInsets.symmetric(horizontal: 16)
|
||||||
|
: EdgeInsets.zero,
|
||||||
controller: _postController.pagingController,
|
controller: _postController.pagingController,
|
||||||
onUpdate: () => _postController.reloadAllOver(),
|
onUpdate: () => _postController.reloadAllOver(),
|
||||||
),
|
),
|
||||||
|
@ -1,114 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
|
||||||
import 'package:solian/models/pagination.dart';
|
|
||||||
import 'package:solian/models/post.dart';
|
|
||||||
import 'package:solian/providers/content/posts.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/posts/post_action.dart';
|
|
||||||
import 'package:solian/widgets/posts/post_owned_list.dart';
|
|
||||||
import 'package:solian/widgets/root_container.dart';
|
|
||||||
|
|
||||||
class DraftBoxScreen extends StatefulWidget {
|
|
||||||
const DraftBoxScreen({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<DraftBoxScreen> createState() => _DraftBoxScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _DraftBoxScreenState extends State<DraftBoxScreen> {
|
|
||||||
final PagingController<int, Post> _pagingController =
|
|
||||||
PagingController(firstPageKey: 0);
|
|
||||||
|
|
||||||
_getPosts(int pageKey) async {
|
|
||||||
final PostProvider provider = Get.find();
|
|
||||||
|
|
||||||
Response resp;
|
|
||||||
try {
|
|
||||||
resp = await provider.listDraft(pageKey);
|
|
||||||
} catch (e) {
|
|
||||||
_pagingController.error = e;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final PaginationResult result = PaginationResult.fromJson(resp.body);
|
|
||||||
if (result.count == 0) {
|
|
||||||
_pagingController.appendLastPage([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final parsed = result.data?.map((e) => Post.fromJson(e)).toList();
|
|
||||||
if (parsed != null && parsed.length >= 10) {
|
|
||||||
_pagingController.appendPage(parsed, pageKey + parsed.length);
|
|
||||||
} else if (parsed != null) {
|
|
||||||
_pagingController.appendLastPage(parsed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_pagingController.addPageRequestListener(_getPosts);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return RootContainer(
|
|
||||||
child: Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
leading: AppBarLeadingButton.adaptive(context),
|
|
||||||
title: AppBarTitle('draftBox'.tr),
|
|
||||||
centerTitle: false,
|
|
||||||
toolbarHeight: AppTheme.toolbarHeight(context),
|
|
||||||
actions: [
|
|
||||||
SizedBox(
|
|
||||||
width: AppTheme.isLargeScreen(context) ? 8 : 16,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
body: RefreshIndicator(
|
|
||||||
onRefresh: () => Future.sync(() => _pagingController.refresh()),
|
|
||||||
child: PagedListView<int, Post>(
|
|
||||||
pagingController: _pagingController,
|
|
||||||
builderDelegate: PagedChildBuilderDelegate(
|
|
||||||
itemBuilder: (context, item, index) {
|
|
||||||
return PostOwnedListEntry(
|
|
||||||
item: item,
|
|
||||||
isFullContent: true,
|
|
||||||
backgroundColor:
|
|
||||||
Theme.of(context).colorScheme.surfaceContainerLow,
|
|
||||||
onTap: () async {
|
|
||||||
showModalBottomSheet(
|
|
||||||
useRootNavigator: true,
|
|
||||||
context: context,
|
|
||||||
builder: (context) => PostAction(
|
|
||||||
item: item,
|
|
||||||
noReact: true,
|
|
||||||
),
|
|
||||||
).then((value) {
|
|
||||||
if (value is Future) {
|
|
||||||
value.then((_) {
|
|
||||||
_pagingController.refresh();
|
|
||||||
});
|
|
||||||
} else if (value != null) {
|
|
||||||
_pagingController.refresh();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
).paddingOnly(left: 12, right: 12, bottom: 4);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_pagingController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,99 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
|
||||||
import 'package:solian/models/pagination.dart';
|
|
||||||
import 'package:solian/providers/content/posts.dart';
|
|
||||||
import 'package:solian/widgets/posts/post_list.dart';
|
|
||||||
|
|
||||||
import '../../models/post.dart';
|
|
||||||
|
|
||||||
class FeedSearchScreen extends StatefulWidget {
|
|
||||||
final String? tag;
|
|
||||||
final String? category;
|
|
||||||
|
|
||||||
const FeedSearchScreen({super.key, this.tag, this.category});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<FeedSearchScreen> createState() => _FeedSearchScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _FeedSearchScreenState extends State<FeedSearchScreen> {
|
|
||||||
final PagingController<int, Post> _pagingController =
|
|
||||||
PagingController(firstPageKey: 0);
|
|
||||||
|
|
||||||
getPosts(int pageKey) async {
|
|
||||||
final PostProvider provider = Get.find();
|
|
||||||
|
|
||||||
Response resp;
|
|
||||||
try {
|
|
||||||
resp = await provider.listPost(
|
|
||||||
pageKey,
|
|
||||||
tag: widget.tag,
|
|
||||||
category: widget.category,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
_pagingController.error = e;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final PaginationResult result = PaginationResult.fromJson(resp.body);
|
|
||||||
final parsed = result.data?.map((e) => Post.fromJson(e)).toList();
|
|
||||||
if (parsed != null && parsed.length >= 10) {
|
|
||||||
_pagingController.appendPage(parsed, pageKey + parsed.length);
|
|
||||||
} else if (parsed != null) {
|
|
||||||
_pagingController.appendLastPage(parsed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
|
|
||||||
_pagingController.addPageRequestListener(getPosts);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
body: Material(
|
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
if (widget.tag != null)
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.label),
|
|
||||||
tileColor: Theme.of(context).colorScheme.surfaceContainer,
|
|
||||||
title: Text('postSearchWithTag'.trParams({'key': widget.tag!})),
|
|
||||||
),
|
|
||||||
if (widget.category != null)
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.category),
|
|
||||||
tileColor: Theme.of(context).colorScheme.surfaceContainer,
|
|
||||||
title: Text('postSearchWithCategory'
|
|
||||||
.trParams({'key': widget.category!})),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: RefreshIndicator(
|
|
||||||
onRefresh: () => Future.sync(() => _pagingController.refresh()),
|
|
||||||
child: CustomScrollView(
|
|
||||||
slivers: [
|
|
||||||
ControlledPostListWidget(
|
|
||||||
controller: _pagingController,
|
|
||||||
onUpdate: () => _pagingController.refresh(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_pagingController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
128
lib/screens/posts/draft_box.dart
Normal file
128
lib/screens/posts/draft_box.dart
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:solian/models/pagination.dart';
|
||||||
|
import 'package:solian/models/post.dart';
|
||||||
|
import 'package:solian/providers/content/posts.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/loading_indicator.dart';
|
||||||
|
import 'package:solian/widgets/posts/post_action.dart';
|
||||||
|
import 'package:solian/widgets/posts/post_item.dart';
|
||||||
|
import 'package:solian/widgets/root_container.dart';
|
||||||
|
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||||
|
|
||||||
|
class DraftBoxScreen extends StatefulWidget {
|
||||||
|
const DraftBoxScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<DraftBoxScreen> createState() => _DraftBoxScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DraftBoxScreenState extends State<DraftBoxScreen> {
|
||||||
|
bool _isBusy = true;
|
||||||
|
int? _totalPosts;
|
||||||
|
final List<Post> _posts = List.empty(growable: true);
|
||||||
|
|
||||||
|
_getPosts() async {
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
|
final PostProvider posts = Get.find();
|
||||||
|
final resp = await posts.listDraft(_posts.length);
|
||||||
|
|
||||||
|
final PaginationResult result = PaginationResult.fromJson(resp.body);
|
||||||
|
|
||||||
|
final parsed = result.data?.map((e) => Post.fromJson(e)).toList();
|
||||||
|
_totalPosts = result.count;
|
||||||
|
_posts.addAll(parsed ?? List.empty());
|
||||||
|
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _openActions(Post item) async {
|
||||||
|
showModalBottomSheet(
|
||||||
|
useRootNavigator: true,
|
||||||
|
context: context,
|
||||||
|
builder: (context) => PostAction(
|
||||||
|
item: item,
|
||||||
|
noReact: true,
|
||||||
|
),
|
||||||
|
).then((value) {
|
||||||
|
if (value is Future) {
|
||||||
|
value.then((_) {
|
||||||
|
_posts.clear();
|
||||||
|
_getPosts();
|
||||||
|
});
|
||||||
|
} else if (value != null) {
|
||||||
|
_posts.clear();
|
||||||
|
_getPosts();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_getPosts();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return RootContainer(
|
||||||
|
child: Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: AppBarLeadingButton.adaptive(context),
|
||||||
|
title: AppBarTitle('draftBox'.tr),
|
||||||
|
centerTitle: false,
|
||||||
|
toolbarHeight: AppTheme.toolbarHeight(context),
|
||||||
|
actions: [
|
||||||
|
SizedBox(
|
||||||
|
width: AppTheme.isLargeScreen(context) ? 8 : 16,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
LoadingIndicator(isActive: _isBusy),
|
||||||
|
Expanded(
|
||||||
|
child: RefreshIndicator(
|
||||||
|
onRefresh: () {
|
||||||
|
_posts.clear();
|
||||||
|
return _getPosts();
|
||||||
|
},
|
||||||
|
child: InfiniteList(
|
||||||
|
itemCount: _posts.length,
|
||||||
|
hasReachedMax: _totalPosts == _posts.length,
|
||||||
|
isLoading: _isBusy,
|
||||||
|
onFetchData: () => _getPosts(),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final item = _posts[index];
|
||||||
|
return Card(
|
||||||
|
child: GestureDetector(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
PostItem(
|
||||||
|
key: Key('p${item.id}'),
|
||||||
|
item: item,
|
||||||
|
isShowEmbed: false,
|
||||||
|
isClickable: false,
|
||||||
|
isShowReply: false,
|
||||||
|
isReactable: false,
|
||||||
|
onTapMore: () => _openActions(item),
|
||||||
|
).paddingSymmetric(vertical: 8),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () => _openActions(item),
|
||||||
|
),
|
||||||
|
).paddingOnly(left: 12, right: 12, bottom: 4);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -4,6 +4,9 @@ import 'package:solian/exts.dart';
|
|||||||
import 'package:solian/models/post.dart';
|
import 'package:solian/models/post.dart';
|
||||||
import 'package:solian/providers/content/posts.dart';
|
import 'package:solian/providers/content/posts.dart';
|
||||||
import 'package:solian/providers/last_read.dart';
|
import 'package:solian/providers/last_read.dart';
|
||||||
|
import 'package:solian/theme.dart';
|
||||||
|
import 'package:solian/widgets/loading_indicator.dart';
|
||||||
|
import 'package:solian/widgets/posts/post_action.dart';
|
||||||
import 'package:solian/widgets/posts/post_item.dart';
|
import 'package:solian/widgets/posts/post_item.dart';
|
||||||
import 'package:solian/widgets/posts/post_replies.dart';
|
import 'package:solian/widgets/posts/post_replies.dart';
|
||||||
|
|
||||||
@ -22,73 +25,109 @@ class PostDetailScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _PostDetailScreenState extends State<PostDetailScreen> {
|
class _PostDetailScreenState extends State<PostDetailScreen> {
|
||||||
Post? item;
|
bool _isBusy = true;
|
||||||
|
|
||||||
Future<Post?> _getDetail() async {
|
Post? _item;
|
||||||
if (widget.post != null) {
|
|
||||||
setState(() {
|
|
||||||
item = widget.post;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
final PostProvider provider = Get.find();
|
Future<void> _getDetail() async {
|
||||||
|
final PostProvider posts = Get.find();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final resp = await provider.getPost(widget.id);
|
final resp = await posts.getPost(widget.id);
|
||||||
item = Post.fromJson(resp.body);
|
_item = Post.fromJson(resp.body);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
context.showErrorDialog(e).then((_) => Navigator.pop(context));
|
context.showErrorDialog(e).then((_) => Navigator.pop(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
Get.find<LastReadProvider>().feedLastReadAt = item?.id;
|
Get.find<LastReadProvider>().feedLastReadAt = _item?.id;
|
||||||
|
|
||||||
return item;
|
if (mounted) setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
if (widget.post != null) {
|
||||||
|
_item = widget.post;
|
||||||
|
}
|
||||||
|
_getDetail();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FutureBuilder(
|
if (_isBusy && _item == null) {
|
||||||
future: _getDetail(),
|
return const Center(
|
||||||
builder: (context, snapshot) {
|
child: CircularProgressIndicator(),
|
||||||
if (!snapshot.hasData || snapshot.data == null) {
|
);
|
||||||
return const Center(
|
}
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return CustomScrollView(
|
return CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: PostItem(
|
child: LoadingIndicator(isActive: _isBusy),
|
||||||
item: item!,
|
),
|
||||||
isClickable: false,
|
SliverToBoxAdapter(
|
||||||
isOverrideEmbedClickable: true,
|
child: PostItem(
|
||||||
isFullDate: true,
|
key: ValueKey(_item),
|
||||||
isFullContent: true,
|
item: _item!,
|
||||||
isShowReply: false,
|
isClickable: false,
|
||||||
isContentSelectable: true,
|
isOverrideEmbedClickable: true,
|
||||||
),
|
isFullDate: true,
|
||||||
),
|
isShowReply: false,
|
||||||
SliverToBoxAdapter(
|
isContentSelectable: true,
|
||||||
child:
|
padding: AppTheme.isLargeScreen(context)
|
||||||
const Divider(thickness: 0.3, height: 1).paddingOnly(top: 4),
|
? EdgeInsets.symmetric(
|
||||||
),
|
horizontal: 4,
|
||||||
SliverToBoxAdapter(
|
vertical: 8,
|
||||||
child: Align(
|
)
|
||||||
alignment: Alignment.centerLeft,
|
: EdgeInsets.zero,
|
||||||
child: Text(
|
onTapMore: () {
|
||||||
'postReplies'.tr,
|
showModalBottomSheet(
|
||||||
style: Theme.of(context).textTheme.headlineSmall,
|
useRootNavigator: true,
|
||||||
).paddingOnly(left: 24, right: 24, top: 16),
|
context: context,
|
||||||
),
|
builder: (context) => PostAction(
|
||||||
),
|
item: _item!,
|
||||||
PostReplyList(item: item!),
|
noReact: true,
|
||||||
SliverToBoxAdapter(
|
),
|
||||||
child: SizedBox(height: MediaQuery.of(context).padding.bottom),
|
).then((value) {
|
||||||
),
|
if (value is Future) {
|
||||||
],
|
value.then((_) {
|
||||||
);
|
_getDetail();
|
||||||
},
|
});
|
||||||
|
} else if (value != null) {
|
||||||
|
_getDetail();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: const Divider(thickness: 0.3, height: 1).paddingOnly(
|
||||||
|
top: 8,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Text(
|
||||||
|
'postReplies'.tr,
|
||||||
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
|
).paddingOnly(left: 24, right: 24, top: 16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
PostReplyList(
|
||||||
|
item: _item!,
|
||||||
|
padding: AppTheme.isLargeScreen(context)
|
||||||
|
? EdgeInsets.symmetric(
|
||||||
|
horizontal: 4,
|
||||||
|
vertical: 8,
|
||||||
|
)
|
||||||
|
: EdgeInsets.zero,
|
||||||
|
),
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: SizedBox(height: MediaQuery.of(context).padding.bottom),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ import 'package:solian/router.dart';
|
|||||||
import 'package:solian/theme.dart';
|
import 'package:solian/theme.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/loading_indicator.dart';
|
||||||
import 'package:solian/widgets/markdown_text_content.dart';
|
import 'package:solian/widgets/markdown_text_content.dart';
|
||||||
import 'package:solian/widgets/posts/post_item.dart';
|
import 'package:solian/widgets/posts/post_item.dart';
|
||||||
import 'package:badges/badges.dart' as badges;
|
import 'package:badges/badges.dart' as badges;
|
||||||
@ -182,7 +183,10 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
ListTile(
|
ListTile(
|
||||||
tileColor: Theme.of(context).colorScheme.surfaceContainerLow,
|
tileColor: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.surfaceContainerLow
|
||||||
|
.withOpacity(0.5),
|
||||||
title: Column(
|
title: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@ -271,7 +275,7 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
LoadingIndicator(isActive: _isBusy),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: DefaultTabController(
|
child: DefaultTabController(
|
||||||
length: 2,
|
length: 2,
|
||||||
|
206
lib/screens/posts/post_search.dart
Normal file
206
lib/screens/posts/post_search.dart
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||||
|
import 'package:solian/models/pagination.dart';
|
||||||
|
import 'package:solian/providers/content/posts.dart';
|
||||||
|
import 'package:solian/widgets/loading_indicator.dart';
|
||||||
|
import 'package:solian/widgets/posts/post_list.dart';
|
||||||
|
|
||||||
|
import '../../models/post.dart';
|
||||||
|
|
||||||
|
class PostSearchScreen extends StatefulWidget {
|
||||||
|
final String? tag;
|
||||||
|
final String? category;
|
||||||
|
|
||||||
|
const PostSearchScreen({super.key, this.tag, this.category});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PostSearchScreen> createState() => _PostSearchScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PostSearchScreenState extends State<PostSearchScreen> {
|
||||||
|
int? _totalCount;
|
||||||
|
Duration? _lastTook;
|
||||||
|
|
||||||
|
final TextEditingController _probeController = TextEditingController();
|
||||||
|
final PagingController<int, Post> _pagingController =
|
||||||
|
PagingController(firstPageKey: 0);
|
||||||
|
|
||||||
|
late bool _isBusy = widget.tag != null || widget.category != null;
|
||||||
|
|
||||||
|
_searchPosts(int pageKey) async {
|
||||||
|
if (widget.tag == null &&
|
||||||
|
widget.category == null &&
|
||||||
|
_probeController.text.isEmpty) {
|
||||||
|
_pagingController.appendLastPage([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_isBusy) {
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pageKey == 0) {
|
||||||
|
_pagingController.itemList?.clear();
|
||||||
|
_pagingController.nextPageKey = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
final PostProvider posts = Get.find();
|
||||||
|
|
||||||
|
Stopwatch stopwatch = new Stopwatch()..start();
|
||||||
|
|
||||||
|
Response resp;
|
||||||
|
try {
|
||||||
|
if (_probeController.text.isEmpty) {
|
||||||
|
resp = await posts.listPost(
|
||||||
|
pageKey,
|
||||||
|
tag: widget.tag,
|
||||||
|
category: widget.category,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
resp = await posts.searchPost(
|
||||||
|
_probeController.text,
|
||||||
|
pageKey,
|
||||||
|
tag: widget.tag,
|
||||||
|
category: widget.category,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
_pagingController.error = e;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final PaginationResult result = PaginationResult.fromJson(resp.body);
|
||||||
|
final parsed = result.data?.map((e) => Post.fromJson(e)).toList();
|
||||||
|
if (parsed != null && parsed.length >= 10) {
|
||||||
|
_pagingController.appendPage(parsed, pageKey + parsed.length);
|
||||||
|
} else if (parsed != null) {
|
||||||
|
_pagingController.appendLastPage(parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
stopwatch.stop();
|
||||||
|
|
||||||
|
_totalCount = result.count;
|
||||||
|
_lastTook = stopwatch.elapsed;
|
||||||
|
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_pagingController.addPageRequestListener(_searchPosts);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_probeController.dispose();
|
||||||
|
_pagingController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Color get _unFocusColor =>
|
||||||
|
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
if (widget.tag != null)
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.label),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
tileColor: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.surfaceContainer
|
||||||
|
.withOpacity(0.5),
|
||||||
|
title: Text('postSearchWithTag'.trParams({'key': widget.tag!})),
|
||||||
|
),
|
||||||
|
if (widget.category != null)
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.category),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
tileColor: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.surfaceContainer
|
||||||
|
.withOpacity(0.5),
|
||||||
|
title: Text('postSearchWithCategory'.trParams({
|
||||||
|
'key': widget.category!,
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.secondaryContainer
|
||||||
|
.withOpacity(0.5),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||||
|
child: TextField(
|
||||||
|
controller: _probeController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
isCollapsed: true,
|
||||||
|
border: InputBorder.none,
|
||||||
|
hintText: 'search'.tr,
|
||||||
|
),
|
||||||
|
onSubmitted: (_) {
|
||||||
|
_searchPosts(0);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
LoadingIndicator(isActive: _isBusy),
|
||||||
|
if (_totalCount != null || _lastTook != null)
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 4),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.summarize_outlined,
|
||||||
|
size: 16,
|
||||||
|
color: _unFocusColor,
|
||||||
|
),
|
||||||
|
const Gap(4),
|
||||||
|
if (_totalCount != null)
|
||||||
|
Text(
|
||||||
|
'searchResult'.trParams({
|
||||||
|
'count': _totalCount!.toString(),
|
||||||
|
}),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: _unFocusColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(4),
|
||||||
|
if (_lastTook != null)
|
||||||
|
Text(
|
||||||
|
'searchTook'.trParams({
|
||||||
|
'time':
|
||||||
|
'${(_lastTook!.inMilliseconds / 1000).toStringAsFixed(3)}s',
|
||||||
|
}),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: _unFocusColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: RefreshIndicator(
|
||||||
|
onRefresh: () => Future.sync(() => _pagingController.refresh()),
|
||||||
|
child: CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
ControlledPostListWidget(
|
||||||
|
controller: _pagingController,
|
||||||
|
onUpdate: () => _pagingController.refresh(),
|
||||||
|
),
|
||||||
|
SliverGap(MediaQuery.of(context).padding.bottom),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,4 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_animate/flutter_animate.dart';
|
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:solian/models/realm.dart';
|
import 'package:solian/models/realm.dart';
|
||||||
@ -15,6 +14,7 @@ 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/auto_cache_image.dart';
|
import 'package:solian/widgets/auto_cache_image.dart';
|
||||||
import 'package:solian/widgets/current_state_action.dart';
|
import 'package:solian/widgets/current_state_action.dart';
|
||||||
|
import 'package:solian/widgets/loading_indicator.dart';
|
||||||
import 'package:solian/widgets/root_container.dart';
|
import 'package:solian/widgets/root_container.dart';
|
||||||
import 'package:solian/widgets/sized_container.dart';
|
import 'package:solian/widgets/sized_container.dart';
|
||||||
|
|
||||||
@ -93,7 +93,7 @@ class _RealmListScreenState extends State<RealmListScreen> {
|
|||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
LoadingIndicator(isActive: _isBusy),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: CenteredContainer(
|
child: CenteredContainer(
|
||||||
child: RefreshIndicator(
|
child: RefreshIndicator(
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_animate/flutter_animate.dart';
|
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:image_cropper/image_cropper.dart';
|
import 'package:image_cropper/image_cropper.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
@ -13,6 +12,7 @@ import 'package:solian/router.dart';
|
|||||||
import 'package:solian/theme.dart';
|
import 'package:solian/theme.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/loading_indicator.dart';
|
||||||
import 'package:solian/widgets/root_container.dart';
|
import 'package:solian/widgets/root_container.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
@ -208,7 +208,7 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
|
|||||||
top: false,
|
top: false,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
LoadingIndicator(isActive: _isBusy),
|
||||||
if (widget.edit != null)
|
if (widget.edit != null)
|
||||||
MaterialBanner(
|
MaterialBanner(
|
||||||
leading: const Icon(Icons.edit),
|
leading: const Icon(Icons.edit),
|
||||||
|
@ -43,7 +43,10 @@ class RootShell extends StatelessWidget {
|
|||||||
|
|
||||||
final showRailNavigation = AppTheme.isLargeScreen(context);
|
final showRailNavigation = AppTheme.isLargeScreen(context);
|
||||||
|
|
||||||
final destNames = AppNavigation.destinations.map((x) => x.page).toList();
|
final destNames = [
|
||||||
|
'postDetail',
|
||||||
|
...AppNavigation.destinations.map((x) => x.page),
|
||||||
|
];
|
||||||
final showBottomNavigation =
|
final showBottomNavigation =
|
||||||
destNames.contains(routeName) && !showRailNavigation;
|
destNames.contains(routeName) && !showRailNavigation;
|
||||||
|
|
||||||
@ -52,13 +55,22 @@ class RootShell extends StatelessWidget {
|
|||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
bottomNavigationBar: showBottomNavigation
|
bottomNavigationBar: showBottomNavigation
|
||||||
? AppNavigationBottom(
|
? AppNavigationBottom(
|
||||||
initialIndex: destNames.indexOf(routeName ?? 'page'),
|
initialIndex: AppNavigation.destinations
|
||||||
|
.map((x) => x.page)
|
||||||
|
.toList()
|
||||||
|
.indexOf(routeName ?? 'page'),
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
body: AppTheme.isLargeScreen(context)
|
body: AppTheme.isLargeScreen(context)
|
||||||
? Row(
|
? Row(
|
||||||
children: [
|
children: [
|
||||||
if (showRailNavigation) const AppNavigationRail(),
|
if (showRailNavigation)
|
||||||
|
AppNavigationRail(
|
||||||
|
initialIndex: AppNavigation.destinations
|
||||||
|
.map((x) => x.page)
|
||||||
|
.toList()
|
||||||
|
.indexOf(routeName ?? 'page'),
|
||||||
|
),
|
||||||
if (showRailNavigation)
|
if (showRailNavigation)
|
||||||
const VerticalDivider(
|
const VerticalDivider(
|
||||||
width: 0.3,
|
width: 0.3,
|
||||||
|
@ -89,8 +89,7 @@ class _AccountProfilePopupState extends State<AccountProfilePopup> {
|
|||||||
|
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: MediaQuery.of(context).size.height * 0.75,
|
height: MediaQuery.of(context).size.height * 0.75,
|
||||||
child: Column(
|
child: ListView(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
children: [
|
||||||
AccountHeadingWidget(
|
AccountHeadingWidget(
|
||||||
avatar: _userinfo!.avatar,
|
avatar: _userinfo!.avatar,
|
||||||
|
@ -21,6 +21,7 @@ import 'package:solian/providers/content/attachment.dart';
|
|||||||
import 'package:solian/widgets/attachments/attachment_attr_editor.dart';
|
import 'package:solian/widgets/attachments/attachment_attr_editor.dart';
|
||||||
import 'package:solian/widgets/attachments/attachment_editor_thumbnail.dart';
|
import 'package:solian/widgets/attachments/attachment_editor_thumbnail.dart';
|
||||||
import 'package:solian/widgets/attachments/attachment_fullscreen.dart';
|
import 'package:solian/widgets/attachments/attachment_fullscreen.dart';
|
||||||
|
import 'package:solian/widgets/loading_indicator.dart';
|
||||||
|
|
||||||
class AttachmentEditorPopup extends StatefulWidget {
|
class AttachmentEditorPopup extends StatefulWidget {
|
||||||
final String pool;
|
final String pool;
|
||||||
@ -32,12 +33,14 @@ class AttachmentEditorPopup extends StatefulWidget {
|
|||||||
final List<String>? initialAttachments;
|
final List<String>? initialAttachments;
|
||||||
final void Function(String) onAdd;
|
final void Function(String) onAdd;
|
||||||
final void Function(String) onRemove;
|
final void Function(String) onRemove;
|
||||||
|
final void Function(String)? onInsert;
|
||||||
|
|
||||||
const AttachmentEditorPopup({
|
const AttachmentEditorPopup({
|
||||||
super.key,
|
super.key,
|
||||||
required this.pool,
|
required this.pool,
|
||||||
required this.onAdd,
|
required this.onAdd,
|
||||||
required this.onRemove,
|
required this.onRemove,
|
||||||
|
this.onInsert,
|
||||||
this.singleMode = false,
|
this.singleMode = false,
|
||||||
this.imageOnly = false,
|
this.imageOnly = false,
|
||||||
this.autoUpload = false,
|
this.autoUpload = false,
|
||||||
@ -228,7 +231,10 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
|
|||||||
.listMetadata(widget.initialAttachments ?? List.empty())
|
.listMetadata(widget.initialAttachments ?? List.empty())
|
||||||
.then((result) {
|
.then((result) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_attachments = List.from(result, growable: true);
|
_attachments = List.from(
|
||||||
|
result.where((x) => x != null),
|
||||||
|
growable: true,
|
||||||
|
);
|
||||||
_isBusy = false;
|
_isBusy = false;
|
||||||
_isFirstTimeBusy = false;
|
_isFirstTimeBusy = false;
|
||||||
});
|
});
|
||||||
@ -553,6 +559,22 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
|
|||||||
setState(() => _attachments.removeAt(idx));
|
setState(() => _attachments.removeAt(idx));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
if (widget.onInsert != null)
|
||||||
|
PopupMenuItem(
|
||||||
|
child: ListTile(
|
||||||
|
title: Text('insert'.tr),
|
||||||
|
leading: const Icon(Icons.insert_link),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
widget.onInsert!(
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -660,7 +682,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
|
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
|
||||||
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
LoadingIndicator(isActive: _isBusy),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: CustomScrollView(
|
child: CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
|
@ -8,6 +8,7 @@ import 'package:flutter_animate/flutter_animate.dart';
|
|||||||
import 'package:gal/gal.dart';
|
import 'package:gal/gal.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.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/platform.dart';
|
||||||
@ -103,9 +104,10 @@ class _AttachmentFullScreenState extends State<AttachmentFullScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final metaTextStyle = TextStyle(
|
final metaTextStyle = GoogleFonts.roboto(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: _unFocusColor,
|
color: _unFocusColor,
|
||||||
|
height: 1,
|
||||||
);
|
);
|
||||||
|
|
||||||
return DismissiblePage(
|
return DismissiblePage(
|
||||||
@ -239,25 +241,58 @@ class _AttachmentFullScreenState extends State<AttachmentFullScreen> {
|
|||||||
child: Wrap(
|
child: Wrap(
|
||||||
spacing: 6,
|
spacing: 6,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
if (widget.item.metadata?['exif'] == null)
|
||||||
'#${widget.item.rid}',
|
|
||||||
style: metaTextStyle,
|
|
||||||
),
|
|
||||||
if (widget.item.metadata?['width'] != null &&
|
|
||||||
widget.item.metadata?['height'] != null)
|
|
||||||
Text(
|
Text(
|
||||||
'${widget.item.metadata?['width']}x${widget.item.metadata?['height']}',
|
'#${widget.item.rid}',
|
||||||
style: metaTextStyle,
|
style: metaTextStyle,
|
||||||
),
|
),
|
||||||
|
if (widget.item.metadata?['exif']?['Model'] != null)
|
||||||
|
Text(
|
||||||
|
'shotOn'.trParams({
|
||||||
|
'device': widget.item.metadata?['exif']
|
||||||
|
?['Model']
|
||||||
|
}),
|
||||||
|
style: metaTextStyle,
|
||||||
|
).paddingOnly(right: 2),
|
||||||
|
if (widget.item.metadata?['exif']?['ShutterSpeed'] !=
|
||||||
|
null)
|
||||||
|
Text(
|
||||||
|
widget.item.metadata?['exif']?['ShutterSpeed'],
|
||||||
|
style: metaTextStyle,
|
||||||
|
).paddingOnly(right: 2),
|
||||||
|
if (widget.item.metadata?['exif']?['ISO'] != null)
|
||||||
|
Text(
|
||||||
|
'ISO${widget.item.metadata?['exif']?['ISO']}',
|
||||||
|
style: metaTextStyle,
|
||||||
|
).paddingOnly(right: 2),
|
||||||
|
if (widget.item.metadata?['exif']?['Aperture'] !=
|
||||||
|
null)
|
||||||
|
Text(
|
||||||
|
'f/${widget.item.metadata?['exif']?['Aperture']}',
|
||||||
|
style: metaTextStyle,
|
||||||
|
).paddingOnly(right: 2),
|
||||||
|
if (widget.item.metadata?['exif']?['Megapixels'] !=
|
||||||
|
null &&
|
||||||
|
widget.item.metadata?['exif']?['Model'] != null)
|
||||||
|
Text(
|
||||||
|
'${widget.item.metadata?['exif']?['Megapixels']}MP',
|
||||||
|
style: metaTextStyle,
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Text(
|
||||||
|
widget.item.size.formatBytes(),
|
||||||
|
style: metaTextStyle,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'${widget.item.metadata?['width']}x${widget.item.metadata?['height']}',
|
||||||
|
style: metaTextStyle,
|
||||||
|
),
|
||||||
if (widget.item.metadata?['ratio'] != null)
|
if (widget.item.metadata?['ratio'] != null)
|
||||||
Text(
|
Text(
|
||||||
'${_getRatio().toPrecision(2)}',
|
(widget.item.metadata?['ratio'] as num)
|
||||||
|
.toStringAsFixed(2),
|
||||||
style: metaTextStyle,
|
style: metaTextStyle,
|
||||||
),
|
),
|
||||||
Text(
|
|
||||||
widget.item.size.formatBytes(),
|
|
||||||
style: metaTextStyle,
|
|
||||||
),
|
|
||||||
Text(
|
Text(
|
||||||
widget.item.mimetype,
|
widget.item.mimetype,
|
||||||
style: metaTextStyle,
|
style: metaTextStyle,
|
||||||
|
@ -155,11 +155,18 @@ class _AttachmentItemImage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
if (showBadge && badge != null)
|
if (showBadge && badge != null)
|
||||||
Positioned(
|
Positioned(
|
||||||
right: 12,
|
right: 8,
|
||||||
bottom: 8,
|
bottom: 4,
|
||||||
child: Material(
|
child: Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
child: Chip(label: Text(badge!)),
|
child: Chip(
|
||||||
|
label: Text(badge!),
|
||||||
|
labelStyle: GoogleFonts.robotoMono(),
|
||||||
|
visualDensity: const VisualDensity(
|
||||||
|
horizontal: -4,
|
||||||
|
vertical: -2,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (showHideButton && item.isMature)
|
if (showHideButton && item.isMature)
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:carousel_slider/carousel_slider.dart';
|
|
||||||
import 'package:dismissible_page/dismissible_page.dart';
|
import 'package:dismissible_page/dismissible_page.dart';
|
||||||
import 'package:flutter/material.dart' hide CarouselController;
|
import 'package:flutter/material.dart' hide CarouselController;
|
||||||
import 'package:flutter_animate/flutter_animate.dart';
|
import 'package:flutter_animate/flutter_animate.dart';
|
||||||
@ -23,6 +22,7 @@ class AttachmentList extends StatefulWidget {
|
|||||||
final bool autoload;
|
final bool autoload;
|
||||||
final double columnMaxWidth;
|
final double columnMaxWidth;
|
||||||
|
|
||||||
|
final EdgeInsets? padding;
|
||||||
final double? width;
|
final double? width;
|
||||||
final double? viewport;
|
final double? viewport;
|
||||||
|
|
||||||
@ -36,6 +36,7 @@ class AttachmentList extends StatefulWidget {
|
|||||||
this.isFullWidth = false,
|
this.isFullWidth = false,
|
||||||
this.autoload = false,
|
this.autoload = false,
|
||||||
this.columnMaxWidth = 480,
|
this.columnMaxWidth = 480,
|
||||||
|
this.padding,
|
||||||
this.width,
|
this.width,
|
||||||
this.viewport,
|
this.viewport,
|
||||||
});
|
});
|
||||||
@ -48,6 +49,7 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
bool _isLoading = true;
|
bool _isLoading = true;
|
||||||
bool _showMature = false;
|
bool _showMature = false;
|
||||||
|
|
||||||
|
// ignore: unused_field
|
||||||
double _aspectRatio = 1;
|
double _aspectRatio = 1;
|
||||||
|
|
||||||
List<Attachment?> _attachments = List.empty();
|
List<Attachment?> _attachments = List.empty();
|
||||||
@ -133,7 +135,17 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
super.initState();
|
super.initState();
|
||||||
assert(widget.attachmentIds != null || widget.attachments != null);
|
assert(widget.attachmentIds != null || widget.attachments != null);
|
||||||
if (widget.attachments == null) {
|
if (widget.attachments == null) {
|
||||||
_getMetadataList();
|
final AttachmentProvider attach = Get.find();
|
||||||
|
final cachedResult = attach.listMetadataFromCache(widget.attachmentIds!);
|
||||||
|
if (cachedResult.every((x) => x != null)) {
|
||||||
|
setState(() {
|
||||||
|
_attachments = cachedResult;
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
_calculateAspectRatio();
|
||||||
|
} else {
|
||||||
|
_getMetadataList();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
setState(() {
|
setState(() {
|
||||||
_attachments = widget.attachments!;
|
_attachments = widget.attachments!;
|
||||||
@ -161,9 +173,7 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
color: _unFocusColor,
|
color: _unFocusColor,
|
||||||
).paddingOnly(right: 5),
|
).paddingOnly(right: 5),
|
||||||
Text(
|
Text(
|
||||||
'attachmentHint'.trParams(
|
'attachmentHint'.trParams({'count': _attachments.toString()}),
|
||||||
{'count': _attachments.toString()},
|
|
||||||
),
|
|
||||||
style: TextStyle(color: _unFocusColor, fontSize: 12),
|
style: TextStyle(color: _unFocusColor, fontSize: 12),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@ -177,16 +187,22 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
|
|
||||||
if (widget.isFullWidth && _attachments.length == 1) {
|
if (widget.isFullWidth && _attachments.length == 1) {
|
||||||
final element = _attachments.first;
|
final element = _attachments.first;
|
||||||
double ratio = element!.metadata?['ratio']?.toDouble() ?? 16 / 9;
|
final isImage = element!.mimetype.split('/').firstOrNull == 'image';
|
||||||
|
double ratio =
|
||||||
|
element.metadata?['ratio']?.toDouble() ?? (isImage ? 1 : 16 / 9);
|
||||||
return Container(
|
return Container(
|
||||||
|
width: MediaQuery.of(context).size.width,
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
maxWidth: widget.columnMaxWidth,
|
|
||||||
maxHeight: 640,
|
maxHeight: 640,
|
||||||
),
|
),
|
||||||
child: AspectRatio(
|
child: AspectRatio(
|
||||||
aspectRatio: ratio,
|
aspectRatio: ratio,
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.surfaceContainer
|
||||||
|
.withOpacity(0.5),
|
||||||
border: Border.symmetric(
|
border: Border.symmetric(
|
||||||
horizontal: BorderSide(
|
horizontal: BorderSide(
|
||||||
color: Theme.of(context).dividerColor,
|
color: Theme.of(context).dividerColor,
|
||||||
@ -219,7 +235,10 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
final element = _attachments[idx];
|
final element = _attachments[idx];
|
||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.surfaceContainer
|
||||||
|
.withOpacity(0.5),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: Theme.of(context).dividerColor,
|
color: Theme.of(context).dividerColor,
|
||||||
width: 1,
|
width: 1,
|
||||||
@ -244,7 +263,9 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
final element = _attachments[idx];
|
final element = _attachments[idx];
|
||||||
idx++;
|
idx++;
|
||||||
if (element == null) return const SizedBox.shrink();
|
if (element == null) return const SizedBox.shrink();
|
||||||
double ratio = element.metadata?['ratio']?.toDouble() ?? 16 / 9;
|
final isImage = element.mimetype.split('/').firstOrNull == 'image';
|
||||||
|
double ratio =
|
||||||
|
element.metadata?['ratio']?.toDouble() ?? (isImage ? 1 : 16 / 9);
|
||||||
return Container(
|
return Container(
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
maxWidth: widget.columnMaxWidth,
|
maxWidth: widget.columnMaxWidth,
|
||||||
@ -254,6 +275,10 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
aspectRatio: ratio,
|
aspectRatio: ratio,
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.surfaceContainer
|
||||||
|
.withOpacity(0.5),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: Theme.of(context).dividerColor,
|
color: Theme.of(context).dividerColor,
|
||||||
width: 1,
|
width: 1,
|
||||||
@ -271,31 +296,37 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return SizedBox(
|
return Container(
|
||||||
width: math.min(MediaQuery.of(context).size.width, widget.columnMaxWidth),
|
constraints: BoxConstraints(
|
||||||
child: CarouselSlider.builder(
|
maxHeight: 320,
|
||||||
options: CarouselOptions(
|
),
|
||||||
disableCenter: true,
|
child: ListView.separated(
|
||||||
animateToClosest: true,
|
padding: widget.padding,
|
||||||
aspectRatio: _aspectRatio,
|
scrollDirection: Axis.horizontal,
|
||||||
enlargeCenterPage: true,
|
shrinkWrap: true,
|
||||||
viewportFraction: widget.viewport ?? 0.95,
|
|
||||||
enableInfiniteScroll: false,
|
|
||||||
),
|
|
||||||
itemCount: _attachments.length,
|
itemCount: _attachments.length,
|
||||||
itemBuilder: (context, idx, _) {
|
itemBuilder: (context, idx) {
|
||||||
final element = _attachments[idx];
|
final element = _attachments[idx];
|
||||||
if (element == null) const SizedBox.shrink();
|
if (element == null) const SizedBox.shrink();
|
||||||
double ratio = element!.metadata?['ratio']?.toDouble() ?? 16 / 9;
|
final isImage = element!.mimetype.split('/').firstOrNull == 'image';
|
||||||
|
double ratio =
|
||||||
|
element.metadata?['ratio']?.toDouble() ?? (isImage ? 1 : 16 / 9);
|
||||||
return Container(
|
return Container(
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
maxWidth: widget.columnMaxWidth,
|
maxWidth: math.min(
|
||||||
maxHeight: 640,
|
widget.columnMaxWidth,
|
||||||
|
MediaQuery.of(context).size.width -
|
||||||
|
(widget.padding?.horizontal ?? 0),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: AspectRatio(
|
child: AspectRatio(
|
||||||
aspectRatio: ratio,
|
aspectRatio: ratio,
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.surfaceContainer
|
||||||
|
.withOpacity(0.5),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: Theme.of(context).dividerColor,
|
color: Theme.of(context).dividerColor,
|
||||||
width: 1,
|
width: 1,
|
||||||
@ -310,6 +341,7 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
separatorBuilder: (context, _) => const Gap(8),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -388,11 +420,13 @@ class AttachmentListEntry extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (item!.isMature && !showMature)
|
if (item!.isMature && !showMature)
|
||||||
BackdropFilter(
|
ClipRect(
|
||||||
filter: ImageFilter.blur(sigmaX: 100, sigmaY: 100),
|
child: BackdropFilter(
|
||||||
child: Container(
|
filter: ImageFilter.blur(sigmaX: 100, sigmaY: 100),
|
||||||
decoration: BoxDecoration(
|
child: Container(
|
||||||
color: Colors.black.withOpacity(0.5),
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black.withOpacity(0.5),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -38,11 +38,13 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
|
|||||||
|
|
||||||
Future<void> _loadLastMessages() async {
|
Future<void> _loadLastMessages() async {
|
||||||
final messages = await _eventController.src.getLastInAllChannels();
|
final messages = await _eventController.src.getLastInAllChannels();
|
||||||
setState(() {
|
if (mounted) {
|
||||||
_lastMessages = messages
|
setState(() {
|
||||||
.map((k, v) => MapEntry(k, v.firstOrNull))
|
_lastMessages = messages
|
||||||
.cast<int, LocalMessageEventTableData>();
|
.map((k, v) => MapEntry(k, v.firstOrNull))
|
||||||
});
|
.cast<int, LocalMessageEventTableData>();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_animate/flutter_animate.dart';
|
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:solian/exts.dart';
|
import 'package:solian/exts.dart';
|
||||||
import 'package:solian/models/channel.dart';
|
import 'package:solian/models/channel.dart';
|
||||||
@ -8,6 +7,7 @@ import 'package:solian/services.dart';
|
|||||||
import 'package:solian/widgets/account/account_avatar.dart';
|
import 'package:solian/widgets/account/account_avatar.dart';
|
||||||
import 'package:solian/widgets/account/account_profile_popup.dart';
|
import 'package:solian/widgets/account/account_profile_popup.dart';
|
||||||
import 'package:solian/widgets/account/relative_select.dart';
|
import 'package:solian/widgets/account/relative_select.dart';
|
||||||
|
import 'package:solian/widgets/loading_indicator.dart';
|
||||||
|
|
||||||
class ChannelMemberListPopup extends StatefulWidget {
|
class ChannelMemberListPopup extends StatefulWidget {
|
||||||
final Channel channel;
|
final Channel channel;
|
||||||
@ -131,7 +131,7 @@ class _ChannelMemberListPopupState extends State<ChannelMemberListPopup> {
|
|||||||
'channelMembers'.tr,
|
'channelMembers'.tr,
|
||||||
style: Theme.of(context).textTheme.headlineSmall,
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
|
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
|
||||||
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
LoadingIndicator(isActive: _isBusy),
|
||||||
ListTile(
|
ListTile(
|
||||||
tileColor: Theme.of(context).colorScheme.surfaceContainerHigh,
|
tileColor: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_animate/flutter_animate.dart';
|
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:solian/models/channel.dart';
|
import 'package:solian/models/channel.dart';
|
||||||
@ -7,6 +6,7 @@ import 'package:solian/models/event.dart';
|
|||||||
import 'package:solian/models/realm.dart';
|
import 'package:solian/models/realm.dart';
|
||||||
import 'package:solian/providers/auth.dart';
|
import 'package:solian/providers/auth.dart';
|
||||||
import 'package:solian/widgets/chat/chat_event_deletion.dart';
|
import 'package:solian/widgets/chat/chat_event_deletion.dart';
|
||||||
|
import 'package:solian/widgets/loading_indicator.dart';
|
||||||
|
|
||||||
class ChatEventAction extends StatefulWidget {
|
class ChatEventAction extends StatefulWidget {
|
||||||
final Channel channel;
|
final Channel channel;
|
||||||
@ -73,7 +73,7 @@ class _ChatEventActionState extends State<ChatEventAction> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
|
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
|
||||||
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
LoadingIndicator(isActive: _isBusy),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ListView(
|
child: ListView(
|
||||||
children: [
|
children: [
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:flutter/gestures.dart';
|
||||||
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';
|
||||||
@ -34,9 +35,28 @@ class ChatEventList extends StatelessWidget {
|
|||||||
return a.createdAt.difference(b.createdAt).inMinutes <= 3;
|
return a.createdAt.difference(b.createdAt).inMinutes <= 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _openActions(BuildContext context, Event item) {
|
||||||
|
showModalBottomSheet(
|
||||||
|
useRootNavigator: true,
|
||||||
|
context: context,
|
||||||
|
builder: (context) => ChatEventAction(
|
||||||
|
channel: channel,
|
||||||
|
realm: channel.realm,
|
||||||
|
item: item,
|
||||||
|
onEdit: () {
|
||||||
|
onEdit(item);
|
||||||
|
},
|
||||||
|
onReply: () {
|
||||||
|
onReply(item);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return CustomScrollView(
|
return CustomScrollView(
|
||||||
|
cacheExtent: 100,
|
||||||
reverse: true,
|
reverse: true,
|
||||||
slivers: [
|
slivers: [
|
||||||
Obx(() {
|
Obx(() {
|
||||||
@ -64,50 +84,45 @@ class ChatEventList extends StatelessWidget {
|
|||||||
|
|
||||||
final item = chatController.currentEvents[index].data;
|
final item = chatController.currentEvents[index].data;
|
||||||
|
|
||||||
return GestureDetector(
|
return TapRegion(
|
||||||
behavior: HitTestBehavior.opaque,
|
child: GestureDetector(
|
||||||
child: Builder(builder: (context) {
|
behavior: HitTestBehavior.opaque,
|
||||||
final widget = ChatEvent(
|
child: Builder(builder: (context) {
|
||||||
key: Key('m${item!.uuid}'),
|
final widget = ChatEvent(
|
||||||
item: item,
|
key: Key('m${item!.uuid}'),
|
||||||
isMerged: isMerged,
|
item: item,
|
||||||
chatController: chatController,
|
isMerged: isMerged,
|
||||||
).paddingOnly(
|
chatController: chatController,
|
||||||
top: !isMerged ? 8 : 0,
|
).paddingOnly(
|
||||||
bottom: !hasMerged ? 8 : 0,
|
top: !isMerged ? 8 : 0,
|
||||||
);
|
bottom: !hasMerged ? 8 : 0,
|
||||||
|
);
|
||||||
|
|
||||||
if (noAnimated) {
|
if (noAnimated) {
|
||||||
return widget;
|
return widget;
|
||||||
} else {
|
} else {
|
||||||
return widget
|
return widget
|
||||||
.animate(
|
.animate(
|
||||||
key: Key('animated-m${item.uuid}'),
|
key: Key('animated-m${item.uuid}'),
|
||||||
)
|
)
|
||||||
.slideY(
|
.slideY(
|
||||||
curve: Curves.fastLinearToSlowEaseIn,
|
curve: Curves.fastLinearToSlowEaseIn,
|
||||||
duration: 250.ms,
|
duration: 250.ms,
|
||||||
begin: 0.5,
|
begin: 0.5,
|
||||||
end: 0,
|
end: 0,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
onLongPress: () {
|
||||||
|
_openActions(context, item!);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
onTapInside: (event) {
|
||||||
|
if (event.buttons == kSecondaryMouseButton) {
|
||||||
|
_openActions(context, item!);
|
||||||
|
} else if (event.buttons == kMiddleMouseButton) {
|
||||||
|
onReply(item!);
|
||||||
}
|
}
|
||||||
}),
|
|
||||||
onLongPress: () {
|
|
||||||
showModalBottomSheet(
|
|
||||||
useRootNavigator: true,
|
|
||||||
context: context,
|
|
||||||
builder: (context) => ChatEventAction(
|
|
||||||
channel: channel,
|
|
||||||
realm: channel.realm,
|
|
||||||
item: item!,
|
|
||||||
onEdit: () {
|
|
||||||
onEdit(item);
|
|
||||||
},
|
|
||||||
onReply: () {
|
|
||||||
onReply(item);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -2,15 +2,21 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||||
import 'package:flutter_svg/svg.dart';
|
import 'package:flutter_svg/svg.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:solian/models/link.dart';
|
||||||
import 'package:solian/providers/link_expander.dart';
|
import 'package:solian/providers/link_expander.dart';
|
||||||
import 'package:solian/widgets/auto_cache_image.dart';
|
import 'package:solian/widgets/auto_cache_image.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
class LinkExpansion extends StatelessWidget {
|
class LinkExpansion extends StatefulWidget {
|
||||||
final String content;
|
final String content;
|
||||||
|
|
||||||
const LinkExpansion({super.key, required this.content});
|
const LinkExpansion({super.key, required this.content});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<LinkExpansion> createState() => _LinkExpansionState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LinkExpansionState extends State<LinkExpansion> {
|
||||||
Widget _buildImage(String url, {double? width, double? height}) {
|
Widget _buildImage(String url, {double? width, double? height}) {
|
||||||
if (url.endsWith('svg')) {
|
if (url.endsWith('svg')) {
|
||||||
return SvgPicture.network(url, width: width, height: height);
|
return SvgPicture.network(url, width: width, height: height);
|
||||||
@ -22,61 +28,74 @@ class LinkExpansion extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
List<LinkMeta>? _meta;
|
||||||
Widget build(BuildContext context) {
|
|
||||||
|
Future<void> _doExpand() async {
|
||||||
final linkRegex = RegExp(
|
final linkRegex = RegExp(
|
||||||
r'(?<!\()(?:(?:https?):\/\/|www\.)(?:[-_a-z0-9]+\.)*(?:[-a-z0-9]+\.[-a-z0-9]+)[^\s<]*[^\s<?!.,:*_~]',
|
r'(?<!\()(?:(?:https?):\/\/|www\.)(?:[-_a-z0-9]+\.)*(?:[-a-z0-9]+\.[-a-z0-9]+)[^\s<]*[^\s<?!.,:*_~]',
|
||||||
);
|
);
|
||||||
final matches = linkRegex.allMatches(content);
|
final matches = linkRegex.allMatches(widget.content);
|
||||||
if (matches.isEmpty) {
|
if (matches.isEmpty) return;
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
|
|
||||||
final LinkExpandProvider expandController = Get.find();
|
final LinkExpandProvider expandController = Get.find();
|
||||||
|
|
||||||
|
if (matches.isEmpty) return;
|
||||||
|
|
||||||
|
List<LinkMeta> out = List.empty(growable: true);
|
||||||
|
for (final x in matches) {
|
||||||
|
final result = await expandController.expandLink(x.group(0)!);
|
||||||
|
if (result != null) out.add(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() => _meta = out);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_doExpand();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (_meta?.isEmpty ?? true) return const SizedBox.shrink();
|
||||||
|
|
||||||
return Wrap(
|
return Wrap(
|
||||||
children: matches.map((x) {
|
children: _meta!.map((x) {
|
||||||
return Container(
|
return Container(
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
maxWidth: matches.length == 1 ? 480 : 340,
|
maxWidth: _meta!.length == 1 ? 480 : 340,
|
||||||
),
|
),
|
||||||
child: FutureBuilder(
|
child: Builder(
|
||||||
future: expandController.expandLink(x.group(0)!),
|
builder: (context) {
|
||||||
builder: (context, snapshot) {
|
|
||||||
if (!snapshot.hasData) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
|
|
||||||
final isRichDescription = [
|
final isRichDescription = [
|
||||||
'solsynth.dev',
|
'solsynth.dev',
|
||||||
].contains(Uri.parse(snapshot.data!.url).host);
|
].contains(Uri.parse(x.url).host);
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
child: Card(
|
child: Card(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if ([
|
if ([(x.icon?.isNotEmpty ?? false), x.siteName != null]
|
||||||
(snapshot.data!.icon?.isNotEmpty ?? false),
|
.any((x) => x))
|
||||||
snapshot.data!.siteName != null
|
|
||||||
].any((x) => x))
|
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
if (snapshot.data!.icon?.isNotEmpty ?? false)
|
if (x.icon?.isNotEmpty ?? false)
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: const BorderRadius.all(
|
borderRadius: const BorderRadius.all(
|
||||||
Radius.circular(8),
|
Radius.circular(8),
|
||||||
),
|
),
|
||||||
child: _buildImage(
|
child: _buildImage(
|
||||||
snapshot.data!.icon!,
|
x.icon!,
|
||||||
width: 32,
|
width: 32,
|
||||||
height: 32,
|
height: 32,
|
||||||
),
|
),
|
||||||
).paddingOnly(right: 8),
|
).paddingOnly(right: 8),
|
||||||
if (snapshot.data!.siteName != null)
|
if (x.siteName != null)
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
snapshot.data!.siteName!,
|
x.siteName!,
|
||||||
style: Theme.of(context).textTheme.labelLarge,
|
style: Theme.of(context).textTheme.labelLarge,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
@ -84,32 +103,27 @@ class LinkExpansion extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
).paddingOnly(
|
).paddingOnly(
|
||||||
bottom: (snapshot.data!.icon?.isNotEmpty ?? false)
|
bottom: (x.icon?.isNotEmpty ?? false) ? 8 : 4,
|
||||||
? 8
|
|
||||||
: 4,
|
|
||||||
),
|
),
|
||||||
if (snapshot.data!.image != null &&
|
if (x.image != null &&
|
||||||
(snapshot.data!.image?.startsWith('http') ?? false))
|
(x.image?.startsWith('http') ?? false))
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: const BorderRadius.all(
|
borderRadius: const BorderRadius.all(
|
||||||
Radius.circular(8),
|
Radius.circular(8),
|
||||||
),
|
),
|
||||||
child: _buildImage(
|
child: _buildImage(x.image!),
|
||||||
snapshot.data!.image!,
|
|
||||||
),
|
|
||||||
).paddingOnly(bottom: 8),
|
).paddingOnly(bottom: 8),
|
||||||
Text(
|
Text(
|
||||||
snapshot.data!.title ?? 'No Title',
|
x.title ?? 'No Title',
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.fade,
|
overflow: TextOverflow.fade,
|
||||||
style: Theme.of(context).textTheme.bodyLarge,
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
),
|
),
|
||||||
if (snapshot.data!.description != null &&
|
if (x.description != null && isRichDescription)
|
||||||
isRichDescription)
|
MarkdownBody(data: x.description!)
|
||||||
MarkdownBody(data: snapshot.data!.description!)
|
else if (x.description != null)
|
||||||
else if (snapshot.data!.description != null)
|
|
||||||
Text(
|
Text(
|
||||||
snapshot.data!.description!,
|
x.description!,
|
||||||
maxLines: 3,
|
maxLines: 3,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
@ -117,7 +131,7 @@ class LinkExpansion extends StatelessWidget {
|
|||||||
).paddingAll(12),
|
).paddingAll(12),
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
launchUrlString(x.group(0)!);
|
launchUrlString(x.url);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
89
lib/widgets/loading_indicator.dart
Normal file
89
lib/widgets/loading_indicator.dart
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
|
||||||
|
class LoadingIndicator extends StatefulWidget {
|
||||||
|
final bool isActive;
|
||||||
|
final Color? backgroundColor;
|
||||||
|
|
||||||
|
const LoadingIndicator({
|
||||||
|
super.key,
|
||||||
|
this.isActive = true,
|
||||||
|
this.backgroundColor,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<LoadingIndicator> createState() => _LoadingIndicatorState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LoadingIndicatorState extends State<LoadingIndicator>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late AnimationController _controller;
|
||||||
|
late Animation<double> _animation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
_controller = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
);
|
||||||
|
|
||||||
|
_animation = CurvedAnimation(
|
||||||
|
parent: _controller,
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (widget.isActive) {
|
||||||
|
_controller.forward();
|
||||||
|
} else {
|
||||||
|
_controller.reverse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant LoadingIndicator oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
|
||||||
|
if (widget.isActive != oldWidget.isActive) {
|
||||||
|
if (widget.isActive) {
|
||||||
|
_controller.forward();
|
||||||
|
} else {
|
||||||
|
_controller.reverse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizeTransition(
|
||||||
|
sizeFactor: _animation,
|
||||||
|
axisAlignment: -1, // Align animation from the top
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 24),
|
||||||
|
color: widget.backgroundColor ??
|
||||||
|
Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(0.5),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
width: 16,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2.5),
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
Text('loading'.tr),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -2,12 +2,13 @@ import 'dart:ui';
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||||
import 'package:gap/gap.dart';
|
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:markdown/markdown.dart' as markdown;
|
import 'package:markdown/markdown.dart' as markdown;
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
|
import 'package:solian/models/attachment.dart';
|
||||||
import 'package:solian/providers/stickers.dart';
|
import 'package:solian/providers/stickers.dart';
|
||||||
|
import 'package:solian/widgets/attachments/attachment_item.dart';
|
||||||
import 'package:solian/widgets/attachments/attachment_list.dart';
|
import 'package:solian/widgets/attachments/attachment_list.dart';
|
||||||
import 'package:solian/widgets/auto_cache_image.dart';
|
import 'package:solian/widgets/auto_cache_image.dart';
|
||||||
import 'package:syntax_highlight/syntax_highlight.dart';
|
import 'package:syntax_highlight/syntax_highlight.dart';
|
||||||
@ -15,9 +16,10 @@ import 'package:url_launcher/url_launcher_string.dart';
|
|||||||
|
|
||||||
import 'account/account_profile_popup.dart';
|
import 'account/account_profile_popup.dart';
|
||||||
|
|
||||||
class MarkdownTextContent extends StatelessWidget {
|
class MarkdownTextContent extends StatefulWidget {
|
||||||
final String content;
|
final String content;
|
||||||
final String parentId;
|
final String parentId;
|
||||||
|
final List<Attachment>? attachments;
|
||||||
final bool isSelectable;
|
final bool isSelectable;
|
||||||
final bool isLargeText;
|
final bool isLargeText;
|
||||||
final bool isAutoWarp;
|
final bool isAutoWarp;
|
||||||
@ -26,195 +28,221 @@ class MarkdownTextContent extends StatelessWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
required this.content,
|
required this.content,
|
||||||
required this.parentId,
|
required this.parentId,
|
||||||
|
this.attachments,
|
||||||
this.isSelectable = false,
|
this.isSelectable = false,
|
||||||
this.isLargeText = false,
|
this.isLargeText = false,
|
||||||
this.isAutoWarp = false,
|
this.isAutoWarp = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
Widget _buildContent(BuildContext context) {
|
@override
|
||||||
|
State<MarkdownTextContent> createState() => _MarkdownTextContentState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MarkdownTextContentState extends State<MarkdownTextContent> {
|
||||||
|
final List<int> _stickerSizes = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
initState() {
|
||||||
|
super.initState();
|
||||||
final stickerRegex = RegExp(r':([-\w]+):');
|
final stickerRegex = RegExp(r':([-\w]+):');
|
||||||
|
|
||||||
// Split the content into paragraphs
|
// Split the content into paragraphs
|
||||||
final paragraphs = content.split(RegExp(r'\n\s*\n'));
|
final paragraphs = widget.content.split(RegExp(r'\n\s*\n'));
|
||||||
|
|
||||||
// Iterate over each paragraph to process stickers individually
|
// Iterate over each paragraph to process stickers individually
|
||||||
List<Widget> contentWidgets = [];
|
|
||||||
for (var idx = 0; idx < paragraphs.length; idx++) {
|
for (var idx = 0; idx < paragraphs.length; idx++) {
|
||||||
// Getting paragraph
|
// Getting paragraph
|
||||||
var paragraph = paragraphs[idx];
|
var paragraph = paragraphs[idx];
|
||||||
|
|
||||||
// Matching stickers
|
// Matching stickers
|
||||||
final stickerMatch = stickerRegex.allMatches(paragraph);
|
final stickerMatch = stickerRegex.allMatches(paragraph);
|
||||||
final isOnlySticker =
|
if (stickerMatch.length > 3) {
|
||||||
paragraph.replaceAll(stickerRegex, '').trim().isEmpty;
|
_stickerSizes.addAll(List.filled(stickerMatch.length, 16));
|
||||||
|
} else if (stickerMatch.length > 1) {
|
||||||
contentWidgets.add(
|
_stickerSizes.addAll(List.filled(stickerMatch.length, 32));
|
||||||
Markdown(
|
} else {
|
||||||
shrinkWrap: true,
|
_stickerSizes.addAll(List.filled(stickerMatch.length, 128));
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
|
||||||
data: paragraph,
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
styleSheet: MarkdownStyleSheet.fromTheme(
|
|
||||||
Theme.of(context),
|
|
||||||
).copyWith(
|
|
||||||
textScaler: TextScaler.linear(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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
codeblockDecoration: BoxDecoration(
|
|
||||||
border: Border.all(
|
|
||||||
color: Theme.of(context).dividerColor,
|
|
||||||
width: 0.3,
|
|
||||||
),
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
|
||||||
color: Theme.of(context).colorScheme.surface.withOpacity(0.5),
|
|
||||||
)),
|
|
||||||
builders: {
|
|
||||||
'code': _MarkdownTextCodeElement(),
|
|
||||||
},
|
|
||||||
softLineBreak: true,
|
|
||||||
extensionSet: markdown.ExtensionSet(
|
|
||||||
<markdown.BlockSyntax>[
|
|
||||||
markdown.CodeBlockSyntax(),
|
|
||||||
...markdown.ExtensionSet.commonMark.blockSyntaxes,
|
|
||||||
...markdown.ExtensionSet.gitHubFlavored.blockSyntaxes,
|
|
||||||
],
|
|
||||||
<markdown.InlineSyntax>[
|
|
||||||
if (isAutoWarp) markdown.LineBreakSyntax(),
|
|
||||||
_UserNameCardInlineSyntax(),
|
|
||||||
_CustomEmoteInlineSyntax(),
|
|
||||||
markdown.AutolinkSyntax(),
|
|
||||||
markdown.AutolinkExtensionSyntax(),
|
|
||||||
markdown.CodeSyntax(),
|
|
||||||
...markdown.ExtensionSet.commonMark.inlineSyntaxes,
|
|
||||||
...markdown.ExtensionSet.gitHubFlavored.inlineSyntaxes
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTapLink: (text, href, title) async {
|
|
||||||
if (href == null) return;
|
|
||||||
if (href.startsWith('solink://')) {
|
|
||||||
final segments = href.replaceFirst('solink://', '').split('/');
|
|
||||||
switch (segments[0]) {
|
|
||||||
case 'users':
|
|
||||||
showModalBottomSheet(
|
|
||||||
useRootNavigator: true,
|
|
||||||
isScrollControlled: true,
|
|
||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
|
||||||
context: context,
|
|
||||||
builder: (context) => AccountProfilePopup(
|
|
||||||
name: segments[1],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await launchUrlString(
|
|
||||||
href,
|
|
||||||
mode: LaunchMode.externalApplication,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
imageBuilder: (uri, title, alt) {
|
|
||||||
var url = uri.toString();
|
|
||||||
double? width, height;
|
|
||||||
BoxFit? fit;
|
|
||||||
if (url.startsWith('solink://')) {
|
|
||||||
final segments = url.replaceFirst('solink://', '').split('/');
|
|
||||||
switch (segments[0]) {
|
|
||||||
case 'stickers':
|
|
||||||
double radius = 8;
|
|
||||||
final StickerProvider sticker = Get.find();
|
|
||||||
|
|
||||||
// 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(isAutoWarp ? const Gap(4) : const Gap(8));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Return the list of widgets for the paragraphs
|
Widget _buildContent(BuildContext context) {
|
||||||
return Column(
|
var stickerIdx = 0;
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
return Markdown(
|
||||||
children: contentWidgets,
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
data: widget.content,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
styleSheet: MarkdownStyleSheet.fromTheme(
|
||||||
|
Theme.of(context),
|
||||||
|
).copyWith(
|
||||||
|
textScaler: TextScaler.linear(widget.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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
codeblockDecoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
width: 0.3,
|
||||||
|
),
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
||||||
|
color: Theme.of(context).colorScheme.surface.withOpacity(0.5),
|
||||||
|
)),
|
||||||
|
builders: {
|
||||||
|
'code': _MarkdownTextCodeElement(),
|
||||||
|
},
|
||||||
|
softLineBreak: true,
|
||||||
|
extensionSet: markdown.ExtensionSet(
|
||||||
|
<markdown.BlockSyntax>[
|
||||||
|
markdown.CodeBlockSyntax(),
|
||||||
|
...markdown.ExtensionSet.commonMark.blockSyntaxes,
|
||||||
|
...markdown.ExtensionSet.gitHubFlavored.blockSyntaxes,
|
||||||
|
],
|
||||||
|
<markdown.InlineSyntax>[
|
||||||
|
if (widget.isAutoWarp) markdown.LineBreakSyntax(),
|
||||||
|
_UserNameCardInlineSyntax(),
|
||||||
|
_CustomEmoteInlineSyntax(),
|
||||||
|
markdown.AutolinkSyntax(),
|
||||||
|
markdown.AutolinkExtensionSyntax(),
|
||||||
|
markdown.CodeSyntax(),
|
||||||
|
...markdown.ExtensionSet.commonMark.inlineSyntaxes,
|
||||||
|
...markdown.ExtensionSet.gitHubFlavored.inlineSyntaxes
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTapLink: (text, href, title) async {
|
||||||
|
if (href == null) return;
|
||||||
|
if (href.startsWith('solink://')) {
|
||||||
|
final segments = href.replaceFirst('solink://', '').split('/');
|
||||||
|
switch (segments[0]) {
|
||||||
|
case 'users':
|
||||||
|
showModalBottomSheet(
|
||||||
|
useRootNavigator: true,
|
||||||
|
isScrollControlled: true,
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AccountProfilePopup(
|
||||||
|
name: segments[1],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await launchUrlString(
|
||||||
|
href,
|
||||||
|
mode: LaunchMode.externalApplication,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
imageBuilder: (uri, title, alt) {
|
||||||
|
var url = uri.toString();
|
||||||
|
double? width, height;
|
||||||
|
BoxFit? fit;
|
||||||
|
if (url.startsWith('solink://')) {
|
||||||
|
final segments = url.replaceFirst('solink://', '').split('/');
|
||||||
|
switch (segments[0]) {
|
||||||
|
case 'stickers':
|
||||||
|
double radius = 4;
|
||||||
|
final StickerProvider sticker = Get.find();
|
||||||
|
|
||||||
|
// Adjust sticker size based on the sticker count in this paragraph
|
||||||
|
width =
|
||||||
|
_stickerSizes.elementAtOrNull(stickerIdx)?.toDouble() ?? 16;
|
||||||
|
height =
|
||||||
|
_stickerSizes.elementAtOrNull(stickerIdx)?.toDouble() ?? 16;
|
||||||
|
if (width > 16) {
|
||||||
|
radius = 8;
|
||||||
|
}
|
||||||
|
stickerIdx++;
|
||||||
|
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':
|
||||||
|
final match = widget.attachments
|
||||||
|
?.where((x) => x.rid == segments[1])
|
||||||
|
.firstOrNull;
|
||||||
|
const radius = BorderRadius.all(Radius.circular(8));
|
||||||
|
if (match != null) {
|
||||||
|
final isImage =
|
||||||
|
match.mimetype.split('/').firstOrNull == 'image';
|
||||||
|
double ratio = match.metadata?['ratio']?.toDouble() ??
|
||||||
|
(isImage ? 1 : 16 / 9);
|
||||||
|
return LimitedBox(
|
||||||
|
maxWidth: 480,
|
||||||
|
maxHeight: 640,
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: ratio,
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: radius,
|
||||||
|
child: AttachmentItem(
|
||||||
|
parentId: widget.parentId,
|
||||||
|
item: match,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).paddingSymmetric(vertical: 4);
|
||||||
|
} else {
|
||||||
|
return LimitedBox(
|
||||||
|
maxHeight: MediaQuery.of(context).size.width,
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: radius,
|
||||||
|
child: AttachmentSelfContainedEntry(
|
||||||
|
isDense: true,
|
||||||
|
parentId: widget.parentId,
|
||||||
|
rid: segments[1],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).paddingSymmetric(vertical: 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return AutoCacheImage(
|
||||||
|
url,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
fit: fit,
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (isSelectable) {
|
if (widget.isSelectable) {
|
||||||
return SelectionArea(child: _buildContent(context));
|
return SelectionArea(child: _buildContent(context));
|
||||||
}
|
}
|
||||||
return _buildContent(context);
|
return _buildContent(context);
|
||||||
|
@ -1,17 +1,21 @@
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:file_saver/file_saver.dart';
|
||||||
import 'package:firebase_analytics/firebase_analytics.dart';
|
import 'package:firebase_analytics/firebase_analytics.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_animate/flutter_animate.dart';
|
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:screenshot/screenshot.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
import 'package:solian/exts.dart';
|
import 'package:solian/exts.dart';
|
||||||
import 'package:solian/models/post.dart';
|
import 'package:solian/models/post.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/posts.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/loading_indicator.dart';
|
||||||
|
import 'package:solian/widgets/posts/post_share.dart';
|
||||||
import 'package:solian/widgets/reports/abuse_report.dart';
|
import 'package:solian/widgets/reports/abuse_report.dart';
|
||||||
|
|
||||||
class PostAction extends StatefulWidget {
|
class PostAction extends StatefulWidget {
|
||||||
@ -25,20 +29,14 @@ class PostAction extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _PostActionState extends State<PostAction> {
|
class _PostActionState extends State<PostAction> {
|
||||||
bool _isBusy = true;
|
bool _isBusy = false;
|
||||||
bool _canModifyContent = false;
|
bool _canModifyContent = false;
|
||||||
|
|
||||||
void _checkAbleToModifyContent() async {
|
void _checkAbleToModifyContent() async {
|
||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
if (auth.isAuthorized.isFalse) return;
|
if (auth.isAuthorized.isFalse) return;
|
||||||
|
|
||||||
setState(() => _isBusy = true);
|
_canModifyContent = auth.userProfile.value!['id'] == widget.item.author.id;
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_canModifyContent =
|
|
||||||
auth.userProfile.value!['id'] == widget.item.author.id;
|
|
||||||
_isBusy = false;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _doShare({bool noUri = false}) async {
|
Future<void> _doShare({bool noUri = false}) async {
|
||||||
@ -69,7 +67,8 @@ class _PostActionState extends State<PostAction> {
|
|||||||
'link': 'https://solsynth.dev/posts/$id',
|
'link': 'https://solsynth.dev/posts/$id',
|
||||||
}),
|
}),
|
||||||
subject: 'postShareSubject'.trParams({
|
subject: 'postShareSubject'.trParams({
|
||||||
'username': widget.item.author.nick,
|
'username': '@${widget.item.author.name}',
|
||||||
|
'title': widget.item.body['title'] ?? '#${widget.item.id}',
|
||||||
}),
|
}),
|
||||||
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
|
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
|
||||||
);
|
);
|
||||||
@ -84,6 +83,78 @@ class _PostActionState extends State<PostAction> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _shareImage() async {
|
||||||
|
final List<String> attachments = widget.item.body['attachments'] is List
|
||||||
|
? List.from(widget.item.body['attachments']?.whereType<String>())
|
||||||
|
: List.empty();
|
||||||
|
final hasMultipleAttachment = attachments.length > 1;
|
||||||
|
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
|
final double width = hasMultipleAttachment ? 640 : 480;
|
||||||
|
|
||||||
|
final screenshot = ScreenshotController();
|
||||||
|
final image = await screenshot.captureFromLongWidget(
|
||||||
|
MediaQuery(
|
||||||
|
data: MediaQuery.of(context).copyWith(
|
||||||
|
size: Size(width, double.infinity),
|
||||||
|
),
|
||||||
|
child: PostShareImage(item: widget.item),
|
||||||
|
),
|
||||||
|
context: context,
|
||||||
|
pixelRatio: 2,
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
minWidth: 480,
|
||||||
|
maxWidth: width,
|
||||||
|
minHeight: 640,
|
||||||
|
maxHeight: double.infinity,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final filename = 'share_post#${widget.item.id}';
|
||||||
|
|
||||||
|
if (PlatformInfo.isAndroid || PlatformInfo.isIOS) {
|
||||||
|
final box = context.findRenderObject() as RenderBox?;
|
||||||
|
|
||||||
|
final file = XFile.fromData(
|
||||||
|
image,
|
||||||
|
mimeType: 'image/png',
|
||||||
|
name: filename,
|
||||||
|
);
|
||||||
|
await Share.shareXFiles(
|
||||||
|
[file],
|
||||||
|
subject: 'postShareSubject'.trParams({
|
||||||
|
'username': '@${widget.item.author.name}',
|
||||||
|
'title': widget.item.body['title'] ?? '#${widget.item.id}',
|
||||||
|
}),
|
||||||
|
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
final filepath = await FileSaver.instance.saveFile(
|
||||||
|
name: filename,
|
||||||
|
ext: 'png',
|
||||||
|
mimeType: MimeType.png,
|
||||||
|
bytes: image,
|
||||||
|
);
|
||||||
|
context.showSnackbar('fileSavedAt'.trParams({'path': filepath}));
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Post> _getFullPost() async {
|
||||||
|
final PostProvider posts = Get.find();
|
||||||
|
|
||||||
|
try {
|
||||||
|
final resp = await posts.getPost(widget.item.id.toString());
|
||||||
|
return Post.fromJson(resp.body);
|
||||||
|
} catch (e) {
|
||||||
|
context.showErrorDialog(e).then((_) => Navigator.pop(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
return widget.item;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -127,7 +198,13 @@ class _PostActionState extends State<PostAction> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
|
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
|
||||||
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
LoadingIndicator(
|
||||||
|
isActive: _isBusy,
|
||||||
|
backgroundColor: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.surfaceContainerHigh
|
||||||
|
.withOpacity(0.5),
|
||||||
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ListView(
|
child: ListView(
|
||||||
children: [
|
children: [
|
||||||
@ -135,16 +212,30 @@ class _PostActionState extends State<PostAction> {
|
|||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
leading: const Icon(Icons.share),
|
leading: const Icon(Icons.share),
|
||||||
title: Text('share'.tr),
|
title: Text('share'.tr),
|
||||||
trailing: PlatformInfo.isIOS || PlatformInfo.isAndroid
|
trailing: Row(
|
||||||
? IconButton(
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (PlatformInfo.isIOS || PlatformInfo.isAndroid)
|
||||||
|
IconButton(
|
||||||
icon: const Icon(Icons.link_off),
|
icon: const Icon(Icons.link_off),
|
||||||
tooltip: 'shareNoUri'.tr,
|
tooltip: 'shareNoUri'.tr,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await _doShare(noUri: true);
|
await _doShare(noUri: true);
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
)
|
),
|
||||||
: null,
|
IconButton(
|
||||||
|
icon: const Icon(Icons.image),
|
||||||
|
tooltip: 'shareImage'.tr,
|
||||||
|
onPressed: _isBusy
|
||||||
|
? null
|
||||||
|
: () async {
|
||||||
|
await _shareImage();
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
await _doShare();
|
await _doShare();
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
@ -221,15 +312,23 @@ class _PostActionState extends State<PostAction> {
|
|||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
leading: const Icon(Icons.edit),
|
leading: const Icon(Icons.edit),
|
||||||
title: Text('edit'.tr),
|
title: Text('edit'.tr),
|
||||||
onTap: () async {
|
onTap: _isBusy
|
||||||
Navigator.pop(
|
? null
|
||||||
context,
|
: () async {
|
||||||
AppRouter.instance.pushNamed(
|
setState(() => _isBusy = true);
|
||||||
'postEditor',
|
var item = widget.item;
|
||||||
extra: PostPublishArguments(edit: widget.item),
|
if (item.body?['content_truncated'] == true) {
|
||||||
),
|
item = await _getFullPost();
|
||||||
);
|
}
|
||||||
},
|
Navigator.pop(
|
||||||
|
context,
|
||||||
|
AppRouter.instance.pushNamed(
|
||||||
|
'postEditor',
|
||||||
|
extra: PostPublishArguments(edit: item),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (mounted) setState(() => _isBusy = false);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
if (_canModifyContent)
|
if (_canModifyContent)
|
||||||
ListTile(
|
ListTile(
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import 'package:animations/animations.dart';
|
import 'package:animations/animations.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
|
||||||
import 'package:flutter_animate/flutter_animate.dart';
|
import 'package:flutter_animate/flutter_animate.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';
|
||||||
@ -8,6 +7,7 @@ import 'package:get/get.dart';
|
|||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:solian/models/post.dart';
|
import 'package:solian/models/post.dart';
|
||||||
import 'package:solian/providers/content/posts.dart';
|
import 'package:solian/providers/content/posts.dart';
|
||||||
|
import 'package:solian/router.dart';
|
||||||
import 'package:solian/screens/posts/post_detail.dart';
|
import 'package:solian/screens/posts/post_detail.dart';
|
||||||
import 'package:solian/shells/title_shell.dart';
|
import 'package:solian/shells/title_shell.dart';
|
||||||
import 'package:solian/theme.dart';
|
import 'package:solian/theme.dart';
|
||||||
@ -30,12 +30,15 @@ class PostItem extends StatefulWidget {
|
|||||||
final bool isShowEmbed;
|
final bool isShowEmbed;
|
||||||
final bool isOverrideEmbedClickable;
|
final bool isOverrideEmbedClickable;
|
||||||
final bool isFullDate;
|
final bool isFullDate;
|
||||||
final bool isFullContent;
|
|
||||||
final bool isContentSelectable;
|
final bool isContentSelectable;
|
||||||
|
final bool isNonScrollAttachment;
|
||||||
final bool showFeaturedReply;
|
final bool showFeaturedReply;
|
||||||
final String? attachmentParent;
|
final String? attachmentParent;
|
||||||
final Color? backgroundColor;
|
|
||||||
|
final EdgeInsets? padding;
|
||||||
|
|
||||||
final Function? onComment;
|
final Function? onComment;
|
||||||
|
final Function? onTapMore;
|
||||||
|
|
||||||
const PostItem({
|
const PostItem({
|
||||||
super.key,
|
super.key,
|
||||||
@ -47,12 +50,13 @@ class PostItem extends StatefulWidget {
|
|||||||
this.isShowEmbed = true,
|
this.isShowEmbed = true,
|
||||||
this.isOverrideEmbedClickable = false,
|
this.isOverrideEmbedClickable = false,
|
||||||
this.isFullDate = false,
|
this.isFullDate = false,
|
||||||
this.isFullContent = false,
|
|
||||||
this.isContentSelectable = false,
|
this.isContentSelectable = false,
|
||||||
|
this.isNonScrollAttachment = false,
|
||||||
this.showFeaturedReply = false,
|
this.showFeaturedReply = false,
|
||||||
this.attachmentParent,
|
this.attachmentParent,
|
||||||
this.backgroundColor,
|
this.padding,
|
||||||
this.onComment,
|
this.onComment,
|
||||||
|
this.onTapMore,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -65,14 +69,20 @@ class _PostItemState extends State<PostItem> {
|
|||||||
Color get _unFocusColor =>
|
Color get _unFocusColor =>
|
||||||
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
||||||
|
|
||||||
|
static final visibilityIcons = [
|
||||||
|
Icons.public,
|
||||||
|
Icons.group,
|
||||||
|
Icons.visibility,
|
||||||
|
Icons.visibility_off,
|
||||||
|
Icons.lock,
|
||||||
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
item = widget.item;
|
item = widget.item;
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
double _contentHeight = 0;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final List<String> attachments = item.body['attachments'] is List
|
final List<String> attachments = item.body['attachments'] is List
|
||||||
@ -90,32 +100,26 @@ class _PostItemState extends State<PostItem> {
|
|||||||
).paddingOnly(bottom: 8),
|
).paddingOnly(bottom: 8),
|
||||||
_PostHeaderWidget(
|
_PostHeaderWidget(
|
||||||
isCompact: widget.isCompact,
|
isCompact: widget.isCompact,
|
||||||
|
isFullDate: widget.isFullDate,
|
||||||
|
onTapMore: widget.onTapMore,
|
||||||
item: item,
|
item: item,
|
||||||
).paddingSymmetric(horizontal: 12),
|
).paddingSymmetric(horizontal: 12),
|
||||||
_PostHeaderDividerWidget(item: item).paddingSymmetric(horizontal: 12),
|
_PostHeaderDividerWidget(item: item).paddingSymmetric(horizontal: 12),
|
||||||
SizedContainer(
|
SizedContainer(
|
||||||
maxWidth: 640,
|
maxWidth: 640,
|
||||||
maxHeight: widget.isFullContent ? double.infinity : 80,
|
child: MarkdownTextContent(
|
||||||
child: _MeasureSize(
|
parentId: 'p${item.id}',
|
||||||
onChange: (size) {
|
content: item.body['content'],
|
||||||
setState(() => _contentHeight = size.height);
|
attachments: item.preload?.attachments,
|
||||||
},
|
isAutoWarp: item.type == 'story',
|
||||||
child: SingleChildScrollView(
|
isSelectable: widget.isContentSelectable,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
|
||||||
child: MarkdownTextContent(
|
|
||||||
parentId: 'p${item.id}',
|
|
||||||
content: item.body['content'],
|
|
||||||
isAutoWarp: item.type == 'story',
|
|
||||||
isSelectable: widget.isContentSelectable,
|
|
||||||
),
|
|
||||||
).paddingOnly(
|
|
||||||
left: 12,
|
|
||||||
right: 12,
|
|
||||||
bottom: hasAttachment ? 4 : 0,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
).paddingOnly(
|
||||||
|
left: 12,
|
||||||
|
right: 12,
|
||||||
|
bottom: hasAttachment ? 4 : 0,
|
||||||
),
|
),
|
||||||
if (_contentHeight >= 80 && !widget.isFullContent)
|
if (widget.item.body?['content_truncated'] == true)
|
||||||
Opacity(
|
Opacity(
|
||||||
opacity: 0.8,
|
opacity: 0.8,
|
||||||
child: InkWell(child: Text('readMore'.tr)),
|
child: InkWell(child: Text('readMore'.tr)),
|
||||||
@ -126,32 +130,20 @@ class _PostItemState extends State<PostItem> {
|
|||||||
LinkExpansion(content: item.body['content']).paddingOnly(
|
LinkExpansion(content: item.body['content']).paddingOnly(
|
||||||
left: 8,
|
left: 8,
|
||||||
right: 8,
|
right: 8,
|
||||||
top: 4,
|
|
||||||
),
|
),
|
||||||
_PostFooterWidget(item: item).paddingOnly(left: 12),
|
|
||||||
if (attachments.isNotEmpty)
|
if (attachments.isNotEmpty)
|
||||||
Row(
|
_PostAttachmentWidget(
|
||||||
children: [
|
item: item,
|
||||||
Icon(
|
padding: widget.padding,
|
||||||
Icons.file_copy,
|
isCompact: true,
|
||||||
size: 15,
|
isNonScrollAttachment: widget.isNonScrollAttachment,
|
||||||
color: _unFocusColor,
|
).paddingOnly(top: 4),
|
||||||
).paddingOnly(right: 5),
|
|
||||||
Text(
|
|
||||||
'attachmentHint'.trParams(
|
|
||||||
{'count': attachments.length.toString()},
|
|
||||||
),
|
|
||||||
style: TextStyle(color: _unFocusColor),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
).paddingOnly(left: 14, top: 4),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return OpenContainer(
|
return GestureDetector(
|
||||||
tappable: widget.isClickable,
|
child: Column(
|
||||||
closedBuilder: (_, openContainer) => Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_PostThumbnail(
|
_PostThumbnail(
|
||||||
@ -163,30 +155,22 @@ class _PostItemState extends State<PostItem> {
|
|||||||
children: [
|
children: [
|
||||||
_PostHeaderWidget(
|
_PostHeaderWidget(
|
||||||
isCompact: widget.isCompact,
|
isCompact: widget.isCompact,
|
||||||
|
isFullDate: widget.isFullDate,
|
||||||
|
onTapMore: widget.onTapMore,
|
||||||
item: item,
|
item: item,
|
||||||
),
|
),
|
||||||
_PostHeaderDividerWidget(item: item),
|
_PostHeaderDividerWidget(item: item),
|
||||||
SizedContainer(
|
SizedContainer(
|
||||||
maxWidth: 640,
|
maxWidth: 640,
|
||||||
maxHeight: widget.isFullContent ? double.infinity : 320,
|
child: MarkdownTextContent(
|
||||||
child: _MeasureSize(
|
parentId: 'p${item.id}-embed',
|
||||||
onChange: (size) {
|
content: item.body['content'],
|
||||||
setState(() => _contentHeight = size.height);
|
attachments: item.preload?.attachments,
|
||||||
},
|
isAutoWarp: item.type == 'story',
|
||||||
child: SingleChildScrollView(
|
isSelectable: widget.isContentSelectable,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
|
||||||
child: MarkdownTextContent(
|
|
||||||
parentId: 'p${item.id}-embed',
|
|
||||||
content: item.body['content'],
|
|
||||||
isAutoWarp: item.type == 'story',
|
|
||||||
isSelectable: widget.isContentSelectable,
|
|
||||||
isLargeText:
|
|
||||||
item.type == 'article' && widget.isFullContent,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (_contentHeight >= 320 && !widget.isFullContent)
|
if (widget.item.body?['content_truncated'] == true)
|
||||||
Opacity(
|
Opacity(
|
||||||
opacity: 0.8,
|
opacity: 0.8,
|
||||||
child: InkWell(child: Text('readMore'.tr)),
|
child: InkWell(child: Text('readMore'.tr)),
|
||||||
@ -220,18 +204,22 @@ class _PostItemState extends State<PostItem> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
_PostFooterWidget(item: item),
|
_PostFooterWidget(item: item),
|
||||||
LinkExpansion(content: item.body['content']).paddingOnly(top: 4),
|
LinkExpansion(content: item.body['content']),
|
||||||
],
|
],
|
||||||
).paddingOnly(
|
).paddingSymmetric(
|
||||||
right: 16,
|
horizontal: (widget.padding?.horizontal ?? 0) + 16,
|
||||||
left: 16,
|
),
|
||||||
|
if (hasAttachment) const Gap(8),
|
||||||
|
_PostAttachmentWidget(
|
||||||
|
item: item,
|
||||||
|
padding: widget.padding,
|
||||||
|
isCompact: item.type == 'article',
|
||||||
|
isNonScrollAttachment: widget.isNonScrollAttachment,
|
||||||
),
|
),
|
||||||
_PostAttachmentWidget(item: item),
|
|
||||||
if (widget.showFeaturedReply)
|
if (widget.showFeaturedReply)
|
||||||
_PostFeaturedReplyWidget(item: item).paddingSymmetric(
|
_PostFeaturedReplyWidget(item: item).paddingSymmetric(
|
||||||
horizontal: 12,
|
horizontal: (widget.padding?.horizontal ?? 0) + 12,
|
||||||
),
|
),
|
||||||
if (widget.showFeaturedReply) const Gap(8),
|
|
||||||
if (widget.isShowReply || widget.isReactable)
|
if (widget.isShowReply || widget.isReactable)
|
||||||
PostQuickAction(
|
PostQuickAction(
|
||||||
isShowReply: widget.isShowReply,
|
isShowReply: widget.isShowReply,
|
||||||
@ -249,22 +237,24 @@ class _PostItemState extends State<PostItem> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
).paddingOnly(
|
).paddingOnly(
|
||||||
left: 14,
|
top: 8,
|
||||||
right: 14,
|
left: (widget.padding?.left ?? 0) + 14,
|
||||||
|
right: (widget.padding?.right ?? 0) + 14,
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
).paddingOnly(
|
||||||
|
top: widget.padding?.top ?? 0,
|
||||||
|
bottom: widget.padding?.bottom ?? 0,
|
||||||
),
|
),
|
||||||
openBuilder: (_, __) => TitleShell(
|
onTap: () {
|
||||||
title: 'postDetail'.tr,
|
if (widget.isClickable) {
|
||||||
child: PostDetailScreen(
|
AppRouter.instance.pushNamed(
|
||||||
id: item.id.toString(),
|
'postDetail',
|
||||||
post: item,
|
pathParameters: {'id': item.id.toString()},
|
||||||
),
|
extra: item,
|
||||||
),
|
);
|
||||||
closedElevation: 0,
|
}
|
||||||
openElevation: 0,
|
},
|
||||||
closedColor: Colors.transparent,
|
|
||||||
openColor: Theme.of(context).colorScheme.surface,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -293,6 +283,7 @@ class _PostFeaturedReplyWidget extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
|
padding: EdgeInsets.only(top: 8),
|
||||||
constraints: const BoxConstraints(maxWidth: 480),
|
constraints: const BoxConstraints(maxWidth: 480),
|
||||||
child: Card(
|
child: Card(
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
@ -389,8 +380,16 @@ class _PostFeaturedReplyWidget extends StatelessWidget {
|
|||||||
|
|
||||||
class _PostAttachmentWidget extends StatelessWidget {
|
class _PostAttachmentWidget extends StatelessWidget {
|
||||||
final Post item;
|
final Post item;
|
||||||
|
final EdgeInsets? padding;
|
||||||
|
final bool isNonScrollAttachment;
|
||||||
|
final bool isCompact;
|
||||||
|
|
||||||
const _PostAttachmentWidget({required this.item});
|
const _PostAttachmentWidget({
|
||||||
|
required this.item,
|
||||||
|
required this.padding,
|
||||||
|
required this.isNonScrollAttachment,
|
||||||
|
this.isCompact = false,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -400,16 +399,40 @@ class _PostAttachmentWidget extends StatelessWidget {
|
|||||||
? List.from(item.body['attachments']?.whereType<String>())
|
? List.from(item.body['attachments']?.whereType<String>())
|
||||||
: List.empty();
|
: List.empty();
|
||||||
|
|
||||||
|
final unFocusColor =
|
||||||
|
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
||||||
|
|
||||||
if (attachments.isEmpty) return const SizedBox.shrink();
|
if (attachments.isEmpty) return const SizedBox.shrink();
|
||||||
|
|
||||||
if (attachments.length == 1) {
|
if (isCompact) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.file_copy,
|
||||||
|
size: 13,
|
||||||
|
color: unFocusColor,
|
||||||
|
).paddingOnly(right: 5),
|
||||||
|
Text(
|
||||||
|
'attachmentHint'.trParams(
|
||||||
|
{'count': attachments.length.toString()},
|
||||||
|
),
|
||||||
|
style: TextStyle(color: unFocusColor, fontSize: 13),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
).paddingOnly(
|
||||||
|
left: (padding?.left ?? 0) + 17,
|
||||||
|
right: (padding?.right ?? 0) + 17,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attachments.length == 1 && !isLargeScreen) {
|
||||||
return AttachmentList(
|
return AttachmentList(
|
||||||
parentId: item.id.toString(),
|
parentId: item.id.toString(),
|
||||||
attachmentIds: item.preload == null ? attachments : null,
|
attachmentIds: item.preload == null ? attachments : null,
|
||||||
attachments: item.preload?.attachments,
|
attachments: item.preload?.attachments,
|
||||||
autoload: false,
|
autoload: false,
|
||||||
isFullWidth: true,
|
isFullWidth: true,
|
||||||
).paddingOnly(top: 4);
|
);
|
||||||
} else if (attachments.length > 1 &&
|
} else if (attachments.length > 1 &&
|
||||||
attachments.length % 3 == 0 &&
|
attachments.length % 3 == 0 &&
|
||||||
!isLargeScreen) {
|
!isLargeScreen) {
|
||||||
@ -419,14 +442,31 @@ class _PostAttachmentWidget extends StatelessWidget {
|
|||||||
attachments: item.preload?.attachments,
|
attachments: item.preload?.attachments,
|
||||||
autoload: false,
|
autoload: false,
|
||||||
isGrid: true,
|
isGrid: true,
|
||||||
).paddingSymmetric(horizontal: 14, vertical: 8);
|
).paddingOnly(
|
||||||
} else {
|
left: (padding?.left ?? 0) + 14,
|
||||||
|
right: (padding?.right ?? 0) + 14,
|
||||||
|
);
|
||||||
|
} else if (attachments.length == 1 || isNonScrollAttachment) {
|
||||||
return AttachmentList(
|
return AttachmentList(
|
||||||
parentId: item.id.toString(),
|
parentId: item.id.toString(),
|
||||||
attachmentIds: item.preload == null ? attachments : null,
|
attachmentIds: item.preload == null ? attachments : null,
|
||||||
attachments: item.preload?.attachments,
|
attachments: item.preload?.attachments,
|
||||||
autoload: false,
|
autoload: false,
|
||||||
).paddingOnly(bottom: 8, top: 4);
|
isColumn: true,
|
||||||
|
).paddingOnly(
|
||||||
|
left: (padding?.left ?? 0) + 14,
|
||||||
|
right: (padding?.right ?? 0) + 14,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return AttachmentList(
|
||||||
|
parentId: item.id.toString(),
|
||||||
|
attachmentIds: item.preload == null ? attachments : null,
|
||||||
|
attachments: item.preload?.attachments,
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: (padding?.horizontal ?? 0) + 14,
|
||||||
|
),
|
||||||
|
autoload: false,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -512,7 +552,7 @@ class _PostHeaderDividerWidget extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (item.body['description'] != null || item.body['title'] != null) {
|
if (item.body['description'] != null || item.body['title'] != null) {
|
||||||
return const Gap(8);
|
return const SizedBox(height: 8);
|
||||||
}
|
}
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
@ -568,18 +608,23 @@ class _PostFooterWidget extends StatelessWidget {
|
|||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: widgets,
|
children: widgets,
|
||||||
).paddingOnly(top: 4);
|
).paddingSymmetric(vertical: 4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PostHeaderWidget extends StatelessWidget {
|
class _PostHeaderWidget extends StatelessWidget {
|
||||||
final bool isCompact;
|
final bool isCompact;
|
||||||
|
final bool isFullDate;
|
||||||
final Post item;
|
final Post item;
|
||||||
|
|
||||||
|
final Function? onTapMore;
|
||||||
|
|
||||||
const _PostHeaderWidget({
|
const _PostHeaderWidget({
|
||||||
required this.isCompact,
|
required this.isCompact,
|
||||||
|
required this.isFullDate,
|
||||||
required this.item,
|
required this.item,
|
||||||
|
required this.onTapMore,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -611,18 +656,43 @@ class _PostHeaderWidget extends StatelessWidget {
|
|||||||
if (isCompact)
|
if (isCompact)
|
||||||
RelativeDate(
|
RelativeDate(
|
||||||
item.publishedAt?.toLocal() ?? DateTime.now(),
|
item.publishedAt?.toLocal() ?? DateTime.now(),
|
||||||
|
isFull: isFullDate,
|
||||||
).paddingOnly(top: 1),
|
).paddingOnly(top: 1),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (!isCompact)
|
if (!isCompact)
|
||||||
RelativeDate(item.publishedAt?.toLocal() ?? DateTime.now()),
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
RelativeDate(
|
||||||
|
item.publishedAt?.toLocal() ?? DateTime.now(),
|
||||||
|
isFull: isFullDate,
|
||||||
|
),
|
||||||
|
const Gap(4),
|
||||||
|
Icon(
|
||||||
|
_PostItemState.visibilityIcons[item.visibility],
|
||||||
|
size: 16,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSurface
|
||||||
|
.withOpacity(0.75),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (item.type == 'article')
|
if (onTapMore != null)
|
||||||
Badge(
|
IconButton(
|
||||||
label: Text('article'.tr),
|
color: Theme.of(context).colorScheme.primary,
|
||||||
).paddingOnly(top: 3),
|
icon: const Icon(Icons.more_vert),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
|
visualDensity: const VisualDensity(
|
||||||
|
horizontal: -4,
|
||||||
|
vertical: -2,
|
||||||
|
),
|
||||||
|
onPressed: () => onTapMore!(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
@ -666,45 +736,3 @@ class _PostThumbnail extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef _OnWidgetSizeChange = void Function(Size size);
|
|
||||||
|
|
||||||
class _MeasureSizeRenderObject extends RenderProxyBox {
|
|
||||||
Size? oldSize;
|
|
||||||
_OnWidgetSizeChange onChange;
|
|
||||||
|
|
||||||
_MeasureSizeRenderObject(this.onChange);
|
|
||||||
|
|
||||||
@override
|
|
||||||
void performLayout() {
|
|
||||||
super.performLayout();
|
|
||||||
|
|
||||||
Size newSize = child!.size;
|
|
||||||
if (oldSize == newSize) return;
|
|
||||||
|
|
||||||
oldSize = newSize;
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
onChange(newSize);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _MeasureSize extends SingleChildRenderObjectWidget {
|
|
||||||
final _OnWidgetSizeChange onChange;
|
|
||||||
|
|
||||||
const _MeasureSize({
|
|
||||||
required this.onChange,
|
|
||||||
required Widget super.child,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
RenderObject createRenderObject(BuildContext context) {
|
|
||||||
return _MeasureSizeRenderObject(onChange);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void updateRenderObject(
|
|
||||||
BuildContext context, covariant _MeasureSizeRenderObject renderObject) {
|
|
||||||
renderObject.onChange = onChange;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.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';
|
||||||
@ -41,7 +42,6 @@ class PostListWidget extends StatelessWidget {
|
|||||||
isClickable: isClickable,
|
isClickable: isClickable,
|
||||||
showFeaturedReply: true,
|
showFeaturedReply: true,
|
||||||
item: item,
|
item: item,
|
||||||
backgroundColor: backgroundColor,
|
|
||||||
onUpdate: () {
|
onUpdate: () {
|
||||||
controller.refresh();
|
controller.refresh();
|
||||||
},
|
},
|
||||||
@ -60,8 +60,8 @@ class PostListEntryWidget extends StatelessWidget {
|
|||||||
final bool isClickable;
|
final bool isClickable;
|
||||||
final bool showFeaturedReply;
|
final bool showFeaturedReply;
|
||||||
final Post item;
|
final Post item;
|
||||||
|
final EdgeInsets? padding;
|
||||||
final Function onUpdate;
|
final Function onUpdate;
|
||||||
final Color? backgroundColor;
|
|
||||||
|
|
||||||
const PostListEntryWidget({
|
const PostListEntryWidget({
|
||||||
super.key,
|
super.key,
|
||||||
@ -70,54 +70,64 @@ class PostListEntryWidget extends StatelessWidget {
|
|||||||
required this.isClickable,
|
required this.isClickable,
|
||||||
required this.showFeaturedReply,
|
required this.showFeaturedReply,
|
||||||
required this.item,
|
required this.item,
|
||||||
|
this.padding,
|
||||||
required this.onUpdate,
|
required this.onUpdate,
|
||||||
this.backgroundColor,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
void _openActions(BuildContext context) {
|
||||||
|
final AuthProvider auth = Get.find();
|
||||||
|
if (auth.isAuthorized.isFalse) return;
|
||||||
|
|
||||||
|
showModalBottomSheet(
|
||||||
|
useRootNavigator: true,
|
||||||
|
context: context,
|
||||||
|
builder: (context) => PostAction(item: item),
|
||||||
|
).then((value) {
|
||||||
|
if (value is Future) {
|
||||||
|
value.then((_) {
|
||||||
|
onUpdate();
|
||||||
|
});
|
||||||
|
} else if (value != null) {
|
||||||
|
onUpdate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GestureDetector(
|
return TapRegion(
|
||||||
child: PostItem(
|
child: GestureDetector(
|
||||||
key: Key('p${item.id}'),
|
onLongPress: () => _openActions(context),
|
||||||
item: item,
|
child: PostItem(
|
||||||
isShowEmbed: isShowEmbed,
|
key: Key('p${item.id}'),
|
||||||
isClickable: isNestedClickable,
|
item: item,
|
||||||
showFeaturedReply: showFeaturedReply,
|
isShowEmbed: isShowEmbed,
|
||||||
backgroundColor: backgroundColor,
|
isClickable: isNestedClickable,
|
||||||
onComment: () {
|
showFeaturedReply: showFeaturedReply,
|
||||||
AppRouter.instance
|
padding: padding,
|
||||||
.pushNamed(
|
onTapMore: () => _openActions(context),
|
||||||
'postEditor',
|
onComment: () {
|
||||||
extra: PostPublishArguments(reply: item),
|
AppRouter.instance
|
||||||
)
|
.pushNamed(
|
||||||
.then((value) {
|
'postEditor',
|
||||||
if (value is Future) {
|
extra: PostPublishArguments(reply: item),
|
||||||
value.then((_) {
|
)
|
||||||
|
.then((value) {
|
||||||
|
if (value is Future) {
|
||||||
|
value.then((_) {
|
||||||
|
onUpdate();
|
||||||
|
});
|
||||||
|
} else if (value != null) {
|
||||||
onUpdate();
|
onUpdate();
|
||||||
});
|
}
|
||||||
} else if (value != null) {
|
|
||||||
onUpdate();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
).paddingSymmetric(vertical: 8),
|
|
||||||
onLongPress: () {
|
|
||||||
final AuthProvider auth = Get.find();
|
|
||||||
if (auth.isAuthorized.isFalse) return;
|
|
||||||
|
|
||||||
showModalBottomSheet(
|
|
||||||
useRootNavigator: true,
|
|
||||||
context: context,
|
|
||||||
builder: (context) => PostAction(item: item),
|
|
||||||
).then((value) {
|
|
||||||
if (value is Future) {
|
|
||||||
value.then((_) {
|
|
||||||
onUpdate();
|
|
||||||
});
|
});
|
||||||
} else if (value != null) {
|
},
|
||||||
onUpdate();
|
).paddingSymmetric(vertical: 8),
|
||||||
}
|
),
|
||||||
});
|
onTapInside: (event) {
|
||||||
|
if (event.buttons == kSecondaryMouseButton) {
|
||||||
|
_openActions(context);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -129,6 +139,7 @@ class ControlledPostListWidget extends StatelessWidget {
|
|||||||
final bool isNestedClickable;
|
final bool isNestedClickable;
|
||||||
final bool isPinned;
|
final bool isPinned;
|
||||||
final PagingController<int, Post> controller;
|
final PagingController<int, Post> controller;
|
||||||
|
final EdgeInsets? padding;
|
||||||
final Function? onUpdate;
|
final Function? onUpdate;
|
||||||
|
|
||||||
const ControlledPostListWidget({
|
const ControlledPostListWidget({
|
||||||
@ -138,6 +149,7 @@ class ControlledPostListWidget extends StatelessWidget {
|
|||||||
this.isClickable = true,
|
this.isClickable = true,
|
||||||
this.isNestedClickable = true,
|
this.isNestedClickable = true,
|
||||||
this.isPinned = true,
|
this.isPinned = true,
|
||||||
|
this.padding,
|
||||||
this.onUpdate,
|
this.onUpdate,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -156,6 +168,7 @@ class ControlledPostListWidget extends StatelessWidget {
|
|||||||
isNestedClickable: isNestedClickable,
|
isNestedClickable: isNestedClickable,
|
||||||
isClickable: isClickable,
|
isClickable: isClickable,
|
||||||
showFeaturedReply: true,
|
showFeaturedReply: true,
|
||||||
|
padding: padding,
|
||||||
item: item,
|
item: item,
|
||||||
onUpdate: onUpdate ?? () {},
|
onUpdate: onUpdate ?? () {},
|
||||||
);
|
);
|
||||||
|
@ -1,43 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:solian/models/post.dart';
|
|
||||||
import 'package:solian/widgets/posts/post_item.dart';
|
|
||||||
|
|
||||||
class PostOwnedListEntry extends StatelessWidget {
|
|
||||||
final Post item;
|
|
||||||
final Function onTap;
|
|
||||||
final bool isFullContent;
|
|
||||||
final Color? backgroundColor;
|
|
||||||
|
|
||||||
const PostOwnedListEntry({
|
|
||||||
super.key,
|
|
||||||
required this.item,
|
|
||||||
required this.onTap,
|
|
||||||
this.isFullContent = false,
|
|
||||||
this.backgroundColor,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Card(
|
|
||||||
child: GestureDetector(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
PostItem(
|
|
||||||
key: Key('p${item.id}'),
|
|
||||||
item: item,
|
|
||||||
isShowEmbed: false,
|
|
||||||
isClickable: false,
|
|
||||||
isShowReply: false,
|
|
||||||
isReactable: false,
|
|
||||||
isFullContent: isFullContent,
|
|
||||||
backgroundColor: backgroundColor,
|
|
||||||
).paddingSymmetric(vertical: 8),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () => onTap(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -8,11 +8,13 @@ import 'package:solian/widgets/posts/post_list.dart';
|
|||||||
|
|
||||||
class PostReplyList extends StatefulWidget {
|
class PostReplyList extends StatefulWidget {
|
||||||
final Post item;
|
final Post item;
|
||||||
|
final EdgeInsets? padding;
|
||||||
final Color? backgroundColor;
|
final Color? backgroundColor;
|
||||||
|
|
||||||
const PostReplyList({
|
const PostReplyList({
|
||||||
super.key,
|
super.key,
|
||||||
required this.item,
|
required this.item,
|
||||||
|
this.padding,
|
||||||
this.backgroundColor,
|
this.backgroundColor,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -53,7 +55,7 @@ class _PostReplyListState extends State<PostReplyList> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return PostListWidget(
|
return PostListWidget(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 10),
|
padding: widget.padding,
|
||||||
isShowEmbed: false,
|
isShowEmbed: false,
|
||||||
controller: _pagingController,
|
controller: _pagingController,
|
||||||
backgroundColor: widget.backgroundColor,
|
backgroundColor: widget.backgroundColor,
|
||||||
@ -93,6 +95,7 @@ class PostReplyListPopup extends StatelessWidget {
|
|||||||
slivers: [
|
slivers: [
|
||||||
PostReplyList(
|
PostReplyList(
|
||||||
item: item,
|
item: item,
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 10),
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
Theme.of(context).colorScheme.surfaceContainerLow,
|
Theme.of(context).colorScheme.surfaceContainerLow,
|
||||||
),
|
),
|
||||||
|
103
lib/widgets/posts/post_share.dart
Normal file
103
lib/widgets/posts/post_share.dart
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:qr_flutter/qr_flutter.dart';
|
||||||
|
import 'package:solian/models/post.dart';
|
||||||
|
import 'package:solian/widgets/posts/post_item.dart';
|
||||||
|
import 'package:solian/widgets/root_container.dart';
|
||||||
|
|
||||||
|
class PostShareImage extends StatelessWidget {
|
||||||
|
final Post item;
|
||||||
|
|
||||||
|
const PostShareImage({super.key, required this.item});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final textColor = Theme.of(context).colorScheme.onSurface.withOpacity(0.3);
|
||||||
|
return RootContainer(
|
||||||
|
child: Wrap(
|
||||||
|
alignment: WrapAlignment.spaceBetween,
|
||||||
|
runAlignment: WrapAlignment.center,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 40),
|
||||||
|
Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: Card(
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
|
child: PostItem(
|
||||||
|
item: item,
|
||||||
|
isShowEmbed: true,
|
||||||
|
isClickable: false,
|
||||||
|
showFeaturedReply: false,
|
||||||
|
isReactable: false,
|
||||||
|
isShowReply: false,
|
||||||
|
isNonScrollAttachment: true,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 4,
|
||||||
|
vertical: 16,
|
||||||
|
),
|
||||||
|
onComment: () {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).paddingOnly(bottom: 24),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/logo.png',
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(16),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'shareImageFooter'.tr,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: textColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'Solsynth LLC © ${DateTime.now().year}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: textColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
child: Material(
|
||||||
|
color: Theme.of(context).colorScheme.surface,
|
||||||
|
child: QrImageView(
|
||||||
|
data: 'https://solsynth.dev/posts/${item.id}',
|
||||||
|
version: QrVersions.auto,
|
||||||
|
padding: const EdgeInsets.all(4),
|
||||||
|
size: 48,
|
||||||
|
dataModuleStyle: QrDataModuleStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
eyeStyle: QrEyeStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).paddingSymmetric(horizontal: 36, vertical: 24),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -25,7 +25,6 @@ class PostSingleDisplay extends StatelessWidget {
|
|||||||
isNestedClickable: true,
|
isNestedClickable: true,
|
||||||
showFeaturedReply: true,
|
showFeaturedReply: true,
|
||||||
onUpdate: onUpdate,
|
onUpdate: onUpdate,
|
||||||
backgroundColor: Theme.of(context).colorScheme.surfaceContainerLow,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_animate/flutter_animate.dart';
|
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:solian/exts.dart';
|
import 'package:solian/exts.dart';
|
||||||
import 'package:solian/models/realm.dart';
|
import 'package:solian/models/realm.dart';
|
||||||
@ -8,6 +7,7 @@ import 'package:solian/services.dart';
|
|||||||
import 'package:solian/widgets/account/account_avatar.dart';
|
import 'package:solian/widgets/account/account_avatar.dart';
|
||||||
import 'package:solian/widgets/account/account_profile_popup.dart';
|
import 'package:solian/widgets/account/account_profile_popup.dart';
|
||||||
import 'package:solian/widgets/account/relative_select.dart';
|
import 'package:solian/widgets/account/relative_select.dart';
|
||||||
|
import 'package:solian/widgets/loading_indicator.dart';
|
||||||
|
|
||||||
class RealmMemberListPopup extends StatefulWidget {
|
class RealmMemberListPopup extends StatefulWidget {
|
||||||
final Realm realm;
|
final Realm realm;
|
||||||
@ -128,7 +128,7 @@ class _RealmMemberListPopupState extends State<RealmMemberListPopup> {
|
|||||||
'realmMembers'.tr,
|
'realmMembers'.tr,
|
||||||
style: Theme.of(context).textTheme.headlineSmall,
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
|
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
|
||||||
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
LoadingIndicator(isActive: _isBusy),
|
||||||
ListTile(
|
ListTile(
|
||||||
tileColor: Theme.of(context).colorScheme.surfaceContainerHigh,
|
tileColor: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
@ -4,20 +4,25 @@ import 'package:timeago/timeago.dart';
|
|||||||
|
|
||||||
class RelativeDate extends StatelessWidget {
|
class RelativeDate extends StatelessWidget {
|
||||||
final DateTime date;
|
final DateTime date;
|
||||||
|
final TextStyle? style;
|
||||||
final bool isFull;
|
final bool isFull;
|
||||||
|
|
||||||
const RelativeDate(this.date, {super.key, this.isFull = false});
|
const RelativeDate(this.date, {super.key, this.style, this.isFull = false});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (isFull) {
|
if (isFull) {
|
||||||
return Text(DateFormat('y/M/d HH:mm').format(date));
|
return Text(
|
||||||
|
DateFormat('y/M/d HH:mm').format(date),
|
||||||
|
style: style,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return Text(
|
return Text(
|
||||||
format(
|
format(
|
||||||
date,
|
date,
|
||||||
locale: 'en_short',
|
locale: 'en_short',
|
||||||
),
|
),
|
||||||
|
style: style,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,9 +7,11 @@
|
|||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
#include <desktop_drop/desktop_drop_plugin.h>
|
#include <desktop_drop/desktop_drop_plugin.h>
|
||||||
|
#include <file_saver/file_saver_plugin.h>
|
||||||
#include <file_selector_linux/file_selector_plugin.h>
|
#include <file_selector_linux/file_selector_plugin.h>
|
||||||
#include <flutter_acrylic/flutter_acrylic_plugin.h>
|
#include <flutter_acrylic/flutter_acrylic_plugin.h>
|
||||||
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
|
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
|
||||||
|
#include <flutter_udid/flutter_udid_plugin.h>
|
||||||
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
|
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
|
||||||
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
|
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
|
||||||
#include <media_kit_video/media_kit_video_plugin.h>
|
#include <media_kit_video/media_kit_video_plugin.h>
|
||||||
@ -21,6 +23,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
|||||||
g_autoptr(FlPluginRegistrar) desktop_drop_registrar =
|
g_autoptr(FlPluginRegistrar) desktop_drop_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopDropPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopDropPlugin");
|
||||||
desktop_drop_plugin_register_with_registrar(desktop_drop_registrar);
|
desktop_drop_plugin_register_with_registrar(desktop_drop_registrar);
|
||||||
|
g_autoptr(FlPluginRegistrar) file_saver_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSaverPlugin");
|
||||||
|
file_saver_plugin_register_with_registrar(file_saver_registrar);
|
||||||
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
|
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
|
||||||
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
|
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
|
||||||
@ -30,6 +35,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
|||||||
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
|
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
|
||||||
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
|
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
|
||||||
|
g_autoptr(FlPluginRegistrar) flutter_udid_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterUdidPlugin");
|
||||||
|
flutter_udid_plugin_register_with_registrar(flutter_udid_registrar);
|
||||||
g_autoptr(FlPluginRegistrar) flutter_webrtc_registrar =
|
g_autoptr(FlPluginRegistrar) flutter_webrtc_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterWebRTCPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterWebRTCPlugin");
|
||||||
flutter_web_r_t_c_plugin_register_with_registrar(flutter_webrtc_registrar);
|
flutter_web_r_t_c_plugin_register_with_registrar(flutter_webrtc_registrar);
|
||||||
|
@ -4,9 +4,11 @@
|
|||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
desktop_drop
|
desktop_drop
|
||||||
|
file_saver
|
||||||
file_selector_linux
|
file_selector_linux
|
||||||
flutter_acrylic
|
flutter_acrylic
|
||||||
flutter_secure_storage_linux
|
flutter_secure_storage_linux
|
||||||
|
flutter_udid
|
||||||
flutter_webrtc
|
flutter_webrtc
|
||||||
media_kit_libs_linux
|
media_kit_libs_linux
|
||||||
media_kit_video
|
media_kit_video
|
||||||
|
@ -8,6 +8,7 @@ import Foundation
|
|||||||
import connectivity_plus
|
import connectivity_plus
|
||||||
import desktop_drop
|
import desktop_drop
|
||||||
import device_info_plus
|
import device_info_plus
|
||||||
|
import file_saver
|
||||||
import file_selector_macos
|
import file_selector_macos
|
||||||
import firebase_analytics
|
import firebase_analytics
|
||||||
import firebase_core
|
import firebase_core
|
||||||
@ -15,6 +16,7 @@ import firebase_crashlytics
|
|||||||
import firebase_messaging
|
import firebase_messaging
|
||||||
import flutter_local_notifications
|
import flutter_local_notifications
|
||||||
import flutter_secure_storage_macos
|
import flutter_secure_storage_macos
|
||||||
|
import flutter_udid
|
||||||
import flutter_webrtc
|
import flutter_webrtc
|
||||||
import gal
|
import gal
|
||||||
import in_app_review
|
import in_app_review
|
||||||
@ -38,6 +40,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
|
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
|
||||||
DesktopDropPlugin.register(with: registry.registrar(forPlugin: "DesktopDropPlugin"))
|
DesktopDropPlugin.register(with: registry.registrar(forPlugin: "DesktopDropPlugin"))
|
||||||
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
||||||
|
FileSaverPlugin.register(with: registry.registrar(forPlugin: "FileSaverPlugin"))
|
||||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||||
FLTFirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAnalyticsPlugin"))
|
FLTFirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAnalyticsPlugin"))
|
||||||
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
|
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
|
||||||
@ -45,6 +48,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
|
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
|
||||||
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
||||||
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
||||||
|
FlutterUdidPlugin.register(with: registry.registrar(forPlugin: "FlutterUdidPlugin"))
|
||||||
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"))
|
InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin"))
|
||||||
|
@ -6,6 +6,8 @@ PODS:
|
|||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- device_info_plus (0.0.1):
|
- device_info_plus (0.0.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
|
- file_saver (0.0.1):
|
||||||
|
- FlutterMacOS
|
||||||
- file_selector_macos (0.0.1):
|
- file_selector_macos (0.0.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- Firebase/Analytics (11.2.0):
|
- Firebase/Analytics (11.2.0):
|
||||||
@ -101,6 +103,9 @@ PODS:
|
|||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- flutter_secure_storage_macos (6.1.1):
|
- flutter_secure_storage_macos (6.1.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
|
- flutter_udid (0.0.1):
|
||||||
|
- FlutterMacOS
|
||||||
|
- SAMKeychain
|
||||||
- flutter_webrtc (0.11.3):
|
- flutter_webrtc (0.11.3):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- WebRTC-SDK (= 125.6422.04)
|
- WebRTC-SDK (= 125.6422.04)
|
||||||
@ -188,6 +193,7 @@ PODS:
|
|||||||
- PromisesObjC (= 2.4.0)
|
- PromisesObjC (= 2.4.0)
|
||||||
- protocol_handler_macos (0.0.1):
|
- protocol_handler_macos (0.0.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
|
- SAMKeychain (1.5.3)
|
||||||
- screen_brightness_macos (0.1.0):
|
- screen_brightness_macos (0.1.0):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- share_plus (0.0.1):
|
- share_plus (0.0.1):
|
||||||
@ -226,6 +232,7 @@ DEPENDENCIES:
|
|||||||
- connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/darwin`)
|
- connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/darwin`)
|
||||||
- desktop_drop (from `Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos`)
|
- desktop_drop (from `Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos`)
|
||||||
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
|
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
|
||||||
|
- file_saver (from `Flutter/ephemeral/.symlinks/plugins/file_saver/macos`)
|
||||||
- file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`)
|
- file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`)
|
||||||
- firebase_analytics (from `Flutter/ephemeral/.symlinks/plugins/firebase_analytics/macos`)
|
- firebase_analytics (from `Flutter/ephemeral/.symlinks/plugins/firebase_analytics/macos`)
|
||||||
- firebase_core (from `Flutter/ephemeral/.symlinks/plugins/firebase_core/macos`)
|
- firebase_core (from `Flutter/ephemeral/.symlinks/plugins/firebase_core/macos`)
|
||||||
@ -233,6 +240,7 @@ DEPENDENCIES:
|
|||||||
- firebase_messaging (from `Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos`)
|
- firebase_messaging (from `Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos`)
|
||||||
- flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`)
|
- flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`)
|
||||||
- flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`)
|
- flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`)
|
||||||
|
- flutter_udid (from `Flutter/ephemeral/.symlinks/plugins/flutter_udid/macos`)
|
||||||
- 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`)
|
||||||
@ -272,6 +280,7 @@ SPEC REPOS:
|
|||||||
- nanopb
|
- nanopb
|
||||||
- PromisesObjC
|
- PromisesObjC
|
||||||
- PromisesSwift
|
- PromisesSwift
|
||||||
|
- SAMKeychain
|
||||||
- sqlite3
|
- sqlite3
|
||||||
- WebRTC-SDK
|
- WebRTC-SDK
|
||||||
|
|
||||||
@ -282,6 +291,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos
|
||||||
device_info_plus:
|
device_info_plus:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos
|
||||||
|
file_saver:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/file_saver/macos
|
||||||
file_selector_macos:
|
file_selector_macos:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos
|
||||||
firebase_analytics:
|
firebase_analytics:
|
||||||
@ -296,6 +307,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos
|
||||||
flutter_secure_storage_macos:
|
flutter_secure_storage_macos:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos
|
||||||
|
flutter_udid:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/flutter_udid/macos
|
||||||
flutter_webrtc:
|
flutter_webrtc:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos
|
||||||
FlutterMacOS:
|
FlutterMacOS:
|
||||||
@ -338,9 +351,10 @@ EXTERNAL SOURCES:
|
|||||||
:path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db
|
connectivity_plus: 4c41c08fc6d7c91f63bc7aec70ffe3730b04f563
|
||||||
desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898
|
desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898
|
||||||
device_info_plus: f1aae8670672f75c4c8850ecbe0b2ddef62b0a22
|
device_info_plus: 74e614483d05c89290d30a4c8feae15d555f7427
|
||||||
|
file_saver: 44e6fbf666677faf097302460e214e977fdd977b
|
||||||
file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d
|
file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d
|
||||||
Firebase: 98e6bf5278170668a7983e12971a66b2cd57fc8c
|
Firebase: 98e6bf5278170668a7983e12971a66b2cd57fc8c
|
||||||
firebase_analytics: 30ff72f6d4847ff0b479d8edd92fc8582e719072
|
firebase_analytics: 30ff72f6d4847ff0b479d8edd92fc8582e719072
|
||||||
@ -358,6 +372,7 @@ SPEC CHECKSUMS:
|
|||||||
FirebaseSessions: 655ff17f3cc1a635cbdc2d69b953878001f9e25b
|
FirebaseSessions: 655ff17f3cc1a635cbdc2d69b953878001f9e25b
|
||||||
flutter_local_notifications: 3805ca215b2fb7f397d78b66db91f6a747af52e4
|
flutter_local_notifications: 3805ca215b2fb7f397d78b66db91f6a747af52e4
|
||||||
flutter_secure_storage_macos: 59459653abe1adb92abbc8ea747d79f8d19866c9
|
flutter_secure_storage_macos: 59459653abe1adb92abbc8ea747d79f8d19866c9
|
||||||
|
flutter_udid: 6b2b89780c3dfeecf0047bdf93f622d6416b1c07
|
||||||
flutter_webrtc: 2b4e4a2de70a1485836e40fd71a7a94c77d49bd9
|
flutter_webrtc: 2b4e4a2de70a1485836e40fd71a7a94c77d49bd9
|
||||||
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
||||||
gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1
|
gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1
|
||||||
@ -371,14 +386,15 @@ SPEC CHECKSUMS:
|
|||||||
media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5
|
media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5
|
||||||
media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5
|
media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5
|
||||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||||
package_info_plus: d2f71247aab4b6521434f887276093acc70d214c
|
package_info_plus: f5790acc797bf17c3e959e9d6cf162cc68ff7523
|
||||||
pasteboard: 9b69dba6fedbb04866be632205d532fe2f6b1d99
|
pasteboard: 9b69dba6fedbb04866be632205d532fe2f6b1d99
|
||||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||||
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
|
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
|
||||||
protocol_handler_macos: d10a6c01d6373389ffd2278013ab4c47ed6d6daa
|
protocol_handler_macos: d10a6c01d6373389ffd2278013ab4c47ed6d6daa
|
||||||
|
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||||
screen_brightness_macos: 2d6d3af2165592d9a55ffcd95b7550970e41ebda
|
screen_brightness_macos: 2d6d3af2165592d9a55ffcd95b7550970e41ebda
|
||||||
share_plus: a182a58e04e51647c0481aadabbc4de44b3a2bce
|
share_plus: fd717ef89a2801d3491e737630112b80c310640e
|
||||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||||
sqflite_darwin: a553b1fd6fe66f53bbb0fe5b4f5bab93f08d7a13
|
sqflite_darwin: a553b1fd6fe66f53bbb0fe5b4f5bab93f08d7a13
|
||||||
sqlite3: 0bb0e6389d824e40296f531b858a2a0b71c0d2fb
|
sqlite3: 0bb0e6389d824e40296f531b858a2a0b71c0d2fb
|
||||||
|
@ -14,6 +14,8 @@
|
|||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.device.camera</key>
|
<key>com.apple.security.device.camera</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>com.apple.security.files.downloads.read-write</key>
|
||||||
|
<true/>
|
||||||
<key>com.apple.security.files.user-selected.read-only</key>
|
<key>com.apple.security.files.user-selected.read-only</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.network.client</key>
|
<key>com.apple.security.network.client</key>
|
||||||
|
@ -62,5 +62,9 @@
|
|||||||
<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>UIFileSharingEnabled</key>
|
||||||
|
<true/>
|
||||||
|
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||||
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
@ -12,6 +12,8 @@
|
|||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.device.camera</key>
|
<key>com.apple.security.device.camera</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>com.apple.security.files.downloads.read-write</key>
|
||||||
|
<true/>
|
||||||
<key>com.apple.security.files.user-selected.read-only</key>
|
<key>com.apple.security.files.user-selected.read-only</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.network.client</key>
|
<key>com.apple.security.network.client</key>
|
||||||
|
184
pubspec.lock
184
pubspec.lock
@ -74,10 +74,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: args
|
name: args
|
||||||
sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a"
|
sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.5.0"
|
version: "2.6.0"
|
||||||
async:
|
async:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -198,14 +198,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.1"
|
version: "1.3.1"
|
||||||
carousel_slider:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: carousel_slider
|
|
||||||
sha256: "7b006ec356205054af5beaef62e2221160ea36b90fb70a35e4deacd49d0349ae"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "5.0.0"
|
|
||||||
characters:
|
characters:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -262,14 +254,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.18.0"
|
version: "1.18.0"
|
||||||
|
confetti:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: confetti
|
||||||
|
sha256: "79376a99648efbc3f23582f5784ced0fe239922bd1a0fb41f582051eba750751"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.8.0"
|
||||||
connectivity_plus:
|
connectivity_plus:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: connectivity_plus
|
name: connectivity_plus
|
||||||
sha256: "2056db5241f96cdc0126bd94459fc4cdc13876753768fc7a31c425e50a7177d0"
|
sha256: "876849631b0c7dc20f8b471a2a03142841b482438e3b707955464f5ffca3e4c3"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.5"
|
version: "6.1.0"
|
||||||
connectivity_plus_platform_interface:
|
connectivity_plus_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -282,10 +282,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: convert
|
name: convert
|
||||||
sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592"
|
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.1"
|
version: "3.1.2"
|
||||||
cross_file:
|
cross_file:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -298,10 +298,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: crypto
|
name: crypto
|
||||||
sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27
|
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.5"
|
version: "3.0.6"
|
||||||
csslib:
|
csslib:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -346,10 +346,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: device_info_plus
|
name: device_info_plus
|
||||||
sha256: db03b2d2a3fa466a4627709e1db58692c3f7f658e36a5942d342d86efedc4091
|
sha256: c4af09051b4f0508f6c1dc0a5c085bf014d5c9a4a0678ce1799c2b4d716387a0
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "11.0.0"
|
version: "11.1.0"
|
||||||
device_info_plus_platform_interface:
|
device_info_plus_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -386,26 +386,26 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: drift
|
name: drift
|
||||||
sha256: d6ff1ec6a0f3fa097dda6b776cf601f1f3d88b53b287288e09c1306f394fb1b3
|
sha256: df027d168a2985a2e9da900adeba2ab0136f0d84436592cf3cd5135f82c8579c
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.20.3"
|
version: "2.21.0"
|
||||||
drift_dev:
|
drift_dev:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: drift_dev
|
name: drift_dev
|
||||||
sha256: "3ee987578ca2281b5ff91eadd757cd6dd36001458d6e33784f990d67ff38f756"
|
sha256: "27bab15e7869b69259663590381180117873b9b273a1ea9ebb21bb73133d1233"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.20.3"
|
version: "2.21.0"
|
||||||
drift_flutter:
|
drift_flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: drift_flutter
|
name: drift_flutter
|
||||||
sha256: c670c947fe17ad149678a43fdbbfdb69321f0c83d315043e34e8ad2729e11f49
|
sha256: fec503e9d408f36bb345f9f6d24bc9d62b7b5f970db49760253d9e8d3acd48d5
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.0"
|
version: "0.2.1"
|
||||||
dropdown_button2:
|
dropdown_button2:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -422,6 +422,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.5"
|
version: "2.0.5"
|
||||||
|
fading_edge_scrollview:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: fading_edge_scrollview
|
||||||
|
sha256: "1f84fe3ea8e251d00d5735e27502a6a250e4aa3d3b330d3fdcb475af741464ef"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.1.1"
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -462,6 +470,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.1.2"
|
version: "8.1.2"
|
||||||
|
file_saver:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: file_saver
|
||||||
|
sha256: "017a127de686af2d2fbbd64afea97052d95f2a0f87d19d25b87e097407bf9c1e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.14"
|
||||||
file_selector_linux:
|
file_selector_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -610,10 +626,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: fixnum
|
name: fixnum
|
||||||
sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1"
|
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0"
|
version: "1.1.1"
|
||||||
fl_chart:
|
fl_chart:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -791,10 +807,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_markdown
|
name: flutter_markdown
|
||||||
sha256: e17575ca576a34b46c58c91f9948891117a1bd97815d2e661813c7f90c647a78
|
sha256: bd9c475d9aae256369edacafc29d1e74c81f78a10cdcdacbbbc9e3c43d009e4a
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.3+2"
|
version: "0.7.4"
|
||||||
flutter_native_splash:
|
flutter_native_splash:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@ -811,6 +827,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.23"
|
version: "2.0.23"
|
||||||
|
flutter_resizable_container:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_resizable_container
|
||||||
|
sha256: "5b15c79c6cc338ed79640c706bb5176baa3333d92fd3627ad279aa3e25d2f0e7"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.0"
|
||||||
flutter_secure_storage:
|
flutter_secure_storage:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -896,6 +920,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.2.0"
|
version: "5.2.0"
|
||||||
|
flutter_udid:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_udid
|
||||||
|
sha256: "63384bd96203aaefccfd7137fab642edda18afede12b0e9e1a2c96fe2589fd07"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.0"
|
||||||
flutter_web_plugins:
|
flutter_web_plugins:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -1025,10 +1057,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: image
|
name: image
|
||||||
sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8"
|
sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.2.0"
|
version: "4.3.0"
|
||||||
image_cropper:
|
image_cropper:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -1065,26 +1097,26 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: image_picker_android
|
name: image_picker_android
|
||||||
sha256: d3e5e00fdfeca8fd4ffb3227001264d449cc8950414c2ff70b0e06b9c628e643
|
sha256: d34e0d9e024e81321b2aeed7b202ec6181cc282e6a1c0c0b4e6ad07ef1065d82
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.8.12+15"
|
version: "0.8.12+16"
|
||||||
image_picker_for_web:
|
image_picker_for_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: image_picker_for_web
|
name: image_picker_for_web
|
||||||
sha256: "65d94623e15372c5c51bebbcb820848d7bcb323836e12dfdba60b5d3a8b39e50"
|
sha256: "717eb042ab08c40767684327be06a5d8dbb341fe791d514e4b92c7bbe1b7bb83"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.5"
|
version: "3.0.6"
|
||||||
image_picker_ios:
|
image_picker_ios:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: image_picker_ios
|
name: image_picker_ios
|
||||||
sha256: "6703696ad49f5c3c8356d576d7ace84d1faf459afb07accbb0fae780753ff447"
|
sha256: "4f0568120c6fcc0aaa04511cb9f9f4d29fc3d0139884b1d06be88dcec7641d6b"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.8.12"
|
version: "0.8.12+1"
|
||||||
image_picker_linux:
|
image_picker_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1225,10 +1257,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: logging
|
name: logging
|
||||||
sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
|
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.0"
|
version: "1.3.0"
|
||||||
macos_window_utils:
|
macos_window_utils:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1261,6 +1293,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.0"
|
version: "0.5.0"
|
||||||
|
marquee:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: marquee
|
||||||
|
sha256: a87e7e80c5d21434f90ad92add9f820cf68be374b226404fe881d2bba7be0862
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.0"
|
||||||
matcher:
|
matcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1361,10 +1401,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: mime
|
name: mime
|
||||||
sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a"
|
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.6"
|
version: "2.0.0"
|
||||||
nested:
|
nested:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1401,10 +1441,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: package_info_plus
|
name: package_info_plus
|
||||||
sha256: "894f37107424311bdae3e476552229476777b8752c5a2a2369c0cb9a2d5442ef"
|
sha256: df3eb3e0aed5c1107bb0fdb80a8e82e778114958b1c5ac5644fb1ac9cae8a998
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.0.3"
|
version: "8.1.0"
|
||||||
package_info_plus_platform_interface:
|
package_info_plus_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1497,10 +1537,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: permission_handler_android
|
name: permission_handler_android
|
||||||
sha256: "76e4ab092c1b240d31177bb64d2b0bea43f43d0e23541ec866151b9f7b2490fa"
|
sha256: "71bbecfee799e65aff7c744761a57e817e73b738fedf62ab7afd5593da21f9f1"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "12.0.12"
|
version: "12.0.13"
|
||||||
permission_handler_apple:
|
permission_handler_apple:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1545,10 +1585,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: platform
|
name: platform
|
||||||
sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65"
|
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.5"
|
version: "3.1.6"
|
||||||
platform_detect:
|
platform_detect:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1685,6 +1725,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.0"
|
version: "1.3.0"
|
||||||
|
qr:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: qr
|
||||||
|
sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.2"
|
||||||
|
qr_flutter:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: qr_flutter
|
||||||
|
sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.1.0"
|
||||||
recase:
|
recase:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1757,6 +1813,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.3"
|
version: "0.1.3"
|
||||||
|
screenshot:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: screenshot
|
||||||
|
sha256: "63817697a7835e6ce82add4228e15d233b74d42975c143ad8cfe07009fab866b"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.0"
|
||||||
sdp_transform:
|
sdp_transform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1769,10 +1833,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: share_plus
|
name: share_plus
|
||||||
sha256: fec12c3c39f01e4df1ec6ad92b6e85503c5ca64ffd6e28d18c9ffe53fcc4cb11
|
sha256: "334fcdf0ef9c0df0e3b428faebcac9568f35c747d59831474b2fc56e156d244e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.0.3"
|
version: "10.1.0"
|
||||||
share_plus_platform_interface:
|
share_plus_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1958,10 +2022,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: sqlparser
|
name: sqlparser
|
||||||
sha256: "852cf80f9e974ac8e1b613758a8aa640215f7701352b66a7f468e95711eb570b"
|
sha256: c5f63dff8677407ddcddfa4744c176ea6dc44286c47ba9e69e76d8071398034d
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.38.1"
|
version: "0.39.1"
|
||||||
stack_trace:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -2034,6 +2098,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.7.0"
|
version: "3.7.0"
|
||||||
|
timeline_tile:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: timeline_tile
|
||||||
|
sha256: "85ec2023c67137397c2812e3e848b2fb20b410b67cd9aff304bb5480c376fc0c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.0"
|
||||||
timezone:
|
timezone:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -2054,10 +2126,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: typed_data
|
name: typed_data
|
||||||
sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
|
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.2"
|
version: "1.4.0"
|
||||||
universal_io:
|
universal_io:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -2142,10 +2214,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_windows
|
name: url_launcher_windows
|
||||||
sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185"
|
sha256: "44cf3aabcedde30f2dba119a9dea3b0f2672fbe6fa96e85536251d678216b3c4"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.2"
|
version: "3.1.3"
|
||||||
uuid:
|
uuid:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -2278,10 +2350,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: win32
|
name: win32
|
||||||
sha256: "4d45dc9069dba4619dc0ebd93c7cec5e66d8482cb625a370ac806dcc8165f2ec"
|
sha256: "2294c64768987ea280b43a3d8357d42d5679f3e2b5b69b602be45b2abbd165b0"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.5.5"
|
version: "5.6.1"
|
||||||
win32_registry:
|
win32_registry:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
11
pubspec.yaml
11
pubspec.yaml
@ -2,7 +2,7 @@ name: solian
|
|||||||
description: "The Solar Network App"
|
description: "The Solar Network App"
|
||||||
publish_to: "none"
|
publish_to: "none"
|
||||||
|
|
||||||
version: 1.3.7+8
|
version: 1.4.0+17
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.3.4 <4.0.0"
|
sdk: ">=3.3.4 <4.0.0"
|
||||||
@ -18,7 +18,6 @@ dependencies:
|
|||||||
flutter_markdown: ^0.7.1
|
flutter_markdown: ^0.7.1
|
||||||
flutter_animate: ^4.5.0
|
flutter_animate: ^4.5.0
|
||||||
flutter_secure_storage: ^9.2.1
|
flutter_secure_storage: ^9.2.1
|
||||||
carousel_slider: ^5.0.0
|
|
||||||
url_launcher: ^6.2.6
|
url_launcher: ^6.2.6
|
||||||
infinite_scroll_pagination: ^4.0.0
|
infinite_scroll_pagination: ^4.0.0
|
||||||
image_picker: ^1.1.1
|
image_picker: ^1.1.1
|
||||||
@ -84,6 +83,14 @@ dependencies:
|
|||||||
action_slider: ^0.7.0
|
action_slider: ^0.7.0
|
||||||
in_app_review: ^2.0.9
|
in_app_review: ^2.0.9
|
||||||
syntax_highlight: ^0.4.0
|
syntax_highlight: ^0.4.0
|
||||||
|
flutter_udid: ^3.0.0
|
||||||
|
timeline_tile: ^2.0.0
|
||||||
|
screenshot: ^3.0.0
|
||||||
|
qr_flutter: ^4.1.0
|
||||||
|
flutter_resizable_container: ^3.0.0
|
||||||
|
file_saver: ^0.2.14
|
||||||
|
marquee: ^2.3.0
|
||||||
|
confetti: ^0.8.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
@ -111,15 +111,14 @@
|
|||||||
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
|
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body oncontextmenu="return false;">
|
||||||
<picture id="splash">
|
<picture id="splash">
|
||||||
<source srcset="splash/img/light-1x.png 1x, splash/img/light-2x.png 2x, splash/img/light-3x.png 3x, splash/img/light-4x.png 4x" media="(prefers-color-scheme: light)">
|
<source srcset="splash/img/light-1x.png 1x, splash/img/light-2x.png 2x, splash/img/light-3x.png 3x, splash/img/light-4x.png 4x" media="(prefers-color-scheme: light)">
|
||||||
<source srcset="splash/img/dark-1x.png 1x, splash/img/dark-2x.png 2x, splash/img/dark-3x.png 3x, splash/img/dark-4x.png 4x" media="(prefers-color-scheme: dark)">
|
<source srcset="splash/img/dark-1x.png 1x, splash/img/dark-2x.png 2x, splash/img/dark-3x.png 3x, splash/img/dark-4x.png 4x" media="(prefers-color-scheme: dark)">
|
||||||
<img class="center" aria-hidden="true" src="splash/img/light-1x.png" alt="">
|
<img class="center" aria-hidden="true" src="splash/img/light-1x.png" alt="">
|
||||||
</picture>
|
</picture>
|
||||||
|
|
||||||
|
<script src="flutter_bootstrap.js" async=""></script>
|
||||||
<script src="flutter_bootstrap.js" async=""></script>
|
|
||||||
|
|
||||||
|
|
||||||
</body></html>
|
</body></html>
|
@ -8,10 +8,12 @@
|
|||||||
|
|
||||||
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
|
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
|
||||||
#include <desktop_drop/desktop_drop_plugin.h>
|
#include <desktop_drop/desktop_drop_plugin.h>
|
||||||
|
#include <file_saver/file_saver_plugin.h>
|
||||||
#include <file_selector_windows/file_selector_windows.h>
|
#include <file_selector_windows/file_selector_windows.h>
|
||||||
#include <firebase_core/firebase_core_plugin_c_api.h>
|
#include <firebase_core/firebase_core_plugin_c_api.h>
|
||||||
#include <flutter_acrylic/flutter_acrylic_plugin.h>
|
#include <flutter_acrylic/flutter_acrylic_plugin.h>
|
||||||
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
||||||
|
#include <flutter_udid/flutter_udid_plugin_c_api.h>
|
||||||
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
|
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
|
||||||
#include <gal/gal_plugin_c_api.h>
|
#include <gal/gal_plugin_c_api.h>
|
||||||
#include <livekit_client/live_kit_plugin.h>
|
#include <livekit_client/live_kit_plugin.h>
|
||||||
@ -30,6 +32,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
|||||||
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
|
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
|
||||||
DesktopDropPluginRegisterWithRegistrar(
|
DesktopDropPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("DesktopDropPlugin"));
|
registry->GetRegistrarForPlugin("DesktopDropPlugin"));
|
||||||
|
FileSaverPluginRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("FileSaverPlugin"));
|
||||||
FileSelectorWindowsRegisterWithRegistrar(
|
FileSelectorWindowsRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||||
FirebaseCorePluginCApiRegisterWithRegistrar(
|
FirebaseCorePluginCApiRegisterWithRegistrar(
|
||||||
@ -38,6 +42,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
|||||||
registry->GetRegistrarForPlugin("FlutterAcrylicPlugin"));
|
registry->GetRegistrarForPlugin("FlutterAcrylicPlugin"));
|
||||||
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
||||||
|
FlutterUdidPluginCApiRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("FlutterUdidPluginCApi"));
|
||||||
FlutterWebRTCPluginRegisterWithRegistrar(
|
FlutterWebRTCPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("FlutterWebRTCPlugin"));
|
registry->GetRegistrarForPlugin("FlutterWebRTCPlugin"));
|
||||||
GalPluginCApiRegisterWithRegistrar(
|
GalPluginCApiRegisterWithRegistrar(
|
||||||
|
@ -5,10 +5,12 @@
|
|||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
connectivity_plus
|
connectivity_plus
|
||||||
desktop_drop
|
desktop_drop
|
||||||
|
file_saver
|
||||||
file_selector_windows
|
file_selector_windows
|
||||||
firebase_core
|
firebase_core
|
||||||
flutter_acrylic
|
flutter_acrylic
|
||||||
flutter_secure_storage_windows
|
flutter_secure_storage_windows
|
||||||
|
flutter_udid
|
||||||
flutter_webrtc
|
flutter_webrtc
|
||||||
gal
|
gal
|
||||||
livekit_client
|
livekit_client
|
||||||
|
Loading…
x
Reference in New Issue
Block a user