Compare commits
42 Commits
Author | SHA1 | Date | |
---|---|---|---|
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 | |||
a04bfe4cf9 | |||
7b7988e6cb | |||
81a616157e | |||
52312662fb | |||
ca18d6ade4 | |||
af7cc8dab0 | |||
382e3c4a4c | |||
1e37c6ddae | |||
442ef06147 | |||
606a0d708a |
13
.roadsignrc
Normal file
13
.roadsignrc
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"sync": {
|
||||
"region": "solian",
|
||||
"configPath": "roadsign.toml"
|
||||
},
|
||||
"deployments": [
|
||||
{
|
||||
"region": "solian",
|
||||
"site": "solian-web",
|
||||
"path": "build/web"
|
||||
}
|
||||
]
|
||||
}
|
358
assets/highlighting/cpp.json
Normal file
358
assets/highlighting/cpp.json
Normal file
@ -0,0 +1,358 @@
|
||||
{
|
||||
"name": "C++",
|
||||
"version": "1.0.0",
|
||||
"fileTypes": ["cpp", "hpp", "cc", "h"],
|
||||
"scopeName": "source.cpp",
|
||||
|
||||
"foldingStartMarker": "\\{\\s*$",
|
||||
"foldingStopMarker": "^\\s*\\}",
|
||||
|
||||
"patterns": [
|
||||
{
|
||||
"name": "meta.preprocessor.script.cpp",
|
||||
"match": "^\\s*#\\s*(include|define|if|ifdef|ifndef|else|endif|pragma)\\b"
|
||||
},
|
||||
{
|
||||
"name": "meta.declaration.cpp",
|
||||
"begin": "^\\w*\\b(namespace|class|struct|enum|typedef|template)\\b",
|
||||
"beginCaptures": {
|
||||
"0": {
|
||||
"name": "keyword.other.declaration.cpp"
|
||||
}
|
||||
},
|
||||
"end": "(\\{|;)",
|
||||
"endCaptures": {
|
||||
"0": {
|
||||
"name": "punctuation.terminator.cpp"
|
||||
}
|
||||
},
|
||||
"patterns": [
|
||||
{
|
||||
"include": "#strings"
|
||||
},
|
||||
{
|
||||
"include": "#comments"
|
||||
},
|
||||
{
|
||||
"name": "keyword.other.cpp",
|
||||
"match": "\\b(public|private|protected|virtual|override|final)\\b"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"include": "#comments"
|
||||
},
|
||||
{
|
||||
"include": "#punctuation"
|
||||
},
|
||||
{
|
||||
"include": "#annotations"
|
||||
},
|
||||
{
|
||||
"include": "#keywords"
|
||||
},
|
||||
{
|
||||
"include": "#constants-and-special-vars"
|
||||
},
|
||||
{
|
||||
"include": "#operators"
|
||||
},
|
||||
{
|
||||
"include": "#strings"
|
||||
}
|
||||
],
|
||||
|
||||
"repository": {
|
||||
"comments": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "comment.block.empty.cpp",
|
||||
"match": "/\\*\\*/",
|
||||
"captures": {
|
||||
"0": {
|
||||
"name": "punctuation.definition.comment.cpp"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"include": "#comments-doc-oldschool"
|
||||
},
|
||||
{
|
||||
"include": "#comments-doc"
|
||||
},
|
||||
{
|
||||
"include": "#comments-inline"
|
||||
}
|
||||
]
|
||||
},
|
||||
"comments-doc-oldschool": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "comment.block.documentation.cpp",
|
||||
"begin": "/\\*\\*",
|
||||
"end": "\\*/",
|
||||
"patterns": [
|
||||
{
|
||||
"include": "#comments-doc-oldschool"
|
||||
},
|
||||
{
|
||||
"include": "#comments-block"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"comments-doc": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "comment.block.documentation.cpp",
|
||||
"begin": "///",
|
||||
"while": "^\\s*///",
|
||||
"patterns": [
|
||||
{
|
||||
"include": "#comments-inline"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"comments-inline": {
|
||||
"patterns": [
|
||||
{
|
||||
"include": "#comments-block"
|
||||
},
|
||||
{
|
||||
"match": "(//.*)$",
|
||||
"captures": {
|
||||
"1": {
|
||||
"name": "comment.line.double-slash.cpp"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"comments-block": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "comment.block.cpp",
|
||||
"begin": "/\\*",
|
||||
"end": "\\*/",
|
||||
"patterns": [
|
||||
{
|
||||
"include": "#comments-block"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"annotations": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "storage.type.annotation.cpp",
|
||||
"match": "__attribute__\\(\\w+\\)"
|
||||
}
|
||||
]
|
||||
},
|
||||
"constants-and-special-vars": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "constant.language.cpp",
|
||||
"match": "\\b(true|false|nullptr)\\b"
|
||||
},
|
||||
{
|
||||
"name": "variable.language.cpp",
|
||||
"match": "\\b(this|super)\\b"
|
||||
},
|
||||
{
|
||||
"name": "constant.numeric.cpp",
|
||||
"match": "\\b((0(x|X)[0-9a-fA-F]+)|(([0-9]+\\.?[0-9]*)|(\\.[0-9]+))((e|E)(\\+|-)?[0-9]+)?)\\b"
|
||||
},
|
||||
{
|
||||
"include": "#class-identifier"
|
||||
},
|
||||
{
|
||||
"include": "#function-identifier"
|
||||
}
|
||||
]
|
||||
},
|
||||
"class-identifier": {
|
||||
"patterns": [
|
||||
{
|
||||
"match": "\\b(bool|int|char|double|float|long|short|signed|unsigned|void)\\b",
|
||||
"name": "storage.type.primitive.cpp"
|
||||
},
|
||||
{
|
||||
"begin": "(\\b[A-Z]\\w*\\b)",
|
||||
"end": "(?!<)",
|
||||
"beginCaptures": {
|
||||
"1": {
|
||||
"name": "support.class.cpp"
|
||||
}
|
||||
},
|
||||
"patterns": [
|
||||
{
|
||||
"include": "#type-args"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"function-identifier": {
|
||||
"patterns": [
|
||||
{
|
||||
"match": "\\b([a-z_][a-zA-Z0-9_]*)\\s*\\(",
|
||||
"captures": {
|
||||
"1": {
|
||||
"name": "entity.name.function.cpp"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"type-args": {
|
||||
"begin": "(<)",
|
||||
"end": "(>)",
|
||||
"beginCaptures": {
|
||||
"1": {
|
||||
"name": "other.source.cpp"
|
||||
}
|
||||
},
|
||||
"endCaptures": {
|
||||
"1": {
|
||||
"name": "other.source.cpp"
|
||||
}
|
||||
},
|
||||
"patterns": [
|
||||
{
|
||||
"include": "#class-identifier"
|
||||
},
|
||||
{
|
||||
"match": ","
|
||||
},
|
||||
{
|
||||
"name": "keyword.declaration.cpp",
|
||||
"match": "extends"
|
||||
},
|
||||
{
|
||||
"include": "#comments"
|
||||
}
|
||||
]
|
||||
},
|
||||
"keywords": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "keyword.control.cpp",
|
||||
"match": "\\b(if|else|for|while|do|switch|case|break|continue|goto|return)\\b"
|
||||
},
|
||||
{
|
||||
"name": "keyword.operator.cpp",
|
||||
"match": "\\b(sizeof|typeid|decltype|new|delete)\\b"
|
||||
},
|
||||
{
|
||||
"name": "keyword.control.try.cpp",
|
||||
"match": "\\b(try|catch|throw)\\b"
|
||||
},
|
||||
{
|
||||
"name": "keyword.control.cpp",
|
||||
"match": "\\b(static|inline|virtual|override|const|volatile|explicit|friend|constexpr)\\b"
|
||||
}
|
||||
]
|
||||
},
|
||||
"operators": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "keyword.operator.comparison.cpp",
|
||||
"match": "(==|!=|<=?|>=?)"
|
||||
},
|
||||
{
|
||||
"name": "keyword.operator.arithmetic.cpp",
|
||||
"match": "(\\+|\\-|\\*|\\/|%)"
|
||||
},
|
||||
{
|
||||
"name": "keyword.operator.assignment.cpp",
|
||||
"match": "(=|\\+=|-=|\\*=|/=|%=)"
|
||||
},
|
||||
{
|
||||
"name": "keyword.operator.logical.cpp",
|
||||
"match": "(\\&\\&|\\|\\||!)"
|
||||
},
|
||||
{
|
||||
"name": "keyword.operator.bitwise.cpp",
|
||||
"match": "(<<|>>|\\&|\\||\\^|~)"
|
||||
}
|
||||
]
|
||||
},
|
||||
"string-interp": {
|
||||
"patterns": [
|
||||
{
|
||||
"match": "\\$([a-zA-Z0-9_]+)",
|
||||
"captures": {
|
||||
"1": {
|
||||
"name": "variable.parameter.cpp"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "string.interpolated.expression.cpp",
|
||||
"begin": "\\$\\{",
|
||||
"end": "\\}",
|
||||
"patterns": [
|
||||
{
|
||||
"include": "#constants-and-special-vars",
|
||||
"name": "variable.parameter.cpp"
|
||||
},
|
||||
{
|
||||
"include": "#strings"
|
||||
},
|
||||
{
|
||||
"name": "variable.parameter.cpp",
|
||||
"match": "[a-zA-Z0-9_]+"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "constant.character.escape.cpp",
|
||||
"match": "\\\\."
|
||||
}
|
||||
]
|
||||
},
|
||||
"strings": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "string.quoted.double.cpp",
|
||||
"begin": "\"",
|
||||
"end": "\"",
|
||||
"patterns": [
|
||||
{
|
||||
"name": "constant.character.escape.cpp",
|
||||
"match": "\\\\."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "string.quoted.single.cpp",
|
||||
"begin": "'",
|
||||
"end": "'",
|
||||
"patterns": [
|
||||
{
|
||||
"name": "constant.character.escape.cpp",
|
||||
"match": "\\\\."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"punctuation": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "punctuation.comma.cpp",
|
||||
"match": ","
|
||||
},
|
||||
{
|
||||
"name": "punctuation.terminator.cpp",
|
||||
"match": ";"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
@ -140,7 +140,7 @@
|
||||
"clear": "Clear",
|
||||
"pinPost": "Pin this post",
|
||||
"unpinPost": "Unpin this post",
|
||||
"postRestoreFromLocal": "Restore from local",
|
||||
"postRestoreFromLocal": "Restored",
|
||||
"postAutoSaveAt": "Auto saved at @date",
|
||||
"postCategoriesAndTags": "Categories n' Tags",
|
||||
"postPublishDate": "Publish Date",
|
||||
@ -367,7 +367,7 @@
|
||||
"bsRegisteringPushNotify": "Enabling Push Notifications",
|
||||
"bsDismissibleErrorHint": "Click anywhere to ignore this error",
|
||||
"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",
|
||||
"themeColorRed": "Modern Red",
|
||||
"themeColorBlue": "Classic Blue",
|
||||
@ -477,5 +477,14 @@
|
||||
"agedTheme": "Old school style theme",
|
||||
"agedThemeDesc": "Downgrade the global theme to Material Design 2. Unexpected issues may occur. For experimental use only.",
|
||||
"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"
|
||||
}
|
||||
|
@ -363,7 +363,7 @@
|
||||
"bsRegisteringPushNotify": "正在启用推送通知",
|
||||
"bsDismissibleErrorHint": "点击任意地方忽略此错误",
|
||||
"postShareContent": "@content\n\n@username 在 Solar Network\n原帖地址:@link",
|
||||
"postShareSubject": "@username 在 Solar Network 上发布了一篇帖子",
|
||||
"postShareSubject": "@username 在 Solar Network 发表的 @title",
|
||||
"themeColor": "全局主题色",
|
||||
"themeColorRed": "现代红",
|
||||
"themeColorBlue": "经典蓝",
|
||||
@ -473,5 +473,14 @@
|
||||
"agedTheme": "过时主题",
|
||||
"agedThemeDesc": "将全局主题降级为 Material Design 2,可能发生意料之外的问题,仅供实验使用",
|
||||
"appBackgroundImage": "全局背景图片",
|
||||
"appBackgroundImageDesc": "全局背景图片将会在所有页面中展示"
|
||||
"appBackgroundImageDesc": "全局背景图片将会在所有页面中展示",
|
||||
"authPreferences": "安全偏好设置",
|
||||
"authPreferencesDesc": "调整账号的安全行为模式",
|
||||
"authMaximumAuthSteps": "最大认证步数",
|
||||
"authMaximumAuthStepsDesc": "登陆时最多的验证步数,值越高则越安全,反之则会相对方便;默认设置为 2",
|
||||
"auditLog": "活动日志",
|
||||
"shareImage": "分享图片",
|
||||
"shareImageFooter": "上 Solar Network 看更多有趣帖子",
|
||||
"fileSavedAt": "文件保存于 @path",
|
||||
"showIp": "显示 IP 地址"
|
||||
}
|
||||
|
@ -38,6 +38,8 @@ PODS:
|
||||
- file_picker (0.0.1):
|
||||
- DKImagePickerController/PhotoGallery
|
||||
- Flutter
|
||||
- file_saver (0.0.1):
|
||||
- Flutter
|
||||
- Firebase/Analytics (11.2.0):
|
||||
- Firebase/Core
|
||||
- Firebase/Core (11.2.0):
|
||||
@ -166,6 +168,9 @@ PODS:
|
||||
- Flutter
|
||||
- flutter_secure_storage (6.0.0):
|
||||
- Flutter
|
||||
- flutter_udid (0.0.1):
|
||||
- Flutter
|
||||
- SAMKeychain
|
||||
- flutter_webrtc (0.11.3):
|
||||
- Flutter
|
||||
- WebRTC-SDK (= 125.6422.04)
|
||||
@ -259,6 +264,7 @@ PODS:
|
||||
- PromisesObjC (= 2.4.0)
|
||||
- protocol_handler_ios (0.0.1):
|
||||
- Flutter
|
||||
- SAMKeychain (1.5.3)
|
||||
- screen_brightness_ios (0.1.0):
|
||||
- Flutter
|
||||
- SDWebImage (5.19.7):
|
||||
@ -269,7 +275,7 @@ PODS:
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- sqflite (0.0.3):
|
||||
- sqflite_darwin (0.0.4):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- "sqlite3 (3.46.1+1)":
|
||||
@ -304,6 +310,7 @@ DEPENDENCIES:
|
||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`)
|
||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/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_core (from `.symlinks/plugins/firebase_core/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_native_splash (from `.symlinks/plugins/flutter_native_splash/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`)
|
||||
- gal (from `.symlinks/plugins/gal/darwin`)
|
||||
- image_cropper (from `.symlinks/plugins/image_cropper/ios`)
|
||||
@ -334,7 +342,7 @@ DEPENDENCIES:
|
||||
- screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`)
|
||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
|
||||
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
||||
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
|
||||
@ -364,6 +372,7 @@ SPEC REPOS:
|
||||
- nanopb
|
||||
- PromisesObjC
|
||||
- PromisesSwift
|
||||
- SAMKeychain
|
||||
- SDWebImage
|
||||
- sqlite3
|
||||
- SwiftyGif
|
||||
@ -377,6 +386,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/device_info_plus/ios"
|
||||
file_picker:
|
||||
:path: ".symlinks/plugins/file_picker/ios"
|
||||
file_saver:
|
||||
:path: ".symlinks/plugins/file_saver/ios"
|
||||
firebase_analytics:
|
||||
:path: ".symlinks/plugins/firebase_analytics/ios"
|
||||
firebase_core:
|
||||
@ -401,6 +412,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
||||
flutter_secure_storage:
|
||||
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
||||
flutter_udid:
|
||||
:path: ".symlinks/plugins/flutter_udid/ios"
|
||||
flutter_webrtc:
|
||||
:path: ".symlinks/plugins/flutter_webrtc/ios"
|
||||
gal:
|
||||
@ -437,8 +450,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/share_plus/ios"
|
||||
shared_preferences_foundation:
|
||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||
sqflite:
|
||||
:path: ".symlinks/plugins/sqflite/darwin"
|
||||
sqflite_darwin:
|
||||
:path: ".symlinks/plugins/sqflite_darwin/darwin"
|
||||
sqlite3_flutter_libs:
|
||||
:path: ".symlinks/plugins/sqlite3_flutter_libs/ios"
|
||||
url_launcher_ios:
|
||||
@ -454,6 +467,7 @@ SPEC CHECKSUMS:
|
||||
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
||||
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
|
||||
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
|
||||
Firebase: 98e6bf5278170668a7983e12971a66b2cd57fc8c
|
||||
firebase_analytics: fbc57838bdb94eef1e0ff504f127d974ff2981ad
|
||||
firebase_core: 2bedc3136ec7c7b8561c6123ed0239387b53f2af
|
||||
@ -480,6 +494,7 @@ SPEC CHECKSUMS:
|
||||
flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086
|
||||
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
|
||||
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
|
||||
flutter_udid: a2482c67a61b9c806ef59dd82ed8d007f1b7ac04
|
||||
flutter_webrtc: 75b868e4f9e817c7a9a42ca4b6169063de4eec9f
|
||||
gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1
|
||||
GoogleAppMeasurement: 76d4f8b36b03bd8381fa9a7fe2cc7f99c0a2e93a
|
||||
@ -501,11 +516,12 @@ SPEC CHECKSUMS:
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
|
||||
protocol_handler_ios: a5db8abc38526ee326988b808be621e5fd568990
|
||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
|
||||
SDWebImage: 8a6b7b160b4d710e2a22b6900e25301075c34cb3
|
||||
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||
sqflite_darwin: a553b1fd6fe66f53bbb0fe5b4f5bab93f08d7a13
|
||||
sqlite3: 0bb0e6389d824e40296f531b858a2a0b71c0d2fb
|
||||
sqlite3_flutter_libs: c00457ebd31e59fa6bb830380ddba24d44fbcd3b
|
||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||
|
@ -59,6 +59,7 @@
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
showGraphicsOverview = "Yes"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
|
@ -83,7 +83,6 @@
|
||||
</array>
|
||||
<key>CFBundleLocalizations</key>
|
||||
<array>
|
||||
<string>zh_CN</string>
|
||||
<string>en</string>
|
||||
</array>
|
||||
<key>UIStatusBarHidden</key>
|
||||
|
@ -43,14 +43,17 @@ class PostEditorController extends GetxController {
|
||||
|
||||
RxBool isRestoreFromLocal = false.obs;
|
||||
Rx<DateTime?> lastSaveTime = Rx(null);
|
||||
Timer? _saveTimer;
|
||||
Future? _saveFuture;
|
||||
|
||||
PostEditorController() {
|
||||
SharedPreferences.getInstance().then((inst) {
|
||||
_prefs = inst;
|
||||
_saveTimer = Timer.periodic(
|
||||
const Duration(seconds: 3),
|
||||
(Timer t) {
|
||||
});
|
||||
contentController.addListener(() {
|
||||
contentLength.value = contentController.text.length;
|
||||
_saveFuture ??= Future.delayed(
|
||||
const Duration(seconds: 1),
|
||||
() {
|
||||
if (isNotEmpty) {
|
||||
localSave();
|
||||
lastSaveTime.value = DateTime.now();
|
||||
@ -59,12 +62,10 @@ class PostEditorController extends GetxController {
|
||||
localClear();
|
||||
lastSaveTime.value = null;
|
||||
}
|
||||
_saveFuture = null;
|
||||
},
|
||||
);
|
||||
});
|
||||
contentController.addListener(() {
|
||||
contentLength.value = contentController.text.length;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> editOverview(BuildContext context) {
|
||||
@ -355,8 +356,6 @@ class PostEditorController extends GetxController {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_saveTimer?.cancel();
|
||||
|
||||
titleController.dispose();
|
||||
descriptionController.dispose();
|
||||
contentController.dispose();
|
||||
|
@ -1,9 +1,12 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:get/get.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
import 'package:solian/models/attachment.dart';
|
||||
import 'package:solian/models/pagination.dart';
|
||||
import 'package:solian/models/post.dart';
|
||||
import 'package:solian/providers/content/attachment.dart';
|
||||
import 'package:solian/providers/content/posts.dart';
|
||||
import 'package:solian/providers/last_read.dart';
|
||||
|
||||
@ -31,9 +34,18 @@ class PostListController extends GetxController {
|
||||
pagingController.addPageRequestListener(_onPagingControllerRequest);
|
||||
}
|
||||
|
||||
Completer<void>? _pagingLoadCompleter;
|
||||
|
||||
Future<void> _onPagingControllerRequest(int pageKey) async {
|
||||
try {
|
||||
if (_pagingLoadCompleter != null) {
|
||||
await _pagingLoadCompleter!.future;
|
||||
return;
|
||||
}
|
||||
_pagingLoadCompleter = Completer();
|
||||
final result = await loadMore();
|
||||
_pagingLoadCompleter!.complete();
|
||||
_pagingLoadCompleter = null;
|
||||
|
||||
if (result != null && hasMore.value) {
|
||||
pagingController.appendPage(result, nextPageKey.value);
|
||||
@ -97,9 +109,6 @@ class PostListController extends GetxController {
|
||||
hasMore.value = false;
|
||||
}
|
||||
|
||||
final idx = <dynamic>{};
|
||||
postList.retainWhere((x) => idx.add(x.id));
|
||||
|
||||
if (postList.isNotEmpty) {
|
||||
var lastId = postList.map((x) => x.id).reduce(max);
|
||||
Get.find<LastReadProvider>().feedLastReadAt = lastId;
|
||||
@ -111,35 +120,39 @@ class PostListController extends GetxController {
|
||||
Future<List<Post>?> _loadPosts(int pageKey) async {
|
||||
isBusy.value = true;
|
||||
|
||||
final PostProvider provider = Get.find();
|
||||
final PostProvider posts = Get.find();
|
||||
|
||||
Response resp;
|
||||
try {
|
||||
if (author != null) {
|
||||
resp = await provider.listPost(
|
||||
resp = await posts.listPost(
|
||||
pageKey,
|
||||
author: author,
|
||||
take: 10,
|
||||
);
|
||||
} else {
|
||||
switch (mode.value) {
|
||||
case 2:
|
||||
resp = await provider.listRecommendations(
|
||||
resp = await posts.listRecommendations(
|
||||
pageKey,
|
||||
channel: 'shuffle',
|
||||
realm: realm,
|
||||
take: 10,
|
||||
);
|
||||
break;
|
||||
case 1:
|
||||
resp = await provider.listRecommendations(
|
||||
resp = await posts.listRecommendations(
|
||||
pageKey,
|
||||
channel: 'friends',
|
||||
realm: realm,
|
||||
take: 10,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
resp = await provider.listRecommendations(
|
||||
resp = await posts.listRecommendations(
|
||||
pageKey,
|
||||
realm: realm,
|
||||
take: 10,
|
||||
);
|
||||
break;
|
||||
}
|
||||
@ -153,6 +166,27 @@ class PostListController extends GetxController {
|
||||
final result = PaginationResult.fromJson(resp.body);
|
||||
final out = result.data?.map((e) => Post.fromJson(e)).toList();
|
||||
|
||||
final AttachmentProvider attach = Get.find();
|
||||
|
||||
if (out != null) {
|
||||
final attachmentIds = out
|
||||
.mapMany((x) => x.body['attachments'] ?? [])
|
||||
.cast<String>()
|
||||
.toSet()
|
||||
.toList();
|
||||
final attachmentOut = await attach.listMetadata(attachmentIds);
|
||||
|
||||
for (var idx = 0; idx < out.length; idx++) {
|
||||
final rids = List<String>.from(out[idx].body['attachments'] ?? []);
|
||||
out[idx].preload = PostPreload(
|
||||
attachments: attachmentOut
|
||||
.where((x) => x != null && rids.contains(x.rid))
|
||||
.cast<Attachment>()
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
postTotal.value = result.count;
|
||||
|
||||
return out;
|
||||
|
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,10 +1,19 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:solian/models/account.dart';
|
||||
import 'package:solian/models/attachment.dart';
|
||||
import 'package:solian/models/post_categories.dart';
|
||||
import 'package:solian/models/realm.dart';
|
||||
|
||||
part 'post.g.dart';
|
||||
|
||||
class PostPreload {
|
||||
List<Attachment> attachments;
|
||||
|
||||
PostPreload({
|
||||
required this.attachments,
|
||||
});
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class Post {
|
||||
int id;
|
||||
@ -15,6 +24,7 @@ class Post {
|
||||
String? alias;
|
||||
String? areaAlias;
|
||||
dynamic body;
|
||||
int visibility;
|
||||
List<Tag>? tags;
|
||||
List<Category>? categories;
|
||||
List<Post>? replies;
|
||||
@ -33,6 +43,9 @@ class Post {
|
||||
Account author;
|
||||
PostMetric? metric;
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
PostPreload? preload;
|
||||
|
||||
Post({
|
||||
required this.id,
|
||||
required this.createdAt,
|
||||
@ -43,6 +56,7 @@ class Post {
|
||||
required this.areaAlias,
|
||||
required this.type,
|
||||
required this.body,
|
||||
required this.visibility,
|
||||
required this.tags,
|
||||
required this.categories,
|
||||
required this.replies,
|
||||
|
@ -20,6 +20,7 @@ Post _$PostFromJson(Map<String, dynamic> json) => Post(
|
||||
areaAlias: json['area_alias'] as String?,
|
||||
type: json['type'] as String,
|
||||
body: json['body'],
|
||||
visibility: (json['visibility'] as num).toInt(),
|
||||
tags: (json['tags'] as List<dynamic>?)
|
||||
?.map((e) => Tag.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
@ -67,6 +68,7 @@ Map<String, dynamic> _$PostToJson(Post instance) => <String, dynamic>{
|
||||
'alias': instance.alias,
|
||||
'area_alias': instance.areaAlias,
|
||||
'body': instance.body,
|
||||
'visibility': instance.visibility,
|
||||
'tags': instance.tags?.map((e) => e.toJson()).toList(),
|
||||
'categories': instance.categories?.map((e) => e.toJson()).toList(),
|
||||
'replies': instance.replies?.map((e) => e.toJson()).toList(),
|
||||
|
@ -125,7 +125,7 @@ class AuthProvider extends GetConnect {
|
||||
userAgent: await ServiceFinder.getUserAgent(),
|
||||
sendUserAgent: true,
|
||||
);
|
||||
client.httpClient.addAuthenticator(requestAuthenticator);
|
||||
client.httpClient.addRequestModifier(requestAuthenticator);
|
||||
client.httpClient.baseUrl = ServiceFinder.buildUrl(service, null);
|
||||
|
||||
return client;
|
||||
|
@ -23,6 +23,21 @@ class AttachmentProvider extends GetConnect {
|
||||
|
||||
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(
|
||||
List<String> rid, {
|
||||
noCache = false,
|
||||
@ -41,25 +56,27 @@ class AttachmentProvider extends GetConnect {
|
||||
}
|
||||
}
|
||||
|
||||
final resp = await get(
|
||||
'/attachments?take=${pendingQuery.length}&id=${pendingQuery.join(',')}',
|
||||
);
|
||||
if (resp.statusCode != 200) return result;
|
||||
if (pendingQuery.isNotEmpty) {
|
||||
final resp = await get(
|
||||
'/attachments?take=${pendingQuery.length}&id=${pendingQuery.join(',')}',
|
||||
);
|
||||
if (resp.statusCode != 200) return result;
|
||||
|
||||
final rawOut = PaginationResult.fromJson(resp.body);
|
||||
if (rawOut.data == null) return result;
|
||||
final rawOut = PaginationResult.fromJson(resp.body);
|
||||
if (rawOut.data == null) return result;
|
||||
|
||||
final List<Attachment> out =
|
||||
rawOut.data!.map((x) => Attachment.fromJson(x)).toList();
|
||||
for (final item in out) {
|
||||
if (item.destination != 0 && item.isAnalyzed) {
|
||||
_cachedResponses[item.rid] = item;
|
||||
final List<Attachment> out =
|
||||
rawOut.data!.map((x) => Attachment.fromJson(x)).toList();
|
||||
for (final item in out) {
|
||||
if (item.destination != 0 && item.isAnalyzed) {
|
||||
_cachedResponses[item.rid] = item;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (var i = 0; i < out.length; i++) {
|
||||
for (var j = 0; j < rid.length; j++) {
|
||||
if (out[i].rid == rid[j]) {
|
||||
result[j] = out[i];
|
||||
for (var i = 0; i < out.length; i++) {
|
||||
for (var j = 0; j < rid.length; j++) {
|
||||
if (out[i].rid == rid[j]) {
|
||||
result[j] = out[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,22 +3,11 @@ import 'package:solian/exceptions/request.dart';
|
||||
import 'package:solian/exceptions/unauthorized.dart';
|
||||
import 'package:solian/models/post.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/services.dart';
|
||||
|
||||
class PostProvider extends GetConnect {
|
||||
@override
|
||||
void onInit() {
|
||||
httpClient.baseUrl = ServiceFinder.buildUrl('interactive', null);
|
||||
}
|
||||
|
||||
class PostProvider extends GetxController {
|
||||
Future<Response> seeWhatsNew(int pivot) async {
|
||||
GetConnect client;
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.value) {
|
||||
client = await auth.configureClient('co');
|
||||
} else {
|
||||
client = await ServiceFinder.configureClient('co');
|
||||
}
|
||||
final client = await auth.configureClient('co');
|
||||
final resp = await client.get('/whats-new?pivot=$pivot');
|
||||
if (resp.statusCode != 200) {
|
||||
throw RequestException(resp);
|
||||
@ -28,19 +17,14 @@ class PostProvider extends GetConnect {
|
||||
}
|
||||
|
||||
Future<Response> listRecommendations(int page,
|
||||
{String? realm, String? channel}) async {
|
||||
GetConnect client;
|
||||
final AuthProvider auth = Get.find();
|
||||
{String? realm, String? channel, int take = 10}) async {
|
||||
final queries = [
|
||||
'take=${10}',
|
||||
'take=$take',
|
||||
'offset=$page',
|
||||
if (realm != null) 'realm=$realm',
|
||||
];
|
||||
if (auth.isAuthorized.value) {
|
||||
client = await auth.configureClient('co');
|
||||
} else {
|
||||
client = await ServiceFinder.configureClient('co');
|
||||
}
|
||||
final AuthProvider auth = Get.find();
|
||||
final client = await auth.configureClient('interactive');
|
||||
final resp = await client.get(
|
||||
channel == null
|
||||
? '/recommendations?${queries.join('&')}'
|
||||
@ -70,17 +54,40 @@ class PostProvider extends GetConnect {
|
||||
return resp;
|
||||
}
|
||||
|
||||
Future<Response> listPost(int page,
|
||||
{String? realm, String? author, tag, category}) async {
|
||||
Future<Response> searchPost(String probe, int page,
|
||||
{String? realm, String? author, tag, category, int take = 10}) async {
|
||||
final queries = [
|
||||
'take=${10}',
|
||||
'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 resp = await get('/posts?${queries.join('&')}');
|
||||
final AuthProvider auth = Get.find();
|
||||
final client = await auth.configureClient('co');
|
||||
final resp = await client.get('/posts/search?${queries.join('&')}');
|
||||
if (resp.statusCode != 200) {
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
Future<Response> listPost(int page,
|
||||
{String? realm, String? author, tag, category, int take = 10}) async {
|
||||
final queries = [
|
||||
'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?${queries.join('&')}');
|
||||
if (resp.statusCode != 200) {
|
||||
throw RequestException(resp);
|
||||
}
|
||||
@ -89,7 +96,10 @@ class PostProvider extends GetConnect {
|
||||
}
|
||||
|
||||
Future<Response> listPostReplies(String alias, int page) async {
|
||||
final resp = await get('/posts/$alias/replies?take=${10}&offset=$page');
|
||||
final AuthProvider auth = Get.find();
|
||||
final client = await auth.configureClient('co');
|
||||
final resp =
|
||||
await client.get('/posts/$alias/replies?take=${10}&offset=$page');
|
||||
if (resp.statusCode != 200) {
|
||||
throw RequestException(resp);
|
||||
}
|
||||
@ -98,7 +108,9 @@ class PostProvider extends GetConnect {
|
||||
}
|
||||
|
||||
Future<List<Post>> listPostFeaturedReply(String alias, {int take = 1}) async {
|
||||
final resp = await get('/posts/$alias/replies/featured?take=$take');
|
||||
final AuthProvider auth = Get.find();
|
||||
final client = await auth.configureClient('co');
|
||||
final resp = await client.get('/posts/$alias/replies/featured?take=$take');
|
||||
if (resp.statusCode != 200) {
|
||||
throw RequestException(resp);
|
||||
}
|
||||
@ -107,16 +119,9 @@ class PostProvider extends GetConnect {
|
||||
}
|
||||
|
||||
Future<Response> getPost(String alias) async {
|
||||
final resp = await get('/posts/$alias');
|
||||
if (resp.statusCode != 200) {
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
Future<Response> getArticle(String alias) async {
|
||||
final resp = await get('/articles/$alias');
|
||||
final AuthProvider auth = Get.find();
|
||||
final client = await auth.configureClient('co');
|
||||
final resp = await client.get('/posts/$alias');
|
||||
if (resp.statusCode != 200) {
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
@ -4,19 +4,19 @@ import 'package:intl/intl.dart';
|
||||
class ExperienceProvider extends GetxController {
|
||||
static List<int> experienceToLevelRequirements = [
|
||||
0, // Level 0
|
||||
100, // Level 1
|
||||
400, // Level 2
|
||||
900, // Level 3
|
||||
1600, // Level 4
|
||||
2500, // Level 5
|
||||
3600, // Level 6
|
||||
4900, // Level 7
|
||||
6400, // Level 8
|
||||
8100, // Level 9
|
||||
10000, // Level 10
|
||||
12100, // Level 11
|
||||
14400, // Level 12
|
||||
36800 // Level 13
|
||||
1000, // Level 1
|
||||
4000, // Level 2
|
||||
9000, // Level 3
|
||||
16000, // Level 4
|
||||
25000, // Level 5
|
||||
36000, // Level 6
|
||||
49000, // Level 7
|
||||
64000, // Level 8
|
||||
81000, // Level 9
|
||||
100000, // Level 10
|
||||
121000, // Level 11
|
||||
144000, // Level 12
|
||||
368000 // Level 13
|
||||
];
|
||||
|
||||
static List<String> levelLabelMapping =
|
||||
@ -35,7 +35,7 @@ class ExperienceProvider extends GetxController {
|
||||
final idx = experienceToLevelRequirements.indexOf(exp);
|
||||
if (idx + 1 >= experienceToLevelRequirements.length) return 1;
|
||||
final nextExp = experienceToLevelRequirements[idx + 1];
|
||||
return exp / nextExp;
|
||||
return (experience - exp).abs() / (exp - nextExp).abs();
|
||||
}
|
||||
|
||||
static String calcLevelUpProgressLevel(int experience) {
|
||||
@ -43,9 +43,9 @@ class ExperienceProvider extends GetxController {
|
||||
.firstWhere((x) => x <= experience);
|
||||
final idx = experienceToLevelRequirements.indexOf(exp);
|
||||
if (idx + 1 >= experienceToLevelRequirements.length) return 'Infinity';
|
||||
final nextExp = experienceToLevelRequirements[idx + 1];
|
||||
final nextExp = exp - experienceToLevelRequirements[idx + 1];
|
||||
final formatter =
|
||||
NumberFormat.compactCurrency(symbol: '', decimalDigits: 1);
|
||||
return '${formatter.format(exp)}/${formatter.format(nextExp)}';
|
||||
return '${formatter.format((exp - experience).abs())}/${formatter.format(nextExp.abs())}';
|
||||
}
|
||||
}
|
||||
|
@ -3,9 +3,9 @@ import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
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:flutter_udid/flutter_udid.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:solian/exceptions/request.dart';
|
||||
@ -165,10 +165,11 @@ class WebSocketProvider extends GetxController {
|
||||
|
||||
late final String? token;
|
||||
late final String provider;
|
||||
final deviceUuid = await _getDeviceUuid();
|
||||
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');
|
||||
}
|
||||
@ -195,33 +196,7 @@ class WebSocketProvider extends GetxController {
|
||||
}
|
||||
|
||||
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;
|
||||
if (PlatformInfo.isWeb) return null;
|
||||
return await FlutterUdid.consistentUdid;
|
||||
}
|
||||
}
|
||||
|
@ -2,11 +2,14 @@ import 'package:animations/animations.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:solian/bootstrapper.dart';
|
||||
import 'package:solian/models/post.dart';
|
||||
import 'package:solian/models/realm.dart';
|
||||
import 'package:solian/screens/about.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/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_page.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/chat.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/feed/draft_box.dart';
|
||||
import 'package:solian/screens/posts/draft_box.dart';
|
||||
import 'package:solian/screens/realms.dart';
|
||||
import 'package:solian/screens/realms/realm_detail.dart';
|
||||
import 'package:solian/screens/realms/realm_organize.dart';
|
||||
@ -94,7 +97,7 @@ abstract class AppRouter {
|
||||
name: 'postSearch',
|
||||
builder: (context, state) => TitleShell(
|
||||
state: state,
|
||||
child: FeedSearchScreen(
|
||||
child: PostSearchScreen(
|
||||
tag: state.uri.queryParameters['tag'],
|
||||
category: state.uri.queryParameters['category'],
|
||||
),
|
||||
@ -107,6 +110,7 @@ abstract class AppRouter {
|
||||
state: state,
|
||||
child: PostDetailScreen(
|
||||
id: state.pathParameters['id']!,
|
||||
post: state.extra as Post?,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -264,6 +268,22 @@ abstract class AppRouter {
|
||||
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(
|
||||
path: '/account/view/:name',
|
||||
name: 'accountProfilePage',
|
||||
|
@ -129,6 +129,24 @@ class _AccountScreenState extends State<AccountScreen> {
|
||||
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)
|
||||
ListTile(
|
||||
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,11 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get/get_connect/http/src/exceptions/exceptions.dart';
|
||||
import 'package:google_fonts/google_fonts.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 NotificationPreferencesScreen extends StatefulWidget {
|
||||
const NotificationPreferencesScreen({super.key});
|
||||
@ -59,10 +59,10 @@ class _NotificationPreferencesScreenState
|
||||
});
|
||||
if (resp.statusCode != 200) {
|
||||
context.showErrorDialog(RequestException(resp));
|
||||
} else {
|
||||
context.showSnackbar('preferencesApplied'.tr);
|
||||
}
|
||||
|
||||
context.showSnackbar('preferencesApplied'.tr);
|
||||
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
|
||||
@ -76,7 +76,7 @@ class _NotificationPreferencesScreenState
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
ListTile(
|
||||
tileColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||
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_animate/flutter_animate.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:get/get.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/services.dart';
|
||||
import 'package:solian/widgets/account/account_avatar.dart';
|
||||
import 'package:solian/widgets/loading_indicator.dart';
|
||||
|
||||
class PersonalizeScreen extends StatefulWidget {
|
||||
const PersonalizeScreen({super.key});
|
||||
@ -188,11 +188,11 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
||||
|
||||
return ListView(
|
||||
children: [
|
||||
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
const Gap(24),
|
||||
Stack(
|
||||
children: [
|
||||
AccountAvatar(content: _avatar, radius: 40),
|
||||
AttachedCircleAvatar(content: _avatar, radius: 40),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 40,
|
||||
|
@ -26,7 +26,6 @@ import 'package:solian/widgets/app_bar_leading.dart';
|
||||
import 'package:solian/widgets/attachments/attachment_list.dart';
|
||||
import 'package:solian/widgets/daily_sign/history_chart.dart';
|
||||
import 'package:solian/widgets/posts/post_list.dart';
|
||||
import 'package:solian/widgets/posts/post_warped_list.dart';
|
||||
import 'package:solian/widgets/reports/abuse_report.dart';
|
||||
import 'package:solian/widgets/root_container.dart';
|
||||
import 'package:solian/widgets/sized_container.dart';
|
||||
@ -261,7 +260,8 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
||||
const Gap(8),
|
||||
const Gap(8),
|
||||
if (_userinfo != null)
|
||||
AccountAvatar(content: _userinfo!.avatar, radius: 16),
|
||||
AttachedCircleAvatar(
|
||||
content: _userinfo!.avatar, radius: 16),
|
||||
const Gap(12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
@ -588,8 +588,6 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
||||
color:
|
||||
Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
child: PostListEntryWidget(
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
item: element,
|
||||
isClickable: true,
|
||||
isNestedClickable: true,
|
||||
@ -609,7 +607,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
if (_userinfo != null)
|
||||
PostWarpedListWidget(
|
||||
ControlledPostListWidget(
|
||||
isPinned: false,
|
||||
controller: _postController.pagingController,
|
||||
onUpdate: () => _postController.reloadAllOver(),
|
||||
|
@ -1,5 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/exts.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/theme.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:uuid/uuid.dart';
|
||||
|
||||
@ -132,7 +132,7 @@ class _ChannelOrganizeScreenState extends State<ChannelOrganizeScreen> {
|
||||
top: false,
|
||||
child: Column(
|
||||
children: [
|
||||
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
if (widget.edit != null)
|
||||
MaterialBanner(
|
||||
leading: const Icon(Icons.edit),
|
||||
|
@ -1,4 +1,5 @@
|
||||
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:gap/gap.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/chat/call/chat_call_indicator.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/sidebar/empty_placeholder.dart';
|
||||
|
||||
@ -41,14 +43,23 @@ class ChatListShell extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RootContainer(
|
||||
child: Row(
|
||||
child: ResizableContainer(
|
||||
direction: Axis.horizontal,
|
||||
divider: ResizableDivider(
|
||||
thickness: 0.3,
|
||||
color: Theme.of(context).dividerColor.withOpacity(0.3),
|
||||
),
|
||||
children: [
|
||||
const SizedBox(
|
||||
width: 360,
|
||||
const ResizableChild(
|
||||
minSize: 280,
|
||||
maxSize: 520,
|
||||
size: ResizableSize.pixels(360),
|
||||
child: ChatList(),
|
||||
),
|
||||
const VerticalDivider(thickness: 0.3, width: 0.3),
|
||||
Expanded(child: child ?? const EmptyPagePlaceholder()),
|
||||
ResizableChild(
|
||||
minSize: 280,
|
||||
child: child ?? const EmptyPagePlaceholder(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@ -69,6 +80,8 @@ class _ChatListState extends State<ChatList> {
|
||||
|
||||
late final ChannelProvider _channels = Get.find();
|
||||
|
||||
bool _isBusy = true;
|
||||
|
||||
List<Channel> _sortChannels(List<Channel> channels) {
|
||||
channels.sort(
|
||||
(a, b) =>
|
||||
@ -117,18 +130,25 @@ class _ChatListState extends State<ChatList> {
|
||||
final ctrl = ChatEventController();
|
||||
await ctrl.initialize();
|
||||
final messages = await ctrl.src.getLastInAllChannels();
|
||||
setState(() {
|
||||
_lastMessages = messages
|
||||
.map((k, v) => MapEntry(k, v.firstOrNull))
|
||||
.cast<int, LocalMessageEventTableData>();
|
||||
});
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_lastMessages = messages
|
||||
.map((k, v) => MapEntry(k, v.firstOrNull))
|
||||
.cast<int, LocalMessageEventTableData>();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadLastMessages().then((_) {
|
||||
_loadAllChannels();
|
||||
if (!mounted) return;
|
||||
_loadAllChannels().then((_) {
|
||||
if (mounted) {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -143,16 +163,7 @@ class _ChatListState extends State<ChatList> {
|
||||
child: ResponsiveRootContainer(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: Obx(() {
|
||||
final adaptive = AppBarLeadingButton.adaptive(context);
|
||||
if (adaptive != null) return adaptive;
|
||||
if (_channels.isLoading.value) {
|
||||
return const CircularProgressIndicator(
|
||||
strokeWidth: 3,
|
||||
).paddingAll(18);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
}),
|
||||
leading: AppBarLeadingButton.adaptive(context),
|
||||
title: AppBarTitle('chat'.tr),
|
||||
centerTitle: true,
|
||||
toolbarHeight: AppTheme.toolbarHeight(context),
|
||||
@ -252,7 +263,7 @@ class _ChatListState extends State<ChatList> {
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
AccountAvatar(
|
||||
AttachedCircleAvatar(
|
||||
content: x.avatar,
|
||||
radius: 14,
|
||||
fallbackWidget: const Icon(
|
||||
@ -280,6 +291,7 @@ class _ChatListState extends State<ChatList> {
|
||||
return Column(
|
||||
children: [
|
||||
const ChatCallCurrentIndicator(),
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
children: [
|
||||
|
@ -75,10 +75,12 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
||||
final src = Get.find<MessagesFetchingProvider>();
|
||||
final out = await src.getWhatsNewEvents(_lastRead.messagesLastReadAt!);
|
||||
if (out == null) return;
|
||||
setState(() {
|
||||
_currentMessages = out.$1;
|
||||
_currentMessagesCount = out.$2;
|
||||
});
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_currentMessages = out.$1;
|
||||
_currentMessagesCount = out.$2;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
bool _signingDaily = true;
|
||||
@ -387,9 +389,10 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
||||
onUpdate: (_) {
|
||||
_pullPosts();
|
||||
},
|
||||
backgroundColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainerLow,
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 4,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -523,7 +526,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
||||
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:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
@ -13,8 +14,9 @@ import 'package:solian/widgets/account/signin_required_overlay.dart';
|
||||
import 'package:solian/widgets/current_state_action.dart';
|
||||
import 'package:solian/widgets/app_bar_leading.dart';
|
||||
import 'package:solian/widgets/navigation/realm_switcher.dart';
|
||||
import 'package:solian/widgets/posts/post_creation.dart';
|
||||
import 'package:solian/widgets/posts/post_list.dart';
|
||||
import 'package:solian/widgets/posts/post_shuffle_swiper.dart';
|
||||
import 'package:solian/widgets/posts/post_warped_list.dart';
|
||||
import 'package:solian/widgets/root_container.dart';
|
||||
|
||||
class ExploreScreen extends StatefulWidget {
|
||||
@ -80,62 +82,98 @@ class _ExploreScreenState extends State<ExploreScreen>
|
||||
body: NestedScrollView(
|
||||
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
||||
return [
|
||||
SliverAppBar(
|
||||
flexibleSpace: SizedBox(
|
||||
height: 48,
|
||||
child: const Row(
|
||||
children: [
|
||||
RealmSwitcher(),
|
||||
SliverLayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final scrollOffset = constraints.scrollOffset;
|
||||
final colorChangeOffset = 120;
|
||||
|
||||
final scrollProgress =
|
||||
(scrollOffset / colorChangeOffset).clamp(0.0, 1.0);
|
||||
final blurSigma = lerpDouble(0, 10, scrollProgress) ?? 0;
|
||||
|
||||
return SliverAppBar(
|
||||
flexibleSpace: ClipRRect(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(
|
||||
sigmaX: blurSigma,
|
||||
sigmaY: blurSigma,
|
||||
),
|
||||
child: ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
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,
|
||||
floating: true,
|
||||
toolbarHeight: AppTheme.toolbarHeight(context),
|
||||
leading: AppBarLeadingButton.adaptive(context),
|
||||
actions: [
|
||||
const BackgroundStateWidget(),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.search),
|
||||
onPressed: () {
|
||||
AppRouter.instance.pushNamed('postSearch');
|
||||
},
|
||||
),
|
||||
const NotificationButton(),
|
||||
SizedBox(
|
||||
width: AppTheme.isLargeScreen(context) ? 8 : 16,
|
||||
),
|
||||
],
|
||||
).paddingSymmetric(horizontal: 8),
|
||||
).paddingOnly(top: MediaQuery.of(context).padding.top),
|
||||
floating: true,
|
||||
toolbarHeight: AppTheme.toolbarHeight(context),
|
||||
leading: AppBarLeadingButton.adaptive(context),
|
||||
actions: [
|
||||
const BackgroundStateWidget(),
|
||||
const NotificationButton(),
|
||||
SizedBox(
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
];
|
||||
},
|
||||
@ -156,7 +194,13 @@ class _ExploreScreenState extends State<ExploreScreen>
|
||||
RefreshIndicator(
|
||||
onRefresh: () => _postController.reloadAllOver(),
|
||||
child: CustomScrollView(slivers: [
|
||||
PostWarpedListWidget(
|
||||
ControlledPostListWidget(
|
||||
padding: AppTheme.isLargeScreen(context)
|
||||
? EdgeInsets.symmetric(
|
||||
horizontal: 4,
|
||||
vertical: 8,
|
||||
)
|
||||
: EdgeInsets.zero,
|
||||
controller: _postController.pagingController,
|
||||
onUpdate: () => _postController.reloadAllOver(),
|
||||
),
|
||||
@ -167,7 +211,10 @@ class _ExploreScreenState extends State<ExploreScreen>
|
||||
return RefreshIndicator(
|
||||
onRefresh: () => _postController.reloadAllOver(),
|
||||
child: CustomScrollView(slivers: [
|
||||
PostWarpedListWidget(
|
||||
ControlledPostListWidget(
|
||||
padding: AppTheme.isLargeScreen(context)
|
||||
? EdgeInsets.symmetric(horizontal: 16)
|
||||
: EdgeInsets.zero,
|
||||
controller: _postController.pagingController,
|
||||
onUpdate: () => _postController.reloadAllOver(),
|
||||
),
|
||||
@ -202,106 +249,3 @@ class _ExploreScreenState extends State<ExploreScreen>
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class PostCreatePopup extends StatelessWidget {
|
||||
final bool hideDraftBox;
|
||||
|
||||
const PostCreatePopup({
|
||||
super.key,
|
||||
this.hideDraftBox = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final AuthProvider auth = Get.find();
|
||||
|
||||
if (auth.isAuthorized.isFalse) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final List<dynamic> actionList = [
|
||||
(
|
||||
icon: const Icon(Icons.post_add),
|
||||
label: 'postEditorModeStory'.tr,
|
||||
onTap: () {
|
||||
Navigator.pop(
|
||||
context,
|
||||
AppRouter.instance.pushNamed(
|
||||
'postEditor',
|
||||
queryParameters: {
|
||||
'mode': 0.toString(),
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
(
|
||||
icon: const Icon(Icons.description),
|
||||
label: 'postEditorModeArticle'.tr,
|
||||
onTap: () {
|
||||
Navigator.pop(
|
||||
context,
|
||||
AppRouter.instance.pushNamed(
|
||||
'postEditor',
|
||||
queryParameters: {
|
||||
'mode': 1.toString(),
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
(
|
||||
icon: const Icon(Icons.drafts),
|
||||
label: 'draftBoxOpen'.tr,
|
||||
onTap: () {
|
||||
Navigator.pop(
|
||||
context,
|
||||
AppRouter.instance.pushNamed('draftBox'),
|
||||
);
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
return SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.38,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'postNew'.tr,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
|
||||
Expanded(
|
||||
child: GridView.count(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
crossAxisCount: 3,
|
||||
children: actionList
|
||||
.map((x) => Card(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: InkWell(
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(8)),
|
||||
onTap: x.onTap,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
x.icon,
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
x.label,
|
||||
overflow: TextOverflow.fade,
|
||||
),
|
||||
),
|
||||
],
|
||||
).paddingAll(18),
|
||||
),
|
||||
))
|
||||
.toList(),
|
||||
).paddingSymmetric(horizontal: 20),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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_warped_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: [
|
||||
PostWarpedListWidget(
|
||||
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/providers/content/posts.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_replies.dart';
|
||||
|
||||
@ -22,73 +25,109 @@ class PostDetailScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _PostDetailScreenState extends State<PostDetailScreen> {
|
||||
Post? item;
|
||||
bool _isBusy = true;
|
||||
|
||||
Future<Post?> getDetail() async {
|
||||
if (widget.post != null) {
|
||||
item = widget.post;
|
||||
Get.find<LastReadProvider>().feedLastReadAt = item?.id;
|
||||
return widget.post;
|
||||
}
|
||||
Post? _item;
|
||||
|
||||
final PostProvider provider = Get.find();
|
||||
Future<void> _getDetail() async {
|
||||
final PostProvider posts = Get.find();
|
||||
|
||||
try {
|
||||
final resp = await provider.getPost(widget.id);
|
||||
item = Post.fromJson(resp.body);
|
||||
final resp = await posts.getPost(widget.id);
|
||||
_item = Post.fromJson(resp.body);
|
||||
} catch (e) {
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder(
|
||||
future: getDetail(),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData || snapshot.data == null) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
if (_isBusy && _item == null) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: PostItem(
|
||||
item: item!,
|
||||
isClickable: false,
|
||||
isOverrideEmbedClickable: true,
|
||||
isFullDate: true,
|
||||
isFullContent: true,
|
||||
isShowReply: false,
|
||||
isContentSelectable: true,
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child:
|
||||
const Divider(thickness: 0.3, height: 1).paddingOnly(top: 4),
|
||||
),
|
||||
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!),
|
||||
SliverToBoxAdapter(
|
||||
child: SizedBox(height: MediaQuery.of(context).padding.bottom),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: LoadingIndicator(isActive: _isBusy),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: PostItem(
|
||||
key: ValueKey(_item),
|
||||
item: _item!,
|
||||
isClickable: false,
|
||||
isOverrideEmbedClickable: true,
|
||||
isFullDate: true,
|
||||
isShowReply: false,
|
||||
isContentSelectable: true,
|
||||
padding: AppTheme.isLargeScreen(context)
|
||||
? EdgeInsets.symmetric(
|
||||
horizontal: 4,
|
||||
vertical: 8,
|
||||
)
|
||||
: EdgeInsets.zero,
|
||||
onTapMore: () {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
context: context,
|
||||
builder: (context) => PostAction(
|
||||
item: _item!,
|
||||
noReact: true,
|
||||
),
|
||||
).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/widgets/app_bar_leading.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/posts/post_item.dart';
|
||||
import 'package:badges/badges.dart' as badges;
|
||||
@ -182,7 +183,10 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ListTile(
|
||||
tileColor: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
tileColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainerLow
|
||||
.withOpacity(0.5),
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -271,118 +275,71 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
Expanded(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListView(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
child: TextField(
|
||||
maxLines: null,
|
||||
autofocus: true,
|
||||
autocorrect: true,
|
||||
keyboardType: TextInputType.multiline,
|
||||
controller:
|
||||
_editorController.contentController,
|
||||
focusNode: _contentFocusNode,
|
||||
decoration: InputDecoration.collapsed(
|
||||
hintText: 'postContentPlaceholder'.tr,
|
||||
),
|
||||
onTapOutside: (_) => FocusManager
|
||||
.instance.primaryFocus
|
||||
?.unfocus(),
|
||||
),
|
||||
),
|
||||
const Gap(120)
|
||||
child: DefaultTabController(
|
||||
length: 2,
|
||||
child: AppTheme.isLargeScreen(context)
|
||||
? Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: _PostEditorTextField(
|
||||
focusNode: _contentFocusNode,
|
||||
controller: _editorController,
|
||||
onUpdate: () => setState(() {}),
|
||||
),
|
||||
),
|
||||
const VerticalDivider(width: 0.3, thickness: 0.3)
|
||||
.paddingSymmetric(horizontal: 16),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding:
|
||||
const EdgeInsets.only(top: 12, bottom: 64),
|
||||
child: MarkdownTextContent(
|
||||
isAutoWarp: _editorController.mode.value == 0,
|
||||
content:
|
||||
_editorController.contentController.text,
|
||||
parentId: 'post-editor-preview',
|
||||
).paddingOnly(right: 16),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Column(
|
||||
children: [
|
||||
TabBar(
|
||||
tabs: [
|
||||
const Tab(icon: Icon(Icons.edit)),
|
||||
const Tab(icon: Icon(Icons.preview)),
|
||||
],
|
||||
),
|
||||
),
|
||||
Obx(() {
|
||||
final textStyle = TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withOpacity(0.75),
|
||||
);
|
||||
final showFactors = [
|
||||
_editorController.isRestoreFromLocal.value,
|
||||
_editorController.lastSaveTime.value != null,
|
||||
];
|
||||
final doShow = showFactors.any((x) => x);
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 4,
|
||||
horizontal: 16,
|
||||
),
|
||||
child: Row(
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
children: [
|
||||
if (showFactors[0])
|
||||
Text('postRestoreFromLocal'.tr,
|
||||
style: textStyle)
|
||||
.paddingOnly(right: 4),
|
||||
if (showFactors[0])
|
||||
InkWell(
|
||||
child: Text('clear'.tr, style: textStyle),
|
||||
onTap: () {
|
||||
_editorController.localClear();
|
||||
_editorController.currentClear();
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
if (showFactors.where((x) => x).length > 1)
|
||||
Text(
|
||||
'·',
|
||||
style: textStyle,
|
||||
).paddingSymmetric(horizontal: 8),
|
||||
if (showFactors[1])
|
||||
Text(
|
||||
'postAutoSaveAt'.trParams({
|
||||
'date': DateFormat('HH:mm:ss').format(
|
||||
_editorController.lastSaveTime.value ??
|
||||
DateTime.now(),
|
||||
)
|
||||
}),
|
||||
style: textStyle,
|
||||
_PostEditorTextField(
|
||||
focusNode: _contentFocusNode,
|
||||
controller: _editorController,
|
||||
onUpdate: () => setState(() {}),
|
||||
),
|
||||
SingleChildScrollView(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 12,
|
||||
bottom: 64,
|
||||
),
|
||||
child: MarkdownTextContent(
|
||||
isAutoWarp:
|
||||
_editorController.mode.value == 0,
|
||||
content: _editorController
|
||||
.contentController.text,
|
||||
parentId: 'post-editor-preview',
|
||||
).paddingOnly(left: 16, right: 16),
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
.animate(
|
||||
key: const Key('post-editor-hint-animation'),
|
||||
target: doShow ? 1 : 0,
|
||||
)
|
||||
.fade(curve: Curves.easeInOut, duration: 300.ms);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (AppTheme.isLargeScreen(context))
|
||||
const VerticalDivider(width: 0.3, thickness: 0.3)
|
||||
.paddingSymmetric(
|
||||
horizontal: 16,
|
||||
),
|
||||
if (AppTheme.isLargeScreen(context))
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: MarkdownTextContent(
|
||||
isAutoWarp: _editorController.mode.value == 0,
|
||||
content: _editorController.contentController.text,
|
||||
parentId: 'post-editor-preview',
|
||||
).paddingOnly(top: 12, right: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Material(
|
||||
@ -391,6 +348,26 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Divider(thickness: 0.3, height: 0.3),
|
||||
SizedBox(
|
||||
height: 40,
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: MarkdownToolbar(
|
||||
width: 38,
|
||||
height: 38,
|
||||
iconSize: 20,
|
||||
spacing: 8,
|
||||
hideImage: true,
|
||||
useIncludedTextField: false,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
iconColor: Theme.of(context).colorScheme.onSurface,
|
||||
controller: _editorController.contentController,
|
||||
focusNode: _contentFocusNode,
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(20)),
|
||||
).paddingSymmetric(horizontal: 12),
|
||||
),
|
||||
).paddingOnly(top: 12),
|
||||
SizedBox(
|
||||
height: 56,
|
||||
child: ListView(
|
||||
@ -520,7 +497,7 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
|
||||
top: -4,
|
||||
end: -6,
|
||||
),
|
||||
child: const Icon(Icons.preview),
|
||||
child: const Icon(Icons.wallpaper),
|
||||
);
|
||||
}),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
@ -547,18 +524,6 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
|
||||
_editorController.editPublishDate(context);
|
||||
},
|
||||
),
|
||||
MarkdownToolbar(
|
||||
hideImage: true,
|
||||
useIncludedTextField: false,
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.surface,
|
||||
iconColor: Theme.of(context).colorScheme.onSurface,
|
||||
controller: _editorController.contentController,
|
||||
focusNode: _contentFocusNode,
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(20)),
|
||||
width: 40,
|
||||
).paddingSymmetric(horizontal: 2),
|
||||
],
|
||||
).paddingSymmetric(horizontal: 6, vertical: 8),
|
||||
),
|
||||
@ -578,3 +543,101 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class _PostEditorTextField extends StatelessWidget {
|
||||
final FocusNode focusNode;
|
||||
final PostEditorController controller;
|
||||
final Function onUpdate;
|
||||
|
||||
const _PostEditorTextField({
|
||||
required this.focusNode,
|
||||
required this.controller,
|
||||
required this.onUpdate,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListView(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
child: TextField(
|
||||
maxLines: null,
|
||||
autofocus: true,
|
||||
autocorrect: true,
|
||||
keyboardType: TextInputType.multiline,
|
||||
controller: controller.contentController,
|
||||
focusNode: focusNode,
|
||||
decoration: InputDecoration.collapsed(
|
||||
hintText: 'postContentPlaceholder'.tr,
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
),
|
||||
const Gap(120)
|
||||
],
|
||||
),
|
||||
),
|
||||
Obx(() {
|
||||
final textStyle = TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.75),
|
||||
);
|
||||
final showFactors = [
|
||||
controller.isRestoreFromLocal.value,
|
||||
controller.lastSaveTime.value != null,
|
||||
];
|
||||
final doShow = showFactors.any((x) => x);
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 4,
|
||||
horizontal: 16,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
if (showFactors[0])
|
||||
Text('postRestoreFromLocal'.tr, style: textStyle)
|
||||
.paddingOnly(right: 4),
|
||||
if (showFactors[0])
|
||||
InkWell(
|
||||
child: Text('clear'.tr, style: textStyle),
|
||||
onTap: () {
|
||||
controller.localClear();
|
||||
controller.currentClear();
|
||||
onUpdate();
|
||||
},
|
||||
),
|
||||
if (showFactors.where((x) => x).length > 1)
|
||||
Text(
|
||||
'·',
|
||||
style: textStyle,
|
||||
).paddingSymmetric(horizontal: 8),
|
||||
if (showFactors[1])
|
||||
Text(
|
||||
'postAutoSaveAt'.trParams({
|
||||
'date': DateFormat('HH:mm:ss').format(
|
||||
controller.lastSaveTime.value ?? DateTime.now(),
|
||||
)
|
||||
}),
|
||||
style: textStyle,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
.animate(
|
||||
key: const Key('post-editor-hint-animation'),
|
||||
target: doShow ? 1 : 0,
|
||||
)
|
||||
.fade(curve: Curves.easeInOut, duration: 300.ms);
|
||||
}),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
157
lib/screens/posts/post_search.dart
Normal file
157
lib/screens/posts/post_search.dart
Normal file
@ -0,0 +1,157 @@
|
||||
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> {
|
||||
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 provider = Get.find();
|
||||
|
||||
Response resp;
|
||||
try {
|
||||
if (_probeController.text.isEmpty) {
|
||||
resp = await provider.listPost(
|
||||
pageKey,
|
||||
tag: widget.tag,
|
||||
category: widget.category,
|
||||
);
|
||||
} else {
|
||||
resp = await provider.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);
|
||||
}
|
||||
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_pagingController.addPageRequestListener(_searchPosts);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_probeController.dispose();
|
||||
_pagingController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@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),
|
||||
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_animate/flutter_animate.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:get/get.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/auto_cache_image.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/sized_container.dart';
|
||||
|
||||
@ -93,7 +93,7 @@ class _RealmListScreenState extends State<RealmListScreen> {
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
Expanded(
|
||||
child: CenteredContainer(
|
||||
child: RefreshIndicator(
|
||||
@ -156,7 +156,7 @@ class _RealmListScreenState extends State<RealmListScreen> {
|
||||
size: 18,
|
||||
),
|
||||
)
|
||||
: AccountAvatar(
|
||||
: AttachedCircleAvatar(
|
||||
content: element.avatar!,
|
||||
bgColor: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
|
@ -1,5 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:image_cropper/image_cropper.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/widgets/app_bar_leading.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:uuid/uuid.dart';
|
||||
|
||||
@ -208,7 +208,7 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
|
||||
top: false,
|
||||
child: Column(
|
||||
children: [
|
||||
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
if (widget.edit != null)
|
||||
MaterialBanner(
|
||||
leading: const Icon(Icons.edit),
|
||||
|
@ -49,6 +49,7 @@ class RootShell extends StatelessWidget {
|
||||
|
||||
return Scaffold(
|
||||
key: rootScaffoldKey,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
bottomNavigationBar: showBottomNavigation
|
||||
? AppNavigationBottom(
|
||||
initialIndex: destNames.indexOf(routeName ?? 'page'),
|
||||
|
@ -1,15 +1,16 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:solian/services.dart';
|
||||
import 'package:solian/widgets/account/account_profile_popup.dart';
|
||||
import 'package:solian/widgets/auto_cache_image.dart';
|
||||
|
||||
class AccountAvatar extends StatelessWidget {
|
||||
class AttachedCircleAvatar extends StatelessWidget {
|
||||
final dynamic content;
|
||||
final Color? bgColor;
|
||||
final Color? feColor;
|
||||
final double? radius;
|
||||
final Widget? fallbackWidget;
|
||||
|
||||
const AccountAvatar({
|
||||
const AttachedCircleAvatar({
|
||||
super.key,
|
||||
required this.content,
|
||||
this.bgColor,
|
||||
@ -39,7 +40,7 @@ class AccountAvatar extends StatelessWidget {
|
||||
child: isEmpty
|
||||
? (fallbackWidget ??
|
||||
Icon(
|
||||
Icons.account_circle,
|
||||
Icons.image,
|
||||
size: radius != null ? radius! * 1.2 : 24,
|
||||
color: feColor,
|
||||
))
|
||||
@ -48,6 +49,54 @@ class AccountAvatar extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class AccountAvatar extends StatelessWidget {
|
||||
final dynamic content;
|
||||
final String username;
|
||||
final Color? bgColor;
|
||||
final Color? feColor;
|
||||
final double? radius;
|
||||
final Widget? fallbackWidget;
|
||||
|
||||
const AccountAvatar({
|
||||
super.key,
|
||||
required this.content,
|
||||
required this.username,
|
||||
this.bgColor,
|
||||
this.feColor,
|
||||
this.radius,
|
||||
this.fallbackWidget,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
child: AttachedCircleAvatar(
|
||||
content: content,
|
||||
bgColor: bgColor,
|
||||
feColor: feColor,
|
||||
radius: radius,
|
||||
fallbackWidget: (fallbackWidget ??
|
||||
Icon(
|
||||
Icons.account_circle,
|
||||
size: radius != null ? radius! * 1.2 : 24,
|
||||
color: feColor,
|
||||
)),
|
||||
),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
context: context,
|
||||
builder: (context) => AccountProfilePopup(
|
||||
name: username,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AccountProfileImage extends StatelessWidget {
|
||||
final dynamic content;
|
||||
final BoxFit fit;
|
||||
|
@ -84,7 +84,7 @@ class AccountHeadingWidget extends StatelessWidget {
|
||||
Positioned(
|
||||
bottom: -30,
|
||||
left: 32,
|
||||
child: AccountAvatar(content: avatar, radius: 40),
|
||||
child: AttachedCircleAvatar(content: avatar, radius: 40),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -89,8 +89,7 @@ class _AccountProfilePopupState extends State<AccountProfilePopup> {
|
||||
|
||||
return SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.75,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
child: ListView(
|
||||
children: [
|
||||
AccountHeadingWidget(
|
||||
avatar: _userinfo!.avatar,
|
||||
|
@ -138,7 +138,7 @@ class _AccountSelectorState extends State<AccountSelector> {
|
||||
return ListTile(
|
||||
title: Text(element.nick),
|
||||
subtitle: Text(element.name),
|
||||
leading: AccountAvatar(content: element.avatar),
|
||||
leading: AttachedCircleAvatar(content: element.avatar),
|
||||
trailing: widget.trailingBuilder != null
|
||||
? widget.trailingBuilder!(element)
|
||||
: _checkSelected(element)
|
||||
|
@ -23,7 +23,7 @@ class SilverRelativeList extends StatelessWidget {
|
||||
title: Text(element.related.nick),
|
||||
subtitle: Text(element.related.name),
|
||||
leading: GestureDetector(
|
||||
child: AccountAvatar(content: element.related.avatar),
|
||||
child: AttachedCircleAvatar(content: element.related.avatar),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
|
@ -56,7 +56,7 @@ class _RelativeSelectorState extends State<RelativeSelector> {
|
||||
return ListTile(
|
||||
title: Text(element.nick),
|
||||
subtitle: Text(element.name),
|
||||
leading: AccountAvatar(content: element.avatar),
|
||||
leading: AttachedCircleAvatar(content: element.avatar),
|
||||
trailing: widget.trailingBuilder != null
|
||||
? widget.trailingBuilder!(element)
|
||||
: null,
|
||||
|
@ -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_editor_thumbnail.dart';
|
||||
import 'package:solian/widgets/attachments/attachment_fullscreen.dart';
|
||||
import 'package:solian/widgets/loading_indicator.dart';
|
||||
|
||||
class AttachmentEditorPopup extends StatefulWidget {
|
||||
final String pool;
|
||||
@ -660,7 +661,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
|
||||
),
|
||||
],
|
||||
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
|
||||
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
Expanded(
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
|
@ -175,7 +175,7 @@ class _AttachmentFullScreenState extends State<AttachmentFullScreen> {
|
||||
Row(
|
||||
children: [
|
||||
IgnorePointer(
|
||||
child: AccountAvatar(
|
||||
child: AttachedCircleAvatar(
|
||||
content: widget.item.account!.avatar,
|
||||
radius: 19,
|
||||
),
|
||||
|
@ -155,11 +155,18 @@ class _AttachmentItemImage extends StatelessWidget {
|
||||
),
|
||||
if (showBadge && badge != null)
|
||||
Positioned(
|
||||
right: 12,
|
||||
bottom: 8,
|
||||
right: 8,
|
||||
bottom: 4,
|
||||
child: Material(
|
||||
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)
|
||||
|
@ -1,7 +1,6 @@
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:carousel_slider/carousel_slider.dart';
|
||||
import 'package:dismissible_page/dismissible_page.dart';
|
||||
import 'package:flutter/material.dart' hide CarouselController;
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
@ -15,27 +14,29 @@ import 'package:solian/widgets/sized_container.dart';
|
||||
|
||||
class AttachmentList extends StatefulWidget {
|
||||
final String parentId;
|
||||
final List<String> attachmentsId;
|
||||
final List<String>? attachmentIds;
|
||||
final List<Attachment>? attachments;
|
||||
final bool isGrid;
|
||||
final bool isColumn;
|
||||
final bool isForceGrid;
|
||||
final bool isFullWidth;
|
||||
final bool autoload;
|
||||
final double flatMaxHeight;
|
||||
final double columnMaxWidth;
|
||||
|
||||
final EdgeInsets? padding;
|
||||
final double? width;
|
||||
final double? viewport;
|
||||
|
||||
const AttachmentList({
|
||||
super.key,
|
||||
required this.parentId,
|
||||
required this.attachmentsId,
|
||||
this.attachmentIds,
|
||||
this.attachments,
|
||||
this.isGrid = false,
|
||||
this.isColumn = false,
|
||||
this.isForceGrid = false,
|
||||
this.isFullWidth = false,
|
||||
this.autoload = false,
|
||||
this.flatMaxHeight = 720,
|
||||
this.columnMaxWidth = 480,
|
||||
this.padding,
|
||||
this.width,
|
||||
this.viewport,
|
||||
});
|
||||
@ -48,23 +49,24 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
bool _isLoading = true;
|
||||
bool _showMature = false;
|
||||
|
||||
// ignore: unused_field
|
||||
double _aspectRatio = 1;
|
||||
|
||||
List<Attachment?> _attachmentsMeta = List.empty();
|
||||
List<Attachment?> _attachments = List.empty();
|
||||
|
||||
void _getMetadataList() {
|
||||
final AttachmentProvider attach = Get.find();
|
||||
|
||||
if (widget.attachmentsId.isEmpty) {
|
||||
if (widget.attachmentIds?.isEmpty ?? false) {
|
||||
return;
|
||||
} else {
|
||||
_attachmentsMeta = List.filled(widget.attachmentsId.length, null);
|
||||
_attachments = List.filled(widget.attachmentIds!.length, null);
|
||||
}
|
||||
|
||||
attach.listMetadata(widget.attachmentsId).then((result) {
|
||||
attach.listMetadata(widget.attachmentIds!).then((result) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_attachmentsMeta = result;
|
||||
_attachments = result;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
@ -76,7 +78,7 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
bool isConsistent = true;
|
||||
double? consistentValue;
|
||||
int portrait = 0, square = 0, landscape = 0;
|
||||
for (var entry in _attachmentsMeta) {
|
||||
for (var entry in _attachments) {
|
||||
if (entry == null) continue;
|
||||
if (entry.metadata?['ratio'] != null) {
|
||||
if (entry.metadata?['ratio'] is int) {
|
||||
@ -117,10 +119,9 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
item: element,
|
||||
parentId: widget.parentId,
|
||||
width: width ?? widget.width,
|
||||
badgeContent: '${idx + 1}/${_attachmentsMeta.length}',
|
||||
showBadge:
|
||||
_attachmentsMeta.length > 1 && !widget.isGrid && !widget.isColumn,
|
||||
showBorder: widget.attachmentsId.length > 1,
|
||||
badgeContent: '${idx + 1}/${_attachments.length}',
|
||||
showBadge: _attachments.length > 1 && !widget.isGrid && !widget.isColumn,
|
||||
showBorder: _attachments.length > 1,
|
||||
showMature: _showMature,
|
||||
autoload: widget.autoload,
|
||||
onReveal: (value) {
|
||||
@ -132,7 +133,26 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_getMetadataList();
|
||||
assert(widget.attachmentIds != null || widget.attachments != null);
|
||||
if (widget.attachments == null) {
|
||||
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 {
|
||||
setState(() {
|
||||
_attachments = widget.attachments!;
|
||||
_isLoading = false;
|
||||
});
|
||||
_calculateAspectRatio();
|
||||
}
|
||||
}
|
||||
|
||||
Color get _unFocusColor =>
|
||||
@ -140,7 +160,7 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.attachmentsId.isEmpty) {
|
||||
if (widget.attachmentIds?.isEmpty ?? widget.attachments!.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
@ -153,9 +173,7 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
color: _unFocusColor,
|
||||
).paddingOnly(right: 5),
|
||||
Text(
|
||||
'attachmentHint'.trParams(
|
||||
{'count': widget.attachmentsId.length.toString()},
|
||||
),
|
||||
'attachmentHint'.trParams({'count': _attachments.toString()}),
|
||||
style: TextStyle(color: _unFocusColor, fontSize: 12),
|
||||
)
|
||||
],
|
||||
@ -165,17 +183,89 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
.fadeIn(duration: 1250.ms);
|
||||
}
|
||||
|
||||
const radius = BorderRadius.all(Radius.circular(8));
|
||||
|
||||
if (widget.isFullWidth && _attachments.length == 1) {
|
||||
final element = _attachments.first;
|
||||
final isImage = element!.mimetype.split('/').firstOrNull == 'image';
|
||||
double ratio =
|
||||
element.metadata?['ratio']?.toDouble() ?? (isImage ? 1 : 16 / 9);
|
||||
return Container(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: 640,
|
||||
),
|
||||
child: AspectRatio(
|
||||
aspectRatio: ratio,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainer
|
||||
.withOpacity(0.5),
|
||||
border: Border.symmetric(
|
||||
horizontal: BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: _buildEntry(element, 0),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final isNotPureImage = _attachments.any(
|
||||
(x) => x?.mimetype.split('/').firstOrNull != 'image',
|
||||
);
|
||||
if (widget.isGrid && !isNotPureImage) {
|
||||
return GridView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
primary: false,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: math.min(3, _attachments.length),
|
||||
mainAxisSpacing: 8.0,
|
||||
crossAxisSpacing: 8.0,
|
||||
),
|
||||
itemCount: _attachments.length,
|
||||
itemBuilder: (context, idx) {
|
||||
final element = _attachments[idx];
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainer
|
||||
.withOpacity(0.5),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1,
|
||||
),
|
||||
borderRadius: radius,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: radius,
|
||||
child: _buildEntry(element, idx),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (widget.isColumn) {
|
||||
var idx = 0;
|
||||
const radius = BorderRadius.all(Radius.circular(8));
|
||||
return Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: widget.attachmentsId.map((x) {
|
||||
final element = _attachmentsMeta[idx];
|
||||
children: _attachments.map((x) {
|
||||
final element = _attachments[idx];
|
||||
idx++;
|
||||
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(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: widget.columnMaxWidth,
|
||||
@ -185,6 +275,10 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
aspectRatio: ratio,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainer
|
||||
.withOpacity(0.5),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1,
|
||||
@ -202,69 +296,52 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
);
|
||||
}
|
||||
|
||||
final isNotPureImage = _attachmentsMeta.any(
|
||||
(x) => x?.mimetype.split('/').firstOrNull != 'image',
|
||||
);
|
||||
if (widget.isGrid && (widget.isForceGrid || !isNotPureImage)) {
|
||||
const radius = BorderRadius.all(Radius.circular(8));
|
||||
return GridView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
primary: false,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
return Container(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: 320,
|
||||
),
|
||||
child: ListView.separated(
|
||||
padding: widget.padding,
|
||||
scrollDirection: Axis.horizontal,
|
||||
shrinkWrap: true,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: math.min(3, widget.attachmentsId.length),
|
||||
mainAxisSpacing: 8.0,
|
||||
crossAxisSpacing: 8.0,
|
||||
),
|
||||
itemCount: widget.attachmentsId.length,
|
||||
itemCount: _attachments.length,
|
||||
itemBuilder: (context, idx) {
|
||||
final element = _attachmentsMeta[idx];
|
||||
final element = _attachments[idx];
|
||||
if (element == null) const SizedBox.shrink();
|
||||
final isImage = element!.mimetype.split('/').firstOrNull == 'image';
|
||||
double ratio =
|
||||
element.metadata?['ratio']?.toDouble() ?? (isImage ? 1 : 16 / 9);
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
border: Border.all(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1,
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: math.min(
|
||||
widget.columnMaxWidth,
|
||||
MediaQuery.of(context).size.width -
|
||||
(widget.padding?.horizontal ?? 0),
|
||||
),
|
||||
borderRadius: radius,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: radius,
|
||||
child: _buildEntry(element, idx),
|
||||
child: AspectRatio(
|
||||
aspectRatio: ratio,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainer
|
||||
.withOpacity(0.5),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1,
|
||||
),
|
||||
borderRadius: radius,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: radius,
|
||||
child: _buildEntry(element, idx),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
).paddingSymmetric(horizontal: 24);
|
||||
}
|
||||
|
||||
return Container(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: widget.flatMaxHeight,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
border: Border.symmetric(
|
||||
horizontal: BorderSide(
|
||||
width: 0.3,
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: CarouselSlider.builder(
|
||||
options: CarouselOptions(
|
||||
animateToClosest: true,
|
||||
aspectRatio: _aspectRatio,
|
||||
viewportFraction:
|
||||
widget.viewport ?? (widget.attachmentsId.length > 1 ? 0.95 : 1),
|
||||
enableInfiniteScroll: false,
|
||||
),
|
||||
itemCount: _attachmentsMeta.length,
|
||||
itemBuilder: (context, idx, _) {
|
||||
final element = _attachmentsMeta[idx];
|
||||
return _buildEntry(element, idx);
|
||||
},
|
||||
separatorBuilder: (context, _) => const Gap(8),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -38,11 +38,13 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
|
||||
|
||||
Future<void> _loadLastMessages() async {
|
||||
final messages = await _eventController.src.getLastInAllChannels();
|
||||
setState(() {
|
||||
_lastMessages = messages
|
||||
.map((k, v) => MapEntry(k, v.firstOrNull))
|
||||
.cast<int, LocalMessageEventTableData>();
|
||||
});
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_lastMessages = messages
|
||||
.map((k, v) => MapEntry(k, v.firstOrNull))
|
||||
.cast<int, LocalMessageEventTableData>();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@ -205,7 +207,7 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
|
||||
item.members!.where((e) => e.account.id != widget.selfId).firstOrNull;
|
||||
|
||||
if (item.type == 1 && otherside != null) {
|
||||
final avatar = AccountAvatar(
|
||||
final avatar = AttachedCircleAvatar(
|
||||
content: otherside.account.avatar,
|
||||
radius: 20,
|
||||
bgColor: Theme.of(context).colorScheme.primary,
|
||||
@ -241,7 +243,7 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
|
||||
padding: const EdgeInsets.all(2),
|
||||
elevation: 8,
|
||||
),
|
||||
badgeContent: AccountAvatar(
|
||||
badgeContent: AttachedCircleAvatar(
|
||||
content: item.realm?.avatar,
|
||||
radius: 10,
|
||||
fallbackWidget: const Icon(
|
||||
|
@ -1,5 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/exts.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_profile_popup.dart';
|
||||
import 'package:solian/widgets/account/relative_select.dart';
|
||||
import 'package:solian/widgets/loading_indicator.dart';
|
||||
|
||||
class ChannelMemberListPopup extends StatefulWidget {
|
||||
final Channel channel;
|
||||
@ -131,7 +131,7 @@ class _ChannelMemberListPopupState extends State<ChannelMemberListPopup> {
|
||||
'channelMembers'.tr,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
|
||||
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
ListTile(
|
||||
tileColor: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
@ -152,7 +152,8 @@ class _ChannelMemberListPopupState extends State<ChannelMemberListPopup> {
|
||||
title: Text(element.account.nick),
|
||||
subtitle: Text(element.account.name),
|
||||
leading: GestureDetector(
|
||||
child: AccountAvatar(content: element.account.avatar),
|
||||
child:
|
||||
AttachedCircleAvatar(content: element.account.avatar),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
|
@ -74,7 +74,7 @@ class _NoContentWidgetState extends State<NoContentWidget>
|
||||
),
|
||||
)
|
||||
],
|
||||
child: AccountAvatar(
|
||||
child: AttachedCircleAvatar(
|
||||
content: widget.userinfo!.avatar,
|
||||
bgColor: Colors.transparent,
|
||||
radius: radius,
|
||||
|
@ -78,7 +78,7 @@ class ChatEvent extends StatelessWidget {
|
||||
child: AttachmentList(
|
||||
key: Key('m${item.uuid}attachments'),
|
||||
parentId: item.uuid,
|
||||
attachmentsId: attachments,
|
||||
attachmentIds: attachments,
|
||||
isColumn: true,
|
||||
),
|
||||
);
|
||||
@ -220,7 +220,7 @@ class ChatEvent extends StatelessWidget {
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
AccountAvatar(
|
||||
AttachedCircleAvatar(
|
||||
content: item.sender.account.avatar,
|
||||
radius: 9,
|
||||
),
|
||||
@ -250,7 +250,8 @@ class ChatEvent extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
GestureDetector(
|
||||
child: AccountAvatar(content: item.sender.account.avatar),
|
||||
child:
|
||||
AttachedCircleAvatar(content: item.sender.account.avatar),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
|
@ -1,5 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:get/get.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/providers/auth.dart';
|
||||
import 'package:solian/widgets/chat/chat_event_deletion.dart';
|
||||
import 'package:solian/widgets/loading_indicator.dart';
|
||||
|
||||
class ChatEventAction extends StatefulWidget {
|
||||
final Channel channel;
|
||||
@ -73,7 +73,7 @@ class _ChatEventActionState extends State<ChatEventAction> {
|
||||
),
|
||||
],
|
||||
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
|
||||
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
children: [
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:get/get.dart';
|
||||
@ -34,9 +35,28 @@ class ChatEventList extends StatelessWidget {
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return CustomScrollView(
|
||||
cacheExtent: 100,
|
||||
reverse: true,
|
||||
slivers: [
|
||||
Obx(() {
|
||||
@ -64,50 +84,45 @@ class ChatEventList extends StatelessWidget {
|
||||
|
||||
final item = chatController.currentEvents[index].data;
|
||||
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Builder(builder: (context) {
|
||||
final widget = ChatEvent(
|
||||
key: Key('m${item!.uuid}'),
|
||||
item: item,
|
||||
isMerged: isMerged,
|
||||
chatController: chatController,
|
||||
).paddingOnly(
|
||||
top: !isMerged ? 8 : 0,
|
||||
bottom: !hasMerged ? 8 : 0,
|
||||
);
|
||||
return TapRegion(
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Builder(builder: (context) {
|
||||
final widget = ChatEvent(
|
||||
key: Key('m${item!.uuid}'),
|
||||
item: item,
|
||||
isMerged: isMerged,
|
||||
chatController: chatController,
|
||||
).paddingOnly(
|
||||
top: !isMerged ? 8 : 0,
|
||||
bottom: !hasMerged ? 8 : 0,
|
||||
);
|
||||
|
||||
if (noAnimated) {
|
||||
return widget;
|
||||
} else {
|
||||
return widget
|
||||
.animate(
|
||||
key: Key('animated-m${item.uuid}'),
|
||||
)
|
||||
.slideY(
|
||||
curve: Curves.fastLinearToSlowEaseIn,
|
||||
duration: 250.ms,
|
||||
begin: 0.5,
|
||||
end: 0,
|
||||
);
|
||||
if (noAnimated) {
|
||||
return widget;
|
||||
} else {
|
||||
return widget
|
||||
.animate(
|
||||
key: Key('animated-m${item.uuid}'),
|
||||
)
|
||||
.slideY(
|
||||
curve: Curves.fastLinearToSlowEaseIn,
|
||||
duration: 250.ms,
|
||||
begin: 0.5,
|
||||
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);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
|
@ -443,7 +443,7 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
|
||||
.map(
|
||||
(x) => ChatMessageSuggestion(
|
||||
type: 'users',
|
||||
leading: AccountAvatar(content: x.avatar),
|
||||
leading: AttachedCircleAvatar(content: x.avatar),
|
||||
display: x.nick,
|
||||
content: '@${x.name}',
|
||||
),
|
||||
|
@ -2,15 +2,21 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/models/link.dart';
|
||||
import 'package:solian/providers/link_expander.dart';
|
||||
import 'package:solian/widgets/auto_cache_image.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class LinkExpansion extends StatelessWidget {
|
||||
class LinkExpansion extends StatefulWidget {
|
||||
final String 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}) {
|
||||
if (url.endsWith('svg')) {
|
||||
return SvgPicture.network(url, width: width, height: height);
|
||||
@ -22,61 +28,74 @@ class LinkExpansion extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<LinkMeta>? _meta;
|
||||
|
||||
Future<void> _doExpand() async {
|
||||
final linkRegex = RegExp(
|
||||
r'(?<!\()(?:(?:https?):\/\/|www\.)(?:[-_a-z0-9]+\.)*(?:[-a-z0-9]+\.[-a-z0-9]+)[^\s<]*[^\s<?!.,:*_~]',
|
||||
);
|
||||
final matches = linkRegex.allMatches(content);
|
||||
if (matches.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final matches = linkRegex.allMatches(widget.content);
|
||||
if (matches.isEmpty) return;
|
||||
|
||||
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(
|
||||
children: matches.map((x) {
|
||||
children: _meta!.map((x) {
|
||||
return Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: matches.length == 1 ? 480 : 340,
|
||||
maxWidth: _meta!.length == 1 ? 480 : 340,
|
||||
),
|
||||
child: FutureBuilder(
|
||||
future: expandController.expandLink(x.group(0)!),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final isRichDescription = [
|
||||
'solsynth.dev',
|
||||
].contains(Uri.parse(snapshot.data!.url).host);
|
||||
].contains(Uri.parse(x.url).host);
|
||||
|
||||
return GestureDetector(
|
||||
child: Card(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if ([
|
||||
(snapshot.data!.icon?.isNotEmpty ?? false),
|
||||
snapshot.data!.siteName != null
|
||||
].any((x) => x))
|
||||
if ([(x.icon?.isNotEmpty ?? false), x.siteName != null]
|
||||
.any((x) => x))
|
||||
Row(
|
||||
children: [
|
||||
if (snapshot.data!.icon?.isNotEmpty ?? false)
|
||||
if (x.icon?.isNotEmpty ?? false)
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(8),
|
||||
),
|
||||
child: _buildImage(
|
||||
snapshot.data!.icon!,
|
||||
x.icon!,
|
||||
width: 32,
|
||||
height: 32,
|
||||
),
|
||||
).paddingOnly(right: 8),
|
||||
if (snapshot.data!.siteName != null)
|
||||
if (x.siteName != null)
|
||||
Expanded(
|
||||
child: Text(
|
||||
snapshot.data!.siteName!,
|
||||
x.siteName!,
|
||||
style: Theme.of(context).textTheme.labelLarge,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
@ -84,32 +103,27 @@ class LinkExpansion extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
).paddingOnly(
|
||||
bottom: (snapshot.data!.icon?.isNotEmpty ?? false)
|
||||
? 8
|
||||
: 4,
|
||||
bottom: (x.icon?.isNotEmpty ?? false) ? 8 : 4,
|
||||
),
|
||||
if (snapshot.data!.image != null &&
|
||||
(snapshot.data!.image?.startsWith('http') ?? false))
|
||||
if (x.image != null &&
|
||||
(x.image?.startsWith('http') ?? false))
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(8),
|
||||
),
|
||||
child: _buildImage(
|
||||
snapshot.data!.image!,
|
||||
),
|
||||
child: _buildImage(x.image!),
|
||||
).paddingOnly(bottom: 8),
|
||||
Text(
|
||||
snapshot.data!.title ?? 'No Title',
|
||||
x.title ?? 'No Title',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.fade,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
if (snapshot.data!.description != null &&
|
||||
isRichDescription)
|
||||
MarkdownBody(data: snapshot.data!.description!)
|
||||
else if (snapshot.data!.description != null)
|
||||
if (x.description != null && isRichDescription)
|
||||
MarkdownBody(data: x.description!)
|
||||
else if (x.description != null)
|
||||
Text(
|
||||
snapshot.data!.description!,
|
||||
x.description!,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
@ -117,7 +131,7 @@ class LinkExpansion extends StatelessWidget {
|
||||
).paddingAll(12),
|
||||
),
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -69,7 +69,7 @@ class _AppAccountWidgetState extends State<AppAccountWidget> {
|
||||
bottom: 0,
|
||||
end: -2,
|
||||
),
|
||||
child: AccountAvatar(
|
||||
child: AttachedCircleAvatar(
|
||||
radius: 14,
|
||||
content: auth.userProfile.value!['avatar'],
|
||||
),
|
||||
|
@ -36,7 +36,7 @@ class RealmSwitcher extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (item != null)
|
||||
AccountAvatar(
|
||||
AttachedCircleAvatar(
|
||||
content: item.avatar,
|
||||
radius: 14,
|
||||
fallbackWidget: const Icon(
|
||||
|
@ -1,17 +1,21 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:file_saver/file_saver.dart';
|
||||
import 'package:firebase_analytics/firebase_analytics.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:screenshot/screenshot.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:solian/exts.dart';
|
||||
import 'package:solian/models/post.dart';
|
||||
import 'package:solian/platform.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/providers/content/posts.dart';
|
||||
import 'package:solian/router.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';
|
||||
|
||||
class PostAction extends StatefulWidget {
|
||||
@ -25,20 +29,14 @@ class PostAction extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _PostActionState extends State<PostAction> {
|
||||
bool _isBusy = true;
|
||||
bool _isBusy = false;
|
||||
bool _canModifyContent = false;
|
||||
|
||||
void _checkAbleToModifyContent() async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) return;
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
setState(() {
|
||||
_canModifyContent =
|
||||
auth.userProfile.value!['id'] == widget.item.author.id;
|
||||
_isBusy = false;
|
||||
});
|
||||
_canModifyContent = auth.userProfile.value!['id'] == widget.item.author.id;
|
||||
}
|
||||
|
||||
Future<void> _doShare({bool noUri = false}) async {
|
||||
@ -69,7 +67,8 @@ class _PostActionState extends State<PostAction> {
|
||||
'link': 'https://solsynth.dev/posts/$id',
|
||||
}),
|
||||
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,
|
||||
);
|
||||
@ -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
|
||||
void initState() {
|
||||
super.initState();
|
||||
@ -127,7 +198,13 @@ class _PostActionState extends State<PostAction> {
|
||||
),
|
||||
],
|
||||
).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(
|
||||
child: ListView(
|
||||
children: [
|
||||
@ -135,16 +212,30 @@ class _PostActionState extends State<PostAction> {
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Icons.share),
|
||||
title: Text('share'.tr),
|
||||
trailing: PlatformInfo.isIOS || PlatformInfo.isAndroid
|
||||
? IconButton(
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (PlatformInfo.isIOS || PlatformInfo.isAndroid)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.link_off),
|
||||
tooltip: 'shareNoUri'.tr,
|
||||
onPressed: () async {
|
||||
await _doShare(noUri: true);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
)
|
||||
: null,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.image),
|
||||
tooltip: 'shareImage'.tr,
|
||||
onPressed: _isBusy
|
||||
? null
|
||||
: () async {
|
||||
await _shareImage();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () async {
|
||||
await _doShare();
|
||||
Navigator.pop(context);
|
||||
@ -221,15 +312,23 @@ class _PostActionState extends State<PostAction> {
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Icons.edit),
|
||||
title: Text('edit'.tr),
|
||||
onTap: () async {
|
||||
Navigator.pop(
|
||||
context,
|
||||
AppRouter.instance.pushNamed(
|
||||
'postEditor',
|
||||
extra: PostPublishArguments(edit: widget.item),
|
||||
),
|
||||
);
|
||||
},
|
||||
onTap: _isBusy
|
||||
? null
|
||||
: () async {
|
||||
setState(() => _isBusy = true);
|
||||
var item = 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)
|
||||
ListTile(
|
||||
|
108
lib/widgets/posts/post_creation.dart
Normal file
108
lib/widgets/posts/post_creation.dart
Normal file
@ -0,0 +1,108 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/router.dart';
|
||||
|
||||
class PostCreatePopup extends StatelessWidget {
|
||||
final bool hideDraftBox;
|
||||
|
||||
const PostCreatePopup({
|
||||
super.key,
|
||||
this.hideDraftBox = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final AuthProvider auth = Get.find();
|
||||
|
||||
if (auth.isAuthorized.isFalse) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final List<dynamic> actionList = [
|
||||
(
|
||||
icon: const Icon(Icons.post_add),
|
||||
label: 'postEditorModeStory'.tr,
|
||||
onTap: () {
|
||||
Navigator.pop(
|
||||
context,
|
||||
AppRouter.instance.pushNamed(
|
||||
'postEditor',
|
||||
queryParameters: {
|
||||
'mode': 0.toString(),
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
(
|
||||
icon: const Icon(Icons.description),
|
||||
label: 'postEditorModeArticle'.tr,
|
||||
onTap: () {
|
||||
Navigator.pop(
|
||||
context,
|
||||
AppRouter.instance.pushNamed(
|
||||
'postEditor',
|
||||
queryParameters: {
|
||||
'mode': 1.toString(),
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
(
|
||||
icon: const Icon(Icons.drafts),
|
||||
label: 'draftBoxOpen'.tr,
|
||||
onTap: () {
|
||||
Navigator.pop(
|
||||
context,
|
||||
AppRouter.instance.pushNamed('draftBox'),
|
||||
);
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
return SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.38,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'postNew'.tr,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
|
||||
Expanded(
|
||||
child: GridView.count(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
crossAxisCount: 3,
|
||||
children: actionList
|
||||
.map((x) => Card(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: InkWell(
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(8)),
|
||||
onTap: x.onTap,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
x.icon,
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
x.label,
|
||||
overflow: TextOverflow.fade,
|
||||
),
|
||||
),
|
||||
],
|
||||
).paddingAll(18),
|
||||
),
|
||||
))
|
||||
.toList(),
|
||||
).paddingSymmetric(horizontal: 20),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
import 'package:animations/animations.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
@ -8,11 +7,11 @@ import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:solian/models/post.dart';
|
||||
import 'package:solian/providers/content/posts.dart';
|
||||
import 'package:solian/router.dart';
|
||||
import 'package:solian/screens/posts/post_detail.dart';
|
||||
import 'package:solian/shells/title_shell.dart';
|
||||
import 'package:solian/theme.dart';
|
||||
import 'package:solian/widgets/account/account_avatar.dart';
|
||||
import 'package:solian/widgets/account/account_profile_popup.dart';
|
||||
import 'package:solian/widgets/attachments/attachment_list.dart';
|
||||
import 'package:solian/widgets/link_expansion.dart';
|
||||
import 'package:solian/widgets/markdown_text_content.dart';
|
||||
@ -31,11 +30,15 @@ class PostItem extends StatefulWidget {
|
||||
final bool isShowEmbed;
|
||||
final bool isOverrideEmbedClickable;
|
||||
final bool isFullDate;
|
||||
final bool isFullContent;
|
||||
final bool isContentSelectable;
|
||||
final bool isNonScrollAttachment;
|
||||
final bool showFeaturedReply;
|
||||
final String? attachmentParent;
|
||||
final Color? backgroundColor;
|
||||
|
||||
final EdgeInsets? padding;
|
||||
|
||||
final Function? onComment;
|
||||
final Function? onTapMore;
|
||||
|
||||
const PostItem({
|
||||
super.key,
|
||||
@ -47,11 +50,13 @@ class PostItem extends StatefulWidget {
|
||||
this.isShowEmbed = true,
|
||||
this.isOverrideEmbedClickable = false,
|
||||
this.isFullDate = false,
|
||||
this.isFullContent = false,
|
||||
this.isContentSelectable = false,
|
||||
this.isNonScrollAttachment = false,
|
||||
this.showFeaturedReply = false,
|
||||
this.attachmentParent,
|
||||
this.backgroundColor,
|
||||
this.padding,
|
||||
this.onComment,
|
||||
this.onTapMore,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -64,14 +69,20 @@ class _PostItemState extends State<PostItem> {
|
||||
Color get _unFocusColor =>
|
||||
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
||||
|
||||
static final visibilityIcons = [
|
||||
Icons.public,
|
||||
Icons.group,
|
||||
Icons.visibility,
|
||||
Icons.visibility_off,
|
||||
Icons.lock,
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
item = widget.item;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
double _contentHeight = 0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final List<String> attachments = item.body['attachments'] is List
|
||||
@ -89,37 +100,25 @@ class _PostItemState extends State<PostItem> {
|
||||
).paddingOnly(bottom: 8),
|
||||
_PostHeaderWidget(
|
||||
isCompact: widget.isCompact,
|
||||
isFullDate: widget.isFullDate,
|
||||
onTapMore: widget.onTapMore,
|
||||
item: item,
|
||||
).paddingSymmetric(horizontal: 12),
|
||||
_PostHeaderDividerWidget(item: item).paddingSymmetric(horizontal: 12),
|
||||
Stack(
|
||||
children: [
|
||||
SizedContainer(
|
||||
maxWidth: 640,
|
||||
maxHeight: widget.isFullContent ? double.infinity : 80,
|
||||
child: _MeasureSize(
|
||||
onChange: (size) {
|
||||
setState(() => _contentHeight = size.height);
|
||||
},
|
||||
child: SingleChildScrollView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
child: MarkdownTextContent(
|
||||
parentId: 'p${item.id}',
|
||||
content: item.body['content'],
|
||||
isAutoWarp: item.type == 'story',
|
||||
isSelectable: widget.isContentSelectable,
|
||||
),
|
||||
).paddingOnly(
|
||||
left: 16,
|
||||
right: 12,
|
||||
top: 2,
|
||||
bottom: hasAttachment ? 4 : 0,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
SizedContainer(
|
||||
maxWidth: 640,
|
||||
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,
|
||||
),
|
||||
if (_contentHeight >= 80 && !widget.isFullContent)
|
||||
if (widget.item.body?['content_truncated'] == true)
|
||||
Opacity(
|
||||
opacity: 0.8,
|
||||
child: InkWell(child: Text('readMore'.tr)),
|
||||
@ -130,9 +129,7 @@ class _PostItemState extends State<PostItem> {
|
||||
LinkExpansion(content: item.body['content']).paddingOnly(
|
||||
left: 8,
|
||||
right: 8,
|
||||
top: 4,
|
||||
),
|
||||
_PostFooterWidget(item: item).paddingOnly(left: 16),
|
||||
if (attachments.isNotEmpty)
|
||||
Row(
|
||||
children: [
|
||||
@ -148,127 +145,87 @@ class _PostItemState extends State<PostItem> {
|
||||
style: TextStyle(color: _unFocusColor),
|
||||
)
|
||||
],
|
||||
).paddingOnly(left: 16, top: 4),
|
||||
).paddingOnly(left: 14, top: 4),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return OpenContainer(
|
||||
tappable: widget.isClickable,
|
||||
closedBuilder: (_, openContainer) => Column(
|
||||
return GestureDetector(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_PostThumbnail(
|
||||
rid: item.body['thumbnail'],
|
||||
parentId: widget.item.id.toString(),
|
||||
).paddingOnly(bottom: 4),
|
||||
Row(
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
GestureDetector(
|
||||
child: AccountAvatar(content: item.author.avatar),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
context: context,
|
||||
builder: (context) => AccountProfilePopup(
|
||||
name: item.author.name,
|
||||
),
|
||||
);
|
||||
},
|
||||
_PostHeaderWidget(
|
||||
isCompact: widget.isCompact,
|
||||
isFullDate: widget.isFullDate,
|
||||
onTapMore: widget.onTapMore,
|
||||
item: item,
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_PostHeaderWidget(
|
||||
isCompact: widget.isCompact,
|
||||
item: item,
|
||||
),
|
||||
_PostHeaderDividerWidget(item: item),
|
||||
Stack(
|
||||
children: [
|
||||
SizedContainer(
|
||||
maxWidth: 640,
|
||||
maxHeight:
|
||||
widget.isFullContent ? double.infinity : 320,
|
||||
child: _MeasureSize(
|
||||
onChange: (size) {
|
||||
setState(() => _contentHeight = size.height);
|
||||
},
|
||||
child: SingleChildScrollView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
child: MarkdownTextContent(
|
||||
parentId: 'p${item.id}-embed',
|
||||
content: item.body['content'],
|
||||
isAutoWarp: item.type == 'story',
|
||||
isSelectable: widget.isContentSelectable,
|
||||
isLargeText: item.type == 'article' &&
|
||||
widget.isFullContent,
|
||||
).paddingOnly(left: 12, right: 8),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_contentHeight >= 320 && !widget.isFullContent)
|
||||
Opacity(
|
||||
opacity: 0.8,
|
||||
child: InkWell(child: Text('readMore'.tr)),
|
||||
).paddingOnly(
|
||||
left: 12,
|
||||
top: 4,
|
||||
),
|
||||
if (widget.item.replyTo != null && widget.isShowEmbed)
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxWidth: 480),
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: _PostEmbedWidget(
|
||||
isClickable: widget.isClickable,
|
||||
isOverrideEmbedClickable:
|
||||
widget.isOverrideEmbedClickable,
|
||||
item: widget.item.replyTo!,
|
||||
username: widget.item.replyTo!.author.name,
|
||||
hintText: 'postRepliedNotify',
|
||||
icon: FontAwesomeIcons.reply,
|
||||
id: widget.item.replyTo!.id.toString(),
|
||||
),
|
||||
),
|
||||
if (widget.item.repostTo != null && widget.isShowEmbed)
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxWidth: 480),
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: _PostEmbedWidget(
|
||||
isClickable: widget.isClickable,
|
||||
isOverrideEmbedClickable:
|
||||
widget.isOverrideEmbedClickable,
|
||||
item: widget.item.repostTo!,
|
||||
username: widget.item.repostTo!.author.name,
|
||||
hintText: 'postRepostedNotify',
|
||||
icon: FontAwesomeIcons.retweet,
|
||||
id: widget.item.repostTo!.id.toString(),
|
||||
),
|
||||
),
|
||||
_PostFooterWidget(item: item).paddingOnly(left: 12),
|
||||
LinkExpansion(content: item.body['content'])
|
||||
.paddingOnly(top: 4),
|
||||
],
|
||||
_PostHeaderDividerWidget(item: item),
|
||||
SizedContainer(
|
||||
maxWidth: 640,
|
||||
child: MarkdownTextContent(
|
||||
parentId: 'p${item.id}-embed',
|
||||
content: item.body['content'],
|
||||
isAutoWarp: item.type == 'story',
|
||||
isSelectable: widget.isContentSelectable,
|
||||
),
|
||||
),
|
||||
if (widget.item.body?['content_truncated'] == true)
|
||||
Opacity(
|
||||
opacity: 0.8,
|
||||
child: InkWell(child: Text('readMore'.tr)),
|
||||
).paddingOnly(top: 4),
|
||||
if (widget.item.replyTo != null && widget.isShowEmbed)
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxWidth: 480),
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: _PostEmbedWidget(
|
||||
isClickable: widget.isClickable,
|
||||
isOverrideEmbedClickable: widget.isOverrideEmbedClickable,
|
||||
item: widget.item.replyTo!,
|
||||
username: widget.item.replyTo!.author.name,
|
||||
hintText: 'postRepliedNotify',
|
||||
icon: FontAwesomeIcons.reply,
|
||||
id: widget.item.replyTo!.id.toString(),
|
||||
),
|
||||
),
|
||||
if (widget.item.repostTo != null && widget.isShowEmbed)
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxWidth: 480),
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: _PostEmbedWidget(
|
||||
isClickable: widget.isClickable,
|
||||
isOverrideEmbedClickable: widget.isOverrideEmbedClickable,
|
||||
item: widget.item.repostTo!,
|
||||
username: widget.item.repostTo!.author.name,
|
||||
hintText: 'postRepostedNotify',
|
||||
icon: FontAwesomeIcons.retweet,
|
||||
id: widget.item.repostTo!.id.toString(),
|
||||
),
|
||||
),
|
||||
_PostFooterWidget(item: item),
|
||||
LinkExpansion(content: item.body['content']),
|
||||
],
|
||||
).paddingOnly(
|
||||
top: 10,
|
||||
bottom:
|
||||
(attachments.length == 1 && !AppTheme.isLargeScreen(context))
|
||||
? 10
|
||||
: 0,
|
||||
right: 16,
|
||||
left: 16,
|
||||
).paddingSymmetric(
|
||||
horizontal: (widget.padding?.horizontal ?? 0) + 16,
|
||||
),
|
||||
_PostAttachmentWidget(item: item),
|
||||
if (widget.showFeaturedReply) _PostFeaturedReplyWidget(item: item),
|
||||
if (hasAttachment) const Gap(8),
|
||||
_PostAttachmentWidget(
|
||||
item: item,
|
||||
padding: widget.padding,
|
||||
isNonScrollAttachment: widget.isNonScrollAttachment,
|
||||
),
|
||||
if (widget.showFeaturedReply)
|
||||
_PostFeaturedReplyWidget(item: item).paddingSymmetric(
|
||||
horizontal: (widget.padding?.horizontal ?? 0) + 12,
|
||||
),
|
||||
if (widget.isShowReply || widget.isReactable)
|
||||
PostQuickAction(
|
||||
isShowReply: widget.isShowReply,
|
||||
@ -280,32 +237,30 @@ class _PostItemState extends State<PostItem> {
|
||||
(item.metric!.reactionList[symbol] ?? 0) + changes;
|
||||
});
|
||||
},
|
||||
onComment: () {
|
||||
if (widget.onComment != null) {
|
||||
widget.onComment!();
|
||||
}
|
||||
},
|
||||
).paddingOnly(
|
||||
top: (attachments.length == 1 && !AppTheme.isLargeScreen(context))
|
||||
? 10
|
||||
: 6,
|
||||
left:
|
||||
(attachments.length == 1 && !AppTheme.isLargeScreen(context))
|
||||
? 24
|
||||
: 60,
|
||||
right: 16,
|
||||
bottom: 10,
|
||||
top: 8,
|
||||
left: (widget.padding?.left ?? 0) + 14,
|
||||
right: (widget.padding?.right ?? 0) + 14,
|
||||
)
|
||||
else
|
||||
const Gap(10),
|
||||
],
|
||||
).paddingOnly(
|
||||
top: widget.padding?.top ?? 0,
|
||||
bottom: widget.padding?.bottom ?? 0,
|
||||
),
|
||||
openBuilder: (_, __) => TitleShell(
|
||||
title: 'postDetail'.tr,
|
||||
child: PostDetailScreen(
|
||||
id: item.id.toString(),
|
||||
post: item,
|
||||
),
|
||||
),
|
||||
closedElevation: 0,
|
||||
openElevation: 0,
|
||||
closedColor: Colors.transparent,
|
||||
openColor: Theme.of(context).colorScheme.surface,
|
||||
onTap: () {
|
||||
if (widget.isClickable) {
|
||||
AppRouter.instance.pushNamed(
|
||||
'postDetail',
|
||||
pathParameters: {'id': item.id.toString()},
|
||||
extra: item,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -317,7 +272,6 @@ class _PostFeaturedReplyWidget extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isLargeScreen = AppTheme.isLargeScreen(context);
|
||||
final unFocusColor =
|
||||
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
||||
|
||||
@ -325,19 +279,17 @@ class _PostFeaturedReplyWidget extends StatelessWidget {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final List<String> attachments = item.body['attachments'] is List
|
||||
? List.from(item.body['attachments']?.whereType<String>())
|
||||
: List.empty();
|
||||
|
||||
return FutureBuilder(
|
||||
future:
|
||||
Get.find<PostProvider>().listPostFeaturedReply(item.id.toString()),
|
||||
future: Get.find<PostProvider>().listPostFeaturedReply(
|
||||
item.id.toString(),
|
||||
),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData || snapshot.data!.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: EdgeInsets.only(top: 8),
|
||||
constraints: const BoxConstraints(maxWidth: 480),
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
@ -351,7 +303,7 @@ class _PostFeaturedReplyWidget extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AccountAvatar(
|
||||
AttachedCircleAvatar(
|
||||
content: reply.author.avatar,
|
||||
radius: 10,
|
||||
),
|
||||
@ -423,16 +375,9 @@ class _PostFeaturedReplyWidget extends StatelessWidget {
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
)
|
||||
.animate()
|
||||
.fadeIn(
|
||||
).animate().fadeIn(
|
||||
duration: 300.ms,
|
||||
curve: Curves.easeIn,
|
||||
)
|
||||
.paddingOnly(
|
||||
top: (attachments.length == 1 && !isLargeScreen) ? 10 : 6,
|
||||
left: (attachments.length == 1 && !isLargeScreen) ? 24 : 60,
|
||||
right: 16,
|
||||
);
|
||||
},
|
||||
);
|
||||
@ -441,8 +386,14 @@ class _PostFeaturedReplyWidget extends StatelessWidget {
|
||||
|
||||
class _PostAttachmentWidget extends StatelessWidget {
|
||||
final Post item;
|
||||
final EdgeInsets? padding;
|
||||
final bool isNonScrollAttachment;
|
||||
|
||||
const _PostAttachmentWidget({required this.item});
|
||||
const _PostAttachmentWidget({
|
||||
required this.item,
|
||||
required this.padding,
|
||||
required this.isNonScrollAttachment,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -452,25 +403,42 @@ class _PostAttachmentWidget extends StatelessWidget {
|
||||
? List.from(item.body['attachments']?.whereType<String>())
|
||||
: List.empty();
|
||||
|
||||
if (attachments.length > 3) {
|
||||
if (attachments.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
if (attachments.length == 1 && !isLargeScreen) {
|
||||
return AttachmentList(
|
||||
parentId: item.id.toString(),
|
||||
attachmentsId: attachments,
|
||||
attachmentIds: item.preload == null ? attachments : null,
|
||||
attachments: item.preload?.attachments,
|
||||
autoload: false,
|
||||
isFullWidth: true,
|
||||
);
|
||||
} else if (attachments.length > 1 &&
|
||||
attachments.length % 3 == 0 &&
|
||||
!isLargeScreen) {
|
||||
return AttachmentList(
|
||||
parentId: item.id.toString(),
|
||||
attachmentIds: item.preload == null ? attachments : null,
|
||||
attachments: item.preload?.attachments,
|
||||
autoload: false,
|
||||
isGrid: true,
|
||||
).paddingOnly(left: 36, top: 4, bottom: 4);
|
||||
} else if (attachments.length > 1 || isLargeScreen) {
|
||||
).paddingSymmetric(horizontal: (padding?.horizontal ?? 0) + 14);
|
||||
} else if (attachments.length == 1 || isNonScrollAttachment) {
|
||||
return AttachmentList(
|
||||
parentId: item.id.toString(),
|
||||
attachmentsId: attachments,
|
||||
attachmentIds: item.preload == null ? attachments : null,
|
||||
attachments: item.preload?.attachments,
|
||||
autoload: false,
|
||||
isColumn: true,
|
||||
).paddingOnly(left: 60, right: 24, top: 4, bottom: 4);
|
||||
).paddingSymmetric(horizontal: (padding?.horizontal ?? 0) + 14);
|
||||
} else {
|
||||
return AttachmentList(
|
||||
flatMaxHeight: MediaQuery.of(context).size.width,
|
||||
parentId: item.id.toString(),
|
||||
attachmentsId: attachments,
|
||||
attachmentIds: item.preload == null ? attachments : null,
|
||||
attachments: item.preload?.attachments,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: (padding?.horizontal ?? 0) + 14,
|
||||
),
|
||||
autoload: false,
|
||||
);
|
||||
}
|
||||
@ -512,16 +480,17 @@ class _PostEmbedWidget extends StatelessWidget {
|
||||
size: 16,
|
||||
color: unFocusColor,
|
||||
),
|
||||
const Gap(6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
hintText.trParams(
|
||||
{'username': '@$username'},
|
||||
),
|
||||
style: TextStyle(color: unFocusColor),
|
||||
).paddingOnly(left: 6),
|
||||
),
|
||||
),
|
||||
],
|
||||
).paddingOnly(left: 12),
|
||||
).paddingOnly(left: 2),
|
||||
Card(
|
||||
elevation: 1,
|
||||
child: PostItem(
|
||||
@ -557,9 +526,7 @@ class _PostHeaderDividerWidget extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (item.body['description'] != null || item.body['title'] != null) {
|
||||
return const Divider(thickness: 0.3, height: 1).paddingSymmetric(
|
||||
vertical: 8,
|
||||
);
|
||||
return const Gap(8);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
@ -615,64 +582,99 @@ class _PostFooterWidget extends StatelessWidget {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: widgets,
|
||||
).paddingOnly(top: 4);
|
||||
).paddingSymmetric(vertical: 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _PostHeaderWidget extends StatelessWidget {
|
||||
final bool isCompact;
|
||||
final bool isFullDate;
|
||||
final Post item;
|
||||
|
||||
final Function? onTapMore;
|
||||
|
||||
const _PostHeaderWidget({
|
||||
required this.isCompact,
|
||||
required this.isFullDate,
|
||||
required this.item,
|
||||
required this.onTapMore,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (isCompact)
|
||||
AccountAvatar(
|
||||
content: item.author.avatar,
|
||||
radius: 10,
|
||||
).paddingOnly(left: 2, top: 1),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AccountAvatar(
|
||||
content: item.author.avatar,
|
||||
username: item.author.name,
|
||||
radius: isCompact ? 10 : null,
|
||||
),
|
||||
Gap(isCompact ? 6 : 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
item.author.nick,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
item.author.nick,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
if (isCompact) const Gap(4),
|
||||
if (isCompact)
|
||||
RelativeDate(
|
||||
item.publishedAt?.toLocal() ?? DateTime.now(),
|
||||
isFull: isFullDate,
|
||||
).paddingOnly(top: 1),
|
||||
],
|
||||
),
|
||||
RelativeDate(item.publishedAt?.toLocal() ?? DateTime.now())
|
||||
.paddingOnly(left: 4),
|
||||
if (!isCompact)
|
||||
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.body['title'] != null)
|
||||
Text(
|
||||
item.body['title'],
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!
|
||||
.copyWith(fontSize: 15),
|
||||
),
|
||||
if (item.body['description'] != null)
|
||||
Text(
|
||||
item.body['description'],
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
).paddingOnly(left: isCompact ? 6 : 12),
|
||||
),
|
||||
if (onTapMore != null)
|
||||
IconButton(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
icon: const Icon(Icons.more_vert),
|
||||
onPressed: () => onTapMore!(),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (item.type == 'article')
|
||||
Badge(
|
||||
label: Text('article'.tr),
|
||||
).paddingOnly(top: 3),
|
||||
const Gap(8),
|
||||
if (item.body['title'] != null)
|
||||
Text(
|
||||
item.body['title'],
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
if (item.body['description'] != null)
|
||||
Text(
|
||||
item.body['description'],
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@ -703,45 +705,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,8 +1,11 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
import 'package:solian/models/post.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/router.dart';
|
||||
import 'package:solian/screens/posts/post_editor.dart';
|
||||
import 'package:solian/widgets/posts/post_action.dart';
|
||||
import 'package:solian/widgets/posts/post_item.dart';
|
||||
|
||||
@ -12,6 +15,7 @@ class PostListWidget extends StatelessWidget {
|
||||
final bool isNestedClickable;
|
||||
final PagingController<int, Post> controller;
|
||||
final Color? backgroundColor;
|
||||
final EdgeInsets? padding;
|
||||
|
||||
const PostListWidget({
|
||||
super.key,
|
||||
@ -20,6 +24,7 @@ class PostListWidget extends StatelessWidget {
|
||||
this.isClickable = true,
|
||||
this.isNestedClickable = true,
|
||||
this.backgroundColor,
|
||||
this.padding,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -29,16 +34,18 @@ class PostListWidget extends StatelessWidget {
|
||||
pagingController: controller,
|
||||
builderDelegate: PagedChildBuilderDelegate<Post>(
|
||||
itemBuilder: (context, item, index) {
|
||||
return PostListEntryWidget(
|
||||
isShowEmbed: isShowEmbed,
|
||||
isNestedClickable: isNestedClickable,
|
||||
isClickable: isClickable,
|
||||
showFeaturedReply: true,
|
||||
item: item,
|
||||
backgroundColor: backgroundColor,
|
||||
onUpdate: () {
|
||||
controller.refresh();
|
||||
},
|
||||
return Padding(
|
||||
padding: padding ?? EdgeInsets.zero,
|
||||
child: PostListEntryWidget(
|
||||
isShowEmbed: isShowEmbed,
|
||||
isNestedClickable: isNestedClickable,
|
||||
isClickable: isClickable,
|
||||
showFeaturedReply: true,
|
||||
item: item,
|
||||
onUpdate: () {
|
||||
controller.refresh();
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
@ -48,56 +55,126 @@ class PostListWidget extends StatelessWidget {
|
||||
}
|
||||
|
||||
class PostListEntryWidget extends StatelessWidget {
|
||||
final int renderOrder;
|
||||
final bool isShowEmbed;
|
||||
final bool isNestedClickable;
|
||||
final bool isClickable;
|
||||
final bool showFeaturedReply;
|
||||
final Post item;
|
||||
final EdgeInsets? padding;
|
||||
final Function onUpdate;
|
||||
final Color? backgroundColor;
|
||||
|
||||
const PostListEntryWidget({
|
||||
super.key,
|
||||
this.renderOrder = 0,
|
||||
required this.isShowEmbed,
|
||||
required this.isNestedClickable,
|
||||
required this.isClickable,
|
||||
required this.showFeaturedReply,
|
||||
required this.item,
|
||||
this.padding,
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
child: PostItem(
|
||||
key: Key('p${item.id}'),
|
||||
item: item,
|
||||
isShowEmbed: isShowEmbed,
|
||||
isClickable: isNestedClickable,
|
||||
showFeaturedReply: showFeaturedReply,
|
||||
backgroundColor: backgroundColor,
|
||||
).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();
|
||||
return TapRegion(
|
||||
child: GestureDetector(
|
||||
onLongPress: () => _openActions(context),
|
||||
child: PostItem(
|
||||
key: Key('p${item.id}'),
|
||||
item: item,
|
||||
isShowEmbed: isShowEmbed,
|
||||
isClickable: isNestedClickable,
|
||||
showFeaturedReply: showFeaturedReply,
|
||||
padding: padding,
|
||||
onTapMore: () => _openActions(context),
|
||||
onComment: () {
|
||||
AppRouter.instance
|
||||
.pushNamed(
|
||||
'postEditor',
|
||||
extra: PostPublishArguments(reply: item),
|
||||
)
|
||||
.then((value) {
|
||||
if (value is Future) {
|
||||
value.then((_) {
|
||||
onUpdate();
|
||||
});
|
||||
} else if (value != null) {
|
||||
onUpdate();
|
||||
}
|
||||
});
|
||||
} else if (value != null) {
|
||||
onUpdate();
|
||||
}
|
||||
});
|
||||
},
|
||||
).paddingSymmetric(vertical: 8),
|
||||
),
|
||||
onTapInside: (event) {
|
||||
if (event.buttons == kSecondaryMouseButton) {
|
||||
_openActions(context);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ControlledPostListWidget extends StatelessWidget {
|
||||
final bool isShowEmbed;
|
||||
final bool isClickable;
|
||||
final bool isNestedClickable;
|
||||
final bool isPinned;
|
||||
final PagingController<int, Post> controller;
|
||||
final EdgeInsets? padding;
|
||||
final Function? onUpdate;
|
||||
|
||||
const ControlledPostListWidget({
|
||||
super.key,
|
||||
required this.controller,
|
||||
this.isShowEmbed = true,
|
||||
this.isClickable = true,
|
||||
this.isNestedClickable = true,
|
||||
this.isPinned = true,
|
||||
this.padding,
|
||||
this.onUpdate,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PagedSliverList<int, Post>.separated(
|
||||
addRepaintBoundaries: true,
|
||||
pagingController: controller,
|
||||
builderDelegate: PagedChildBuilderDelegate<Post>(
|
||||
itemBuilder: (context, item, index) {
|
||||
if (item.pinnedAt != null && !isPinned) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return PostListEntryWidget(
|
||||
isShowEmbed: isShowEmbed,
|
||||
isNestedClickable: isNestedClickable,
|
||||
isClickable: isClickable,
|
||||
showFeaturedReply: true,
|
||||
padding: padding,
|
||||
item: item,
|
||||
onUpdate: onUpdate ?? () {},
|
||||
);
|
||||
},
|
||||
),
|
||||
separatorBuilder: (_, __) => const Divider(thickness: 0.3, height: 0.3),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -11,6 +11,7 @@ class PostQuickAction extends StatefulWidget {
|
||||
final Post item;
|
||||
final bool isReactable;
|
||||
final bool isShowReply;
|
||||
final Function onComment;
|
||||
final void Function(String symbol, int num) onReact;
|
||||
|
||||
const PostQuickAction({
|
||||
@ -18,6 +19,7 @@ class PostQuickAction extends StatefulWidget {
|
||||
required this.item,
|
||||
this.isShowReply = true,
|
||||
this.isReactable = true,
|
||||
required this.onComment,
|
||||
required this.onReact,
|
||||
});
|
||||
|
||||
@ -106,7 +108,11 @@ class _PostQuickActionState extends State<PostQuickAction> {
|
||||
builder: (context) {
|
||||
return PostReplyListPopup(item: widget.item);
|
||||
},
|
||||
);
|
||||
).then((signal) {
|
||||
if (signal == true) {
|
||||
widget.onComment();
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
|
@ -8,11 +8,13 @@ import 'package:solian/widgets/posts/post_list.dart';
|
||||
|
||||
class PostReplyList extends StatefulWidget {
|
||||
final Post item;
|
||||
final EdgeInsets? padding;
|
||||
final Color? backgroundColor;
|
||||
|
||||
const PostReplyList({
|
||||
super.key,
|
||||
required this.item,
|
||||
this.padding,
|
||||
this.backgroundColor,
|
||||
});
|
||||
|
||||
@ -53,6 +55,7 @@ class _PostReplyListState extends State<PostReplyList> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PostListWidget(
|
||||
padding: widget.padding,
|
||||
isShowEmbed: false,
|
||||
controller: _pagingController,
|
||||
backgroundColor: widget.backgroundColor,
|
||||
@ -70,16 +73,31 @@ class PostReplyListPopup extends StatelessWidget {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'postReplies'.tr,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
'postReplies'.tr,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add_comment),
|
||||
visualDensity: const VisualDensity(horizontal: -4),
|
||||
onPressed: () {
|
||||
Navigator.pop(context, true);
|
||||
},
|
||||
),
|
||||
],
|
||||
).paddingOnly(left: 24, right: 24, top: 24, bottom: 8),
|
||||
Expanded(
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
PostReplyList(
|
||||
item: item,
|
||||
backgroundColor: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
padding: EdgeInsets.symmetric(horizontal: 10),
|
||||
backgroundColor:
|
||||
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,
|
||||
showFeaturedReply: true,
|
||||
onUpdate: onUpdate,
|
||||
backgroundColor: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -1,48 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
import 'package:solian/models/post.dart';
|
||||
import 'package:solian/widgets/posts/post_list.dart';
|
||||
|
||||
class PostWarpedListWidget extends StatelessWidget {
|
||||
final bool isShowEmbed;
|
||||
final bool isClickable;
|
||||
final bool isNestedClickable;
|
||||
final bool isPinned;
|
||||
final PagingController<int, Post> controller;
|
||||
final Function? onUpdate;
|
||||
|
||||
const PostWarpedListWidget({
|
||||
super.key,
|
||||
required this.controller,
|
||||
this.isShowEmbed = true,
|
||||
this.isClickable = true,
|
||||
this.isNestedClickable = true,
|
||||
this.isPinned = true,
|
||||
this.onUpdate,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PagedSliverList<int, Post>.separated(
|
||||
addRepaintBoundaries: true,
|
||||
pagingController: controller,
|
||||
builderDelegate: PagedChildBuilderDelegate<Post>(
|
||||
itemBuilder: (context, item, index) {
|
||||
if (item.pinnedAt != null && !isPinned) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return PostListEntryWidget(
|
||||
renderOrder: index,
|
||||
isShowEmbed: isShowEmbed,
|
||||
isNestedClickable: isNestedClickable,
|
||||
isClickable: isClickable,
|
||||
showFeaturedReply: true,
|
||||
item: item,
|
||||
onUpdate: onUpdate ?? () {},
|
||||
);
|
||||
},
|
||||
),
|
||||
separatorBuilder: (_, __) => const Divider(thickness: 0.3, height: 0.3),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/exts.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_profile_popup.dart';
|
||||
import 'package:solian/widgets/account/relative_select.dart';
|
||||
import 'package:solian/widgets/loading_indicator.dart';
|
||||
|
||||
class RealmMemberListPopup extends StatefulWidget {
|
||||
final Realm realm;
|
||||
@ -128,7 +128,7 @@ class _RealmMemberListPopupState extends State<RealmMemberListPopup> {
|
||||
'realmMembers'.tr,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
|
||||
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
ListTile(
|
||||
tileColor: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
@ -149,7 +149,8 @@ class _RealmMemberListPopupState extends State<RealmMemberListPopup> {
|
||||
title: Text(element.account.nick),
|
||||
subtitle: Text(element.account.name),
|
||||
leading: GestureDetector(
|
||||
child: AccountAvatar(content: element.account.avatar),
|
||||
child:
|
||||
AttachedCircleAvatar(content: element.account.avatar),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
|
@ -7,9 +7,11 @@
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <desktop_drop/desktop_drop_plugin.h>
|
||||
#include <file_saver/file_saver_plugin.h>
|
||||
#include <file_selector_linux/file_selector_plugin.h>
|
||||
#include <flutter_acrylic/flutter_acrylic_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 <media_kit_libs_linux/media_kit_libs_linux_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 =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopDropPlugin");
|
||||
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 =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
|
||||
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 =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
|
||||
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 =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterWebRTCPlugin");
|
||||
flutter_web_r_t_c_plugin_register_with_registrar(flutter_webrtc_registrar);
|
||||
|
@ -4,9 +4,11 @@
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
desktop_drop
|
||||
file_saver
|
||||
file_selector_linux
|
||||
flutter_acrylic
|
||||
flutter_secure_storage_linux
|
||||
flutter_udid
|
||||
flutter_webrtc
|
||||
media_kit_libs_linux
|
||||
media_kit_video
|
||||
|
@ -8,6 +8,7 @@ import Foundation
|
||||
import connectivity_plus
|
||||
import desktop_drop
|
||||
import device_info_plus
|
||||
import file_saver
|
||||
import file_selector_macos
|
||||
import firebase_analytics
|
||||
import firebase_core
|
||||
@ -15,6 +16,7 @@ import firebase_crashlytics
|
||||
import firebase_messaging
|
||||
import flutter_local_notifications
|
||||
import flutter_secure_storage_macos
|
||||
import flutter_udid
|
||||
import flutter_webrtc
|
||||
import gal
|
||||
import in_app_review
|
||||
@ -29,7 +31,7 @@ import protocol_handler_macos
|
||||
import screen_brightness_macos
|
||||
import share_plus
|
||||
import shared_preferences_foundation
|
||||
import sqflite
|
||||
import sqflite_darwin
|
||||
import sqlite3_flutter_libs
|
||||
import url_launcher_macos
|
||||
import wakelock_plus
|
||||
@ -38,6 +40,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
|
||||
DesktopDropPlugin.register(with: registry.registrar(forPlugin: "DesktopDropPlugin"))
|
||||
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
||||
FileSaverPlugin.register(with: registry.registrar(forPlugin: "FileSaverPlugin"))
|
||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||
FLTFirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAnalyticsPlugin"))
|
||||
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
|
||||
@ -45,6 +48,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
|
||||
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
||||
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
||||
FlutterUdidPlugin.register(with: registry.registrar(forPlugin: "FlutterUdidPlugin"))
|
||||
FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin"))
|
||||
GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin"))
|
||||
InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin"))
|
||||
|
@ -6,6 +6,8 @@ PODS:
|
||||
- FlutterMacOS
|
||||
- device_info_plus (0.0.1):
|
||||
- FlutterMacOS
|
||||
- file_saver (0.0.1):
|
||||
- FlutterMacOS
|
||||
- file_selector_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- Firebase/Analytics (11.2.0):
|
||||
@ -101,6 +103,9 @@ PODS:
|
||||
- FlutterMacOS
|
||||
- flutter_secure_storage_macos (6.1.1):
|
||||
- FlutterMacOS
|
||||
- flutter_udid (0.0.1):
|
||||
- FlutterMacOS
|
||||
- SAMKeychain
|
||||
- flutter_webrtc (0.11.3):
|
||||
- FlutterMacOS
|
||||
- WebRTC-SDK (= 125.6422.04)
|
||||
@ -188,6 +193,7 @@ PODS:
|
||||
- PromisesObjC (= 2.4.0)
|
||||
- protocol_handler_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- SAMKeychain (1.5.3)
|
||||
- screen_brightness_macos (0.1.0):
|
||||
- FlutterMacOS
|
||||
- share_plus (0.0.1):
|
||||
@ -195,7 +201,7 @@ PODS:
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- sqflite (0.0.3):
|
||||
- sqflite_darwin (0.0.4):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- "sqlite3 (3.46.1+1)":
|
||||
@ -226,6 +232,7 @@ DEPENDENCIES:
|
||||
- connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/darwin`)
|
||||
- desktop_drop (from `Flutter/ephemeral/.symlinks/plugins/desktop_drop/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`)
|
||||
- firebase_analytics (from `Flutter/ephemeral/.symlinks/plugins/firebase_analytics/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`)
|
||||
- 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_udid (from `Flutter/ephemeral/.symlinks/plugins/flutter_udid/macos`)
|
||||
- flutter_webrtc (from `Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos`)
|
||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||
- gal (from `Flutter/ephemeral/.symlinks/plugins/gal/darwin`)
|
||||
@ -249,7 +257,7 @@ DEPENDENCIES:
|
||||
- screen_brightness_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_brightness_macos/macos`)
|
||||
- share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
|
||||
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/darwin`)
|
||||
- sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`)
|
||||
- sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos`)
|
||||
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
||||
- wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`)
|
||||
@ -272,6 +280,7 @@ SPEC REPOS:
|
||||
- nanopb
|
||||
- PromisesObjC
|
||||
- PromisesSwift
|
||||
- SAMKeychain
|
||||
- sqlite3
|
||||
- WebRTC-SDK
|
||||
|
||||
@ -282,6 +291,8 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos
|
||||
device_info_plus:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos
|
||||
file_saver:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/file_saver/macos
|
||||
file_selector_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos
|
||||
firebase_analytics:
|
||||
@ -296,6 +307,8 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos
|
||||
flutter_secure_storage_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos
|
||||
flutter_udid:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/flutter_udid/macos
|
||||
flutter_webrtc:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos
|
||||
FlutterMacOS:
|
||||
@ -328,8 +341,8 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos
|
||||
shared_preferences_foundation:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
|
||||
sqflite:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/sqflite/darwin
|
||||
sqflite_darwin:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin
|
||||
sqlite3_flutter_libs:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos
|
||||
url_launcher_macos:
|
||||
@ -340,7 +353,8 @@ EXTERNAL SOURCES:
|
||||
SPEC CHECKSUMS:
|
||||
connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db
|
||||
desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898
|
||||
device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720
|
||||
device_info_plus: f1aae8670672f75c4c8850ecbe0b2ddef62b0a22
|
||||
file_saver: 44e6fbf666677faf097302460e214e977fdd977b
|
||||
file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d
|
||||
Firebase: 98e6bf5278170668a7983e12971a66b2cd57fc8c
|
||||
firebase_analytics: 30ff72f6d4847ff0b479d8edd92fc8582e719072
|
||||
@ -358,6 +372,7 @@ SPEC CHECKSUMS:
|
||||
FirebaseSessions: 655ff17f3cc1a635cbdc2d69b953878001f9e25b
|
||||
flutter_local_notifications: 3805ca215b2fb7f397d78b66db91f6a747af52e4
|
||||
flutter_secure_storage_macos: 59459653abe1adb92abbc8ea747d79f8d19866c9
|
||||
flutter_udid: 6b2b89780c3dfeecf0047bdf93f622d6416b1c07
|
||||
flutter_webrtc: 2b4e4a2de70a1485836e40fd71a7a94c77d49bd9
|
||||
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
||||
gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1
|
||||
@ -371,16 +386,17 @@ SPEC CHECKSUMS:
|
||||
media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5
|
||||
media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5
|
||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||
package_info_plus: fa739dd842b393193c5ca93c26798dff6e3d0e0c
|
||||
package_info_plus: d2f71247aab4b6521434f887276093acc70d214c
|
||||
pasteboard: 9b69dba6fedbb04866be632205d532fe2f6b1d99
|
||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
|
||||
protocol_handler_macos: d10a6c01d6373389ffd2278013ab4c47ed6d6daa
|
||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||
screen_brightness_macos: 2d6d3af2165592d9a55ffcd95b7550970e41ebda
|
||||
share_plus: 36537c04ce0c3e3f5bd297ce4318b6d5ee5fd6cf
|
||||
share_plus: a182a58e04e51647c0481aadabbc4de44b3a2bce
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||
sqflite_darwin: a553b1fd6fe66f53bbb0fe5b4f5bab93f08d7a13
|
||||
sqlite3: 0bb0e6389d824e40296f531b858a2a0b71c0d2fb
|
||||
sqlite3_flutter_libs: 5ca46c1a04eddfbeeb5b16566164aa7ad1616e7b
|
||||
url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404
|
||||
|
@ -14,6 +14,8 @@
|
||||
<true/>
|
||||
<key>com.apple.security.device.camera</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.downloads.read-write</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-only</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
|
@ -49,7 +49,6 @@
|
||||
<string>NSApplication</string>
|
||||
<key>CFBundleLocalizations</key>
|
||||
<array>
|
||||
<string>zh_CN</string>
|
||||
<string>en</string>
|
||||
</array>
|
||||
<key>NSUserActivityTypes</key>
|
||||
@ -63,5 +62,9 @@
|
||||
<string>Allow you record audio for your message or post</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>Allow you add photo to your message or post</string>
|
||||
<key>UIFileSharingEnabled</key>
|
||||
<true/>
|
||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -12,6 +12,8 @@
|
||||
<true/>
|
||||
<key>com.apple.security.device.camera</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.downloads.read-write</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-only</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
|
156
pubspec.lock
156
pubspec.lock
@ -74,10 +74,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: args
|
||||
sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a"
|
||||
sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.0"
|
||||
version: "2.6.0"
|
||||
async:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -198,14 +198,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -346,10 +338,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: device_info_plus
|
||||
sha256: a7fd703482b391a87d60b6061d04dfdeab07826b96f9abd8f5ed98068acc0074
|
||||
sha256: db03b2d2a3fa466a4627709e1db58692c3f7f658e36a5942d342d86efedc4091
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.1.2"
|
||||
version: "11.0.0"
|
||||
device_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -422,6 +414,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -450,10 +450,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file
|
||||
sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
|
||||
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.0"
|
||||
version: "7.0.1"
|
||||
file_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -462,6 +462,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -695,10 +703,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_card_swiper
|
||||
sha256: "880ad669017154d6d1f8c3abd861db08af97b3b7b0f7d7d5cbde690a9253811d"
|
||||
sha256: "1eacbfab31b572223042e03409726553aec431abe48af48c8d591d376d070d3d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
version: "7.0.2"
|
||||
flutter_keyboard_visibility:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -791,10 +799,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_markdown
|
||||
sha256: e17575ca576a34b46c58c91f9948891117a1bd97815d2e661813c7f90c647a78
|
||||
sha256: bd9c475d9aae256369edacafc29d1e74c81f78a10cdcdacbbbc9e3c43d009e4a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.3+2"
|
||||
version: "0.7.4"
|
||||
flutter_native_splash:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@ -811,6 +819,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -896,6 +912,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
@ -1261,6 +1285,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.0"
|
||||
marquee:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: marquee
|
||||
sha256: a87e7e80c5d21434f90ad92add9f820cf68be374b226404fe881d2bba7be0862
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1401,10 +1433,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: package_info_plus
|
||||
sha256: a75164ade98cb7d24cfd0a13c6408927c6b217fa60dee5a7ff5c116a58f28918
|
||||
sha256: "894f37107424311bdae3e476552229476777b8752c5a2a2369c0cb9a2d5442ef"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.0.2"
|
||||
version: "8.0.3"
|
||||
package_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1449,10 +1481,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: f7544c346a0742aee1450f9e5c0f5269d7c602b9c95fdbcd9fb8f5b1df13b1cc
|
||||
sha256: c464428172cb986b758c6d1724c603097febb8fb855aa265aeecc9280c294d4a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.11"
|
||||
version: "2.2.12"
|
||||
path_provider_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1685,6 +1717,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1757,6 +1805,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1769,18 +1825,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: share_plus
|
||||
sha256: "468c43f285207c84bcabf5737f33b914ceb8eb38398b91e5e3ad1698d1b72a52"
|
||||
sha256: fec12c3c39f01e4df1ec6ad92b6e85503c5ca64ffd6e28d18c9ffe53fcc4cb11
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.2"
|
||||
version: "10.0.3"
|
||||
share_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: share_plus_platform_interface
|
||||
sha256: "6ababf341050edff57da8b6990f11f4e99eaba837865e2e6defe16d039619db5"
|
||||
sha256: c57c0bbfec7142e3a0f55633be504b796af72e60e3c791b44d5a017b985f7a48
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.0"
|
||||
version: "5.0.1"
|
||||
shared_preferences:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1902,18 +1958,42 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite
|
||||
sha256: ff5a2436ef8ebdfda748fbfe957f9981524cb5ff11e7bafa8c42771840e8a788
|
||||
sha256: "79a297dc3cc137e758c6a4baf83342b039e5a6d2436fcdf3f96a00adaaf2ad62"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.3+2"
|
||||
version: "2.4.0"
|
||||
sqflite_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_android
|
||||
sha256: "78f489aab276260cdd26676d2169446c7ecd3484bbd5fead4ca14f3ed4dd9ee3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
sqflite_common:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_common
|
||||
sha256: "2d8e607db72e9cb7748c9c6e739e2c9618320a5517de693d5a24609c4671b1a4"
|
||||
sha256: "4468b24876d673418a7b7147e5a08a715b4998a7ae69227acafaab762e0e5490"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.4+4"
|
||||
version: "2.5.4+5"
|
||||
sqflite_darwin:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_darwin
|
||||
sha256: "769733dddf94622d5541c73e4ddc6aa7b252d865285914b6fcd54a63c4b4f027"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1-1"
|
||||
sqflite_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_platform_interface
|
||||
sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
sqlite3:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -2010,6 +2090,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -2062,10 +2150,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: url_launcher
|
||||
sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3"
|
||||
sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.0"
|
||||
version: "6.3.1"
|
||||
url_launcher_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -2254,10 +2342,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
sha256: "4d45dc9069dba4619dc0ebd93c7cec5e66d8482cb625a370ac806dcc8165f2ec"
|
||||
sha256: e5c39a90447e7c81cfec14b041cdbd0d0916bd9ebbc7fe02ab69568be703b9bd
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.5.5"
|
||||
version: "5.6.0"
|
||||
win32_registry:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
12
pubspec.yaml
12
pubspec.yaml
@ -2,7 +2,7 @@ name: solian
|
||||
description: "The Solar Network App"
|
||||
publish_to: "none"
|
||||
|
||||
version: 1.3.6+5
|
||||
version: 1.4.0+14
|
||||
|
||||
environment:
|
||||
sdk: ">=3.3.4 <4.0.0"
|
||||
@ -18,7 +18,6 @@ dependencies:
|
||||
flutter_markdown: ^0.7.1
|
||||
flutter_animate: ^4.5.0
|
||||
flutter_secure_storage: ^9.2.1
|
||||
carousel_slider: ^5.0.0
|
||||
url_launcher: ^6.2.6
|
||||
infinite_scroll_pagination: ^4.0.0
|
||||
image_picker: ^1.1.1
|
||||
@ -38,7 +37,7 @@ dependencies:
|
||||
firebase_core: ^3.0.0
|
||||
firebase_messaging: ^15.0.0
|
||||
package_info_plus: ^8.0.0
|
||||
device_info_plus: ^10.1.0
|
||||
device_info_plus: ^11.0.0
|
||||
flutter_acrylic: ^1.1.4
|
||||
protocol_handler: ^0.2.0
|
||||
markdown: ^7.2.2
|
||||
@ -84,6 +83,13 @@ dependencies:
|
||||
action_slider: ^0.7.0
|
||||
in_app_review: ^2.0.9
|
||||
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
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
9
roadsign.toml
Normal file
9
roadsign.toml
Normal file
@ -0,0 +1,9 @@
|
||||
id = "solian"
|
||||
|
||||
[[locations]]
|
||||
id = "solian"
|
||||
host = ["sn.solsynth.dev"]
|
||||
path = ["/"]
|
||||
[[locations.destinations]]
|
||||
id = "solian-web"
|
||||
uri = "files:///workdir/solian?fallback=index.html&index=index.html"
|
@ -111,15 +111,14 @@
|
||||
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<body oncontextmenu="return false;">
|
||||
<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/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="">
|
||||
</picture>
|
||||
|
||||
|
||||
<script src="flutter_bootstrap.js" async=""></script>
|
||||
<script src="flutter_bootstrap.js" async=""></script>
|
||||
|
||||
|
||||
</body></html>
|
@ -8,10 +8,12 @@
|
||||
|
||||
#include <connectivity_plus/connectivity_plus_windows_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 <firebase_core/firebase_core_plugin_c_api.h>
|
||||
#include <flutter_acrylic/flutter_acrylic_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 <gal/gal_plugin_c_api.h>
|
||||
#include <livekit_client/live_kit_plugin.h>
|
||||
@ -30,6 +32,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
|
||||
DesktopDropPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("DesktopDropPlugin"));
|
||||
FileSaverPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FileSaverPlugin"));
|
||||
FileSelectorWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||
FirebaseCorePluginCApiRegisterWithRegistrar(
|
||||
@ -38,6 +42,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
registry->GetRegistrarForPlugin("FlutterAcrylicPlugin"));
|
||||
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
||||
FlutterUdidPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FlutterUdidPluginCApi"));
|
||||
FlutterWebRTCPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FlutterWebRTCPlugin"));
|
||||
GalPluginCApiRegisterWithRegistrar(
|
||||
|
@ -5,10 +5,12 @@
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
connectivity_plus
|
||||
desktop_drop
|
||||
file_saver
|
||||
file_selector_windows
|
||||
firebase_core
|
||||
flutter_acrylic
|
||||
flutter_secure_storage_windows
|
||||
flutter_udid
|
||||
flutter_webrtc
|
||||
gal
|
||||
livekit_client
|
||||
|
Reference in New Issue
Block a user