Compare commits
22 Commits
c1e10916ee
...
2.0.0+2
| Author | SHA1 | Date | |
|---|---|---|---|
| 654a71e852 | |||
| 455ffcac19 | |||
| 9c8dad0176 | |||
| 2c6b1feca6 | |||
| af044a86bc | |||
| 4884d04a51 | |||
| b9ad6d4fd0 | |||
| 468d1377af | |||
| 9851093a1e | |||
| 5368f8ebb0 | |||
| e5239a6ca0 | |||
| 5b198412f6 | |||
| 7087c41f07 | |||
| b166a6e85c | |||
| f23ffe61f5 | |||
| 1ff4dc2a4b | |||
| 9df9674ada | |||
| 5c9ea588cc | |||
| 5364aecf74 | |||
| a673beb87c | |||
| 49cabd1f39 | |||
| ac70624c4e |
3
.fvmrc
3
.fvmrc
@@ -1,4 +1,3 @@
|
|||||||
{
|
{
|
||||||
"flutter": "master",
|
"flutter": "stable"
|
||||||
"flutterUrl": "https://github.com/Flutter-Foundation/flutter.git"
|
|
||||||
}
|
}
|
||||||
4
.github/workflows/nightly.yml
vendored
4
.github/workflows/nightly.yml
vendored
@@ -13,10 +13,10 @@ jobs:
|
|||||||
- name: Set up Flutter
|
- name: Set up Flutter
|
||||||
uses: subosito/flutter-action@v2
|
uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
channel: master
|
channel: stable
|
||||||
cache: true
|
cache: true
|
||||||
- run: flutter pub get
|
- run: flutter pub get
|
||||||
- run: flutter build web --release --base-href=/
|
- run: flutter build web --release
|
||||||
- name: Archive production artifacts
|
- name: Archive production artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
|
|||||||
13
.roadsignrc
Normal file
13
.roadsignrc
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"sync": {
|
||||||
|
"region": "solian-next",
|
||||||
|
"configPath": "roadsign.toml"
|
||||||
|
},
|
||||||
|
"deployments": [
|
||||||
|
{
|
||||||
|
"region": "solian-next",
|
||||||
|
"site": "solian-next-web",
|
||||||
|
"path": "build/web"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -14,6 +14,9 @@
|
|||||||
"screenAccountPublisherNew": "New Publisher",
|
"screenAccountPublisherNew": "New Publisher",
|
||||||
"screenAccountPublisherEdit": "Edit Publisher",
|
"screenAccountPublisherEdit": "Edit Publisher",
|
||||||
"screenAccountProfileEdit": "Edit Profile",
|
"screenAccountProfileEdit": "Edit Profile",
|
||||||
|
"screenSettings": "Settings",
|
||||||
|
"screenAlbum": "Album",
|
||||||
|
"screenChat": "Chat",
|
||||||
"dialogOkay": "Okay",
|
"dialogOkay": "Okay",
|
||||||
"dialogCancel": "Cancel",
|
"dialogCancel": "Cancel",
|
||||||
"dialogConfirm": "Confirm",
|
"dialogConfirm": "Confirm",
|
||||||
@@ -25,17 +28,24 @@
|
|||||||
"errorRequestNotFound": "The resource that you looking for is not found.",
|
"errorRequestNotFound": "The resource that you looking for is not found.",
|
||||||
"errorRequestConnection": "Network connection error, please check your network or the service status.",
|
"errorRequestConnection": "Network connection error, please check your network or the service status.",
|
||||||
"errorRequestUnknown": "Unknown request error, maybe you want to take screenshot and report it to us.",
|
"errorRequestUnknown": "Unknown request error, maybe you want to take screenshot and report it to us.",
|
||||||
"prev": "Next",
|
"prev": "Previous",
|
||||||
"next": "Previous",
|
"next": "Next",
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
"apply": "Apply",
|
"apply": "Apply",
|
||||||
"create": "Create",
|
"create": "Create",
|
||||||
"preview": "Preview",
|
"preview": "Preview",
|
||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
|
"unlink": "Unlink",
|
||||||
|
"crop": "Crop",
|
||||||
|
"compress": "Compress",
|
||||||
"report": "Report",
|
"report": "Report",
|
||||||
"repost": "Repost",
|
"repost": "Repost",
|
||||||
"reply": "Reply",
|
"reply": "Reply",
|
||||||
|
"unset": "Unset",
|
||||||
|
"untitled": "Untitled",
|
||||||
|
"postDetail": "Post detail",
|
||||||
|
"postNoun": "Post",
|
||||||
"fieldUsername": "Username",
|
"fieldUsername": "Username",
|
||||||
"fieldNickname": "Nickname",
|
"fieldNickname": "Nickname",
|
||||||
"fieldEmail": "Email address",
|
"fieldEmail": "Email address",
|
||||||
@@ -78,7 +88,48 @@
|
|||||||
"fieldPostTitle": "Title",
|
"fieldPostTitle": "Title",
|
||||||
"fieldPostDescription": "Description",
|
"fieldPostDescription": "Description",
|
||||||
"postPublish": "Publish",
|
"postPublish": "Publish",
|
||||||
|
"postPosted": "Post has been posted.",
|
||||||
|
"postPublishedAt": "Published At",
|
||||||
|
"postPublishedUntil": "Published Until",
|
||||||
"postEditingNotice": "You're about to editing a post that posted {}.",
|
"postEditingNotice": "You're about to editing a post that posted {}.",
|
||||||
"postReplyingNotice": "You're about to reply to a post that posted {}.",
|
"postReplyingNotice": "You're about to reply to a post that posted {}.",
|
||||||
"postRepostingNotice": "You're about to repost a post that posted {}."
|
"postRepostingNotice": "You're about to repost a post that posted {}.",
|
||||||
|
"postReact": "React",
|
||||||
|
"postReactions": "Reactions of Post",
|
||||||
|
"postReactionPoints": {
|
||||||
|
"zero": "{} pt",
|
||||||
|
"one": "{} pt",
|
||||||
|
"other": "{} pts"
|
||||||
|
},
|
||||||
|
"postReactCompleted": "Reaction has been added.",
|
||||||
|
"postReactUncompleted": "Reaction has been removed.",
|
||||||
|
"postComments": {
|
||||||
|
"zero": "Comment",
|
||||||
|
"one": "{} comment",
|
||||||
|
"other": "{} comments"
|
||||||
|
},
|
||||||
|
"postCommentsDetailed": {
|
||||||
|
"zero": "No comments",
|
||||||
|
"one": "{} comment",
|
||||||
|
"other": "{} comments"
|
||||||
|
},
|
||||||
|
"settingsAppearance": "Appearance",
|
||||||
|
"settingsBackgroundImage": "Background Image",
|
||||||
|
"settingsBackgroundImageDescription": "Set the background image that will be applied globally.",
|
||||||
|
"settingsBackgroundImageClear": "Clear Existing Background Image",
|
||||||
|
"settingsBackgroundImageClearDescription": "Reset the background image to blank.",
|
||||||
|
"settingsThemeMaterial3": "Use Material You Design",
|
||||||
|
"settingsThemeMaterial3Description": "Set the application theme to Material 3 Design.",
|
||||||
|
"settingsNetwork": "Network",
|
||||||
|
"settingsNetworkServer": "HyperNet Server",
|
||||||
|
"settingsNetworkServerDescription": "Set the HyperNet server address, choose ours or build your own.",
|
||||||
|
"settingsNetworkServerReset": "Reset to Official Server",
|
||||||
|
"settingsNetworkServerResetDescription": "Reset to the official server address of Solar Network.",
|
||||||
|
"settingsNetworkServerPreset": "Present HyperNet Server",
|
||||||
|
"settingsNetworkServerPresetDescription": "You can choose one of our preset HyperNet server addresses from the list on the right.",
|
||||||
|
"settingsNetworkServerSaved": "Server address saved.",
|
||||||
|
"sensitiveContent": "Sensitive Content",
|
||||||
|
"sensitiveContentCollapsed": "Sensitive content has been collapsed.",
|
||||||
|
"sensitiveContentDescription": "This content has been marked as sensitive, and may not be suitable for all viewers.",
|
||||||
|
"sensitiveContentReveal": "Reveal"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,9 @@
|
|||||||
"screenAccountPublisherNew": "新建发布者",
|
"screenAccountPublisherNew": "新建发布者",
|
||||||
"screenAccountPublisherEdit": "编辑发布者",
|
"screenAccountPublisherEdit": "编辑发布者",
|
||||||
"screenAccountProfileEdit": "编辑资料",
|
"screenAccountProfileEdit": "编辑资料",
|
||||||
|
"screenSettings": "设置",
|
||||||
|
"screenAlbum": "相册",
|
||||||
|
"screenChat": "聊天",
|
||||||
"dialogOkay": "好的",
|
"dialogOkay": "好的",
|
||||||
"dialogCancel": "取消",
|
"dialogCancel": "取消",
|
||||||
"dialogConfirm": "确认",
|
"dialogConfirm": "确认",
|
||||||
@@ -33,9 +36,16 @@
|
|||||||
"create": "创建",
|
"create": "创建",
|
||||||
"preview": "预览",
|
"preview": "预览",
|
||||||
"delete": "删除",
|
"delete": "删除",
|
||||||
|
"unlink": "解除链接",
|
||||||
|
"crop": "裁剪",
|
||||||
|
"compress": "压缩",
|
||||||
"report": "检举",
|
"report": "检举",
|
||||||
"repost": "转帖",
|
"repost": "转帖",
|
||||||
"reply": "回贴",
|
"reply": "回贴",
|
||||||
|
"unset": "未设置",
|
||||||
|
"untitled": "无题",
|
||||||
|
"postDetail": "帖子详情",
|
||||||
|
"postNoun": "帖子",
|
||||||
"fieldUsername": "用户名",
|
"fieldUsername": "用户名",
|
||||||
"fieldNickname": "显示名",
|
"fieldNickname": "显示名",
|
||||||
"fieldEmail": "电子邮箱地址",
|
"fieldEmail": "电子邮箱地址",
|
||||||
@@ -78,7 +88,48 @@
|
|||||||
"fieldPostTitle": "标题",
|
"fieldPostTitle": "标题",
|
||||||
"fieldPostDescription": "描述",
|
"fieldPostDescription": "描述",
|
||||||
"postPublish": "发布",
|
"postPublish": "发布",
|
||||||
|
"postPublishedAt": "发布于",
|
||||||
|
"postPublishedUntil": "取消发布于",
|
||||||
"postEditingNotice": "你正在修改由 {} 发布的帖子。",
|
"postEditingNotice": "你正在修改由 {} 发布的帖子。",
|
||||||
"postReplyingNotice": "你正在回复由 {} 发布的帖子。",
|
"postReplyingNotice": "你正在回复由 {} 发布的帖子。",
|
||||||
"postRepostingNotice": "你正在转发由 {} 发布的帖子。"
|
"postRepostingNotice": "你正在转发由 {} 发布的帖子。",
|
||||||
|
"postReact": "反应",
|
||||||
|
"postPosted": "帖子已经发表。",
|
||||||
|
"postReactions": "帖子的反应",
|
||||||
|
"postReactionPoints": {
|
||||||
|
"zero": "{} 点",
|
||||||
|
"one": "{} 点",
|
||||||
|
"other": "{} 点"
|
||||||
|
},
|
||||||
|
"postReactCompleted": "反应已被添加。",
|
||||||
|
"postReactUncompleted": "反应已被移除。",
|
||||||
|
"postComments": {
|
||||||
|
"zero": "评论",
|
||||||
|
"one": "{} 条评论",
|
||||||
|
"other": "{} 条评论"
|
||||||
|
},
|
||||||
|
"postCommentsDetailed": {
|
||||||
|
"zero": "没有评论",
|
||||||
|
"one": "{} 条评论",
|
||||||
|
"other": "{} 条评论"
|
||||||
|
},
|
||||||
|
"settingsAppearance": "外观",
|
||||||
|
"settingsBackgroundImage": "背景图片",
|
||||||
|
"settingsBackgroundImageDescription": "设置应用全局生效的的背景图片。",
|
||||||
|
"settingsBackgroundImageClear": "清除现存背景图",
|
||||||
|
"settingsBackgroundImageClearDescription": "将应用背景图重置为空白。",
|
||||||
|
"settingsThemeMaterial3": "使用 Material You 设计范式",
|
||||||
|
"settingsThemeMaterial3Description": "将应用主题设置为 Material 3 设计范式的主题。",
|
||||||
|
"settingsNetwork": "网络",
|
||||||
|
"settingsNetworkServer": "HyperNet 服务器",
|
||||||
|
"settingsNetworkServerDescription": "设置 HyperNet 服务器地址,选择我们提供的,或者自己搭建。",
|
||||||
|
"settingsNetworkServerReset": "重设为官方服务器",
|
||||||
|
"settingsNetworkServerResetDescription": "重设为 Solar Network 的服务器地址。",
|
||||||
|
"settingsNetworkServerPreset": "预设的 HyperNet 服务器",
|
||||||
|
"settingsNetworkServerPresetDescription": "你可以在旁边的列表中选择我们提供的预设 HyperNet 服务器地址。",
|
||||||
|
"settingsNetworkServerSaved": "服务器地址已保存。",
|
||||||
|
"sensitiveContent": "敏感内容",
|
||||||
|
"sensitiveContentCollapsed": "敏感内容已折叠。",
|
||||||
|
"sensitiveContentDescription": "此内容已被标记,可能不适合所有人查看。",
|
||||||
|
"sensitiveContentReveal": "显示内容"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,41 +41,18 @@ PODS:
|
|||||||
- DKImagePickerController/PhotoGallery
|
- DKImagePickerController/PhotoGallery
|
||||||
- Flutter
|
- Flutter
|
||||||
- Flutter (1.0.0)
|
- Flutter (1.0.0)
|
||||||
- flutter_image_compress_common (1.0.0):
|
|
||||||
- Flutter
|
|
||||||
- Mantle
|
|
||||||
- SDWebImage
|
|
||||||
- SDWebImageWebPCoder
|
|
||||||
- flutter_native_splash (0.0.1):
|
- flutter_native_splash (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- flutter_secure_storage (3.3.1):
|
- flutter_secure_storage (3.3.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- image_picker_ios (0.0.1):
|
- image_picker_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- libwebp (1.3.2):
|
|
||||||
- libwebp/demux (= 1.3.2)
|
|
||||||
- libwebp/mux (= 1.3.2)
|
|
||||||
- libwebp/sharpyuv (= 1.3.2)
|
|
||||||
- libwebp/webp (= 1.3.2)
|
|
||||||
- libwebp/demux (1.3.2):
|
|
||||||
- libwebp/webp
|
|
||||||
- libwebp/mux (1.3.2):
|
|
||||||
- libwebp/demux
|
|
||||||
- libwebp/sharpyuv (1.3.2)
|
|
||||||
- libwebp/webp (1.3.2):
|
|
||||||
- libwebp/sharpyuv
|
|
||||||
- Mantle (2.2.0):
|
|
||||||
- Mantle/extobjc (= 2.2.0)
|
|
||||||
- Mantle/extobjc (2.2.0)
|
|
||||||
- path_provider_foundation (0.0.1):
|
- path_provider_foundation (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- SDWebImage (5.19.7):
|
- SDWebImage (5.19.7):
|
||||||
- SDWebImage/Core (= 5.19.7)
|
- SDWebImage/Core (= 5.19.7)
|
||||||
- SDWebImage/Core (5.19.7)
|
- SDWebImage/Core (5.19.7)
|
||||||
- SDWebImageWebPCoder (0.14.6):
|
|
||||||
- libwebp (~> 1.0)
|
|
||||||
- SDWebImage/Core (~> 5.17)
|
|
||||||
- shared_preferences_foundation (0.0.1):
|
- shared_preferences_foundation (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
@@ -92,7 +69,6 @@ DEPENDENCIES:
|
|||||||
- cupertino_http (from `.symlinks/plugins/cupertino_http/ios`)
|
- cupertino_http (from `.symlinks/plugins/cupertino_http/ios`)
|
||||||
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
||||||
- Flutter (from `Flutter`)
|
- Flutter (from `Flutter`)
|
||||||
- flutter_image_compress_common (from `.symlinks/plugins/flutter_image_compress_common/ios`)
|
|
||||||
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
||||||
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
||||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||||
@@ -105,10 +81,7 @@ SPEC REPOS:
|
|||||||
trunk:
|
trunk:
|
||||||
- DKImagePickerController
|
- DKImagePickerController
|
||||||
- DKPhotoGallery
|
- DKPhotoGallery
|
||||||
- libwebp
|
|
||||||
- Mantle
|
|
||||||
- SDWebImage
|
- SDWebImage
|
||||||
- SDWebImageWebPCoder
|
|
||||||
- SwiftyGif
|
- SwiftyGif
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
@@ -122,8 +95,6 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/file_picker/ios"
|
:path: ".symlinks/plugins/file_picker/ios"
|
||||||
Flutter:
|
Flutter:
|
||||||
:path: Flutter
|
:path: Flutter
|
||||||
flutter_image_compress_common:
|
|
||||||
:path: ".symlinks/plugins/flutter_image_compress_common/ios"
|
|
||||||
flutter_native_splash:
|
flutter_native_splash:
|
||||||
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
||||||
flutter_secure_storage:
|
flutter_secure_storage:
|
||||||
@@ -147,15 +118,11 @@ SPEC CHECKSUMS:
|
|||||||
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||||
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
|
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
|
||||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||||
flutter_image_compress_common: ec1d45c362c9d30a3f6a0426c297f47c52007e3e
|
|
||||||
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
|
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
|
||||||
flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec
|
flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec
|
||||||
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
||||||
libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009
|
|
||||||
Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
|
|
||||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||||
SDWebImage: 8a6b7b160b4d710e2a22b6900e25301075c34cb3
|
SDWebImage: 8a6b7b160b4d710e2a22b6900e25301075c34cb3
|
||||||
SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380
|
|
||||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||||
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
||||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||||
|
|||||||
@@ -161,7 +161,6 @@
|
|||||||
64FBE78F9C282712818D6D95 /* Pods-RunnerTests.release.xcconfig */,
|
64FBE78F9C282712818D6D95 /* Pods-RunnerTests.release.xcconfig */,
|
||||||
96081771773FA019A97CCC3F /* Pods-RunnerTests.profile.xcconfig */,
|
96081771773FA019A97CCC3F /* Pods-RunnerTests.profile.xcconfig */,
|
||||||
);
|
);
|
||||||
name = Pods;
|
|
||||||
path = Pods;
|
path = Pods;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@@ -474,6 +473,7 @@
|
|||||||
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = Solian;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
@@ -657,6 +657,7 @@
|
|||||||
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = Solian;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
@@ -680,6 +681,7 @@
|
|||||||
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = Solian;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>Surface</string>
|
<string>Solian</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>surface</string>
|
<string>Solian</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
|
|||||||
391
lib/controllers/post_write_controller.dart
Normal file
391
lib/controllers/post_write_controller.dart
Normal file
@@ -0,0 +1,391 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
import 'package:mime/mime.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:surface/providers/sn_attachment.dart';
|
||||||
|
import 'package:surface/providers/sn_network.dart';
|
||||||
|
import 'package:surface/types/attachment.dart';
|
||||||
|
import 'package:surface/types/post.dart';
|
||||||
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
import 'package:surface/widgets/universal_image.dart';
|
||||||
|
|
||||||
|
enum PostWriteMediaType {
|
||||||
|
image,
|
||||||
|
video,
|
||||||
|
audio,
|
||||||
|
file,
|
||||||
|
}
|
||||||
|
|
||||||
|
class PostWriteMedia {
|
||||||
|
late String name;
|
||||||
|
late PostWriteMediaType type;
|
||||||
|
final SnAttachment? attachment;
|
||||||
|
final XFile? file;
|
||||||
|
final Uint8List? raw;
|
||||||
|
|
||||||
|
PostWriteMedia(this.attachment, {this.file, this.raw}) {
|
||||||
|
name = attachment!.name;
|
||||||
|
|
||||||
|
switch (attachment?.mimetype.split('/').firstOrNull) {
|
||||||
|
case 'image':
|
||||||
|
type = PostWriteMediaType.image;
|
||||||
|
break;
|
||||||
|
case 'video':
|
||||||
|
type = PostWriteMediaType.video;
|
||||||
|
break;
|
||||||
|
case 'audio':
|
||||||
|
type = PostWriteMediaType.audio;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
type = PostWriteMediaType.file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PostWriteMedia.fromFile(this.file, {this.attachment, this.raw}) {
|
||||||
|
name = file!.name;
|
||||||
|
|
||||||
|
String? mimetype = file!.mimeType;
|
||||||
|
mimetype ??= lookupMimeType(file!.path);
|
||||||
|
|
||||||
|
switch (mimetype?.split('/').firstOrNull) {
|
||||||
|
case 'image':
|
||||||
|
type = PostWriteMediaType.image;
|
||||||
|
break;
|
||||||
|
case 'video':
|
||||||
|
type = PostWriteMediaType.video;
|
||||||
|
break;
|
||||||
|
case 'audio':
|
||||||
|
type = PostWriteMediaType.audio;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
type = PostWriteMediaType.file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PostWriteMedia.fromBytes(this.raw, this.name, this.type,
|
||||||
|
{this.attachment, this.file});
|
||||||
|
|
||||||
|
bool get isEmpty => attachment == null && file == null && raw == null;
|
||||||
|
|
||||||
|
Future<int?> length() async {
|
||||||
|
if (attachment != null) {
|
||||||
|
return attachment!.size;
|
||||||
|
} else if (file != null) {
|
||||||
|
return await file!.length();
|
||||||
|
} else if (raw != null) {
|
||||||
|
return raw!.length;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
XFile? toFile() {
|
||||||
|
if (file != null) {
|
||||||
|
return file!;
|
||||||
|
} else if (raw != null) {
|
||||||
|
return XFile.fromData(raw!, name: name);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageProvider? getImageProvider(
|
||||||
|
BuildContext context, {
|
||||||
|
int? width,
|
||||||
|
int? height,
|
||||||
|
}) {
|
||||||
|
if (attachment != null) {
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
final ImageProvider provider =
|
||||||
|
UniversalImage.provider(sn.getAttachmentUrl(attachment!.rid));
|
||||||
|
if (width != null && height != null) {
|
||||||
|
return ResizeImage(
|
||||||
|
provider,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
policy: ResizeImagePolicy.fit,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return provider;
|
||||||
|
} else if (file != null) {
|
||||||
|
final ImageProvider provider =
|
||||||
|
kIsWeb ? NetworkImage(file!.path) : FileImage(File(file!.path));
|
||||||
|
if (width != null && height != null) {
|
||||||
|
return ResizeImage(
|
||||||
|
provider,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
policy: ResizeImagePolicy.fit,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return provider;
|
||||||
|
} else if (raw != null) {
|
||||||
|
final provider = MemoryImage(raw!);
|
||||||
|
if (width != null && height != null) {
|
||||||
|
return ResizeImage(
|
||||||
|
provider,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
policy: ResizeImagePolicy.fit,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PostWriteController extends ChangeNotifier {
|
||||||
|
static const Map<String, String> kTitleMap = {
|
||||||
|
'stories': 'writePostTypeStory',
|
||||||
|
'articles': 'writePostTypeArticle',
|
||||||
|
};
|
||||||
|
|
||||||
|
static const kAttachmentProgressWeight = 0.9;
|
||||||
|
static const kPostingProgressWeight = 0.1;
|
||||||
|
|
||||||
|
final TextEditingController contentController = TextEditingController();
|
||||||
|
final TextEditingController titleController = TextEditingController();
|
||||||
|
final TextEditingController descriptionController = TextEditingController();
|
||||||
|
|
||||||
|
PostWriteController() {
|
||||||
|
titleController.addListener(() => notifyListeners());
|
||||||
|
descriptionController.addListener(() => notifyListeners());
|
||||||
|
}
|
||||||
|
|
||||||
|
String mode = kTitleMap.keys.first;
|
||||||
|
|
||||||
|
String get title => titleController.text;
|
||||||
|
String get description => descriptionController.text;
|
||||||
|
bool get isRelatedNull =>
|
||||||
|
![editingPost, repostingPost, replyingPost].any((ele) => ele != null);
|
||||||
|
|
||||||
|
bool isLoading = false, isBusy = false;
|
||||||
|
double? progress;
|
||||||
|
|
||||||
|
SnPublisher? publisher;
|
||||||
|
SnPost? editingPost, repostingPost, replyingPost;
|
||||||
|
|
||||||
|
List<PostWriteMedia> attachments = List.empty(growable: true);
|
||||||
|
DateTime? publishedAt, publishedUntil;
|
||||||
|
|
||||||
|
Future<void> fetchRelatedPost(
|
||||||
|
BuildContext context, {
|
||||||
|
int? editing,
|
||||||
|
int? reposting,
|
||||||
|
int? replying,
|
||||||
|
}) async {
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
final attach = context.read<SnAttachmentProvider>();
|
||||||
|
|
||||||
|
isLoading = true;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (editing != null) {
|
||||||
|
final resp = await sn.client.get('/cgi/co/posts/$editing');
|
||||||
|
final post = SnPost.fromJson(resp.data);
|
||||||
|
final alts = await attach
|
||||||
|
.getMultiple(post.body['attachments']?.cast<String>() ?? []);
|
||||||
|
publisher = post.publisher;
|
||||||
|
titleController.text = post.body['title'] ?? '';
|
||||||
|
descriptionController.text = post.body['description'] ?? '';
|
||||||
|
contentController.text = post.body['content'] ?? '';
|
||||||
|
publishedAt = post.publishedAt;
|
||||||
|
publishedUntil = post.publishedUntil;
|
||||||
|
attachments.addAll(alts.map((ele) => PostWriteMedia(ele)));
|
||||||
|
|
||||||
|
editingPost = post.copyWith(
|
||||||
|
preload: SnPostPreload(
|
||||||
|
attachments: alts,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (replying != null) {
|
||||||
|
final resp = await sn.client.get('/cgi/co/posts/$replying');
|
||||||
|
final post = SnPost.fromJson(resp.data);
|
||||||
|
replyingPost = post.copyWith(
|
||||||
|
preload: SnPostPreload(
|
||||||
|
attachments: await attach
|
||||||
|
.getMultiple(post.body['attachments']?.cast<String>() ?? []),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reposting != null) {
|
||||||
|
final resp = await sn.client.get('/cgi/co/posts/$reposting');
|
||||||
|
final post = SnPost.fromJson(resp.data);
|
||||||
|
repostingPost = post.copyWith(
|
||||||
|
preload: SnPostPreload(
|
||||||
|
attachments: await attach
|
||||||
|
.getMultiple(post.body['attachments']?.cast<String>() ?? []),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (!context.mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
} finally {
|
||||||
|
isLoading = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> post(BuildContext context) async {
|
||||||
|
if (isBusy || publisher == null) return;
|
||||||
|
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
final attach = context.read<SnAttachmentProvider>();
|
||||||
|
|
||||||
|
progress = 0;
|
||||||
|
isBusy = true;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
// Uploading attachments
|
||||||
|
try {
|
||||||
|
for (int i = 0; i < attachments.length; i++) {
|
||||||
|
final media = attachments[i];
|
||||||
|
if (media.attachment != null) continue; // Already uploaded, skip
|
||||||
|
if (media.isEmpty) continue; // Nothing to do, skip
|
||||||
|
|
||||||
|
final place = await attach.chunkedUploadInitialize(
|
||||||
|
(await media.length())!,
|
||||||
|
media.name,
|
||||||
|
'interactive',
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
final item = await attach.chunkedUploadParts(
|
||||||
|
media.toFile()!,
|
||||||
|
place.$1,
|
||||||
|
place.$2,
|
||||||
|
onProgress: (progress) {
|
||||||
|
// Calculate overall progress for attachments
|
||||||
|
progress = ((i + progress) / attachments.length) *
|
||||||
|
kAttachmentProgressWeight;
|
||||||
|
notifyListeners();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
attachments[i] = PostWriteMedia(item);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
isBusy = false;
|
||||||
|
notifyListeners();
|
||||||
|
if (!context.mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
progress = kAttachmentProgressWeight;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
// Posting the content
|
||||||
|
try {
|
||||||
|
final baseProgressVal = progress!;
|
||||||
|
await sn.client.request(
|
||||||
|
[
|
||||||
|
'/cgi/co/$mode',
|
||||||
|
if (editingPost != null) '${editingPost!.id}',
|
||||||
|
].join('/'),
|
||||||
|
data: {
|
||||||
|
'publisher': publisher!.id,
|
||||||
|
'content': contentController.text,
|
||||||
|
if (titleController.text.isNotEmpty) 'title': titleController.text,
|
||||||
|
if (descriptionController.text.isNotEmpty)
|
||||||
|
'description': descriptionController.text,
|
||||||
|
'attachments': attachments
|
||||||
|
.where((e) => e.attachment != null)
|
||||||
|
.map((e) => e.attachment!.rid)
|
||||||
|
.toList(),
|
||||||
|
if (publishedAt != null)
|
||||||
|
'published_at': publishedAt!.toUtc().toIso8601String(),
|
||||||
|
if (publishedUntil != null)
|
||||||
|
'published_until': publishedAt!.toUtc().toIso8601String(),
|
||||||
|
if (replyingPost != null) 'reply_to': replyingPost!.id,
|
||||||
|
if (repostingPost != null) 'repost_to': repostingPost!.id,
|
||||||
|
},
|
||||||
|
onSendProgress: (count, total) {
|
||||||
|
progress =
|
||||||
|
baseProgressVal + (count / total) * (kPostingProgressWeight / 2);
|
||||||
|
notifyListeners();
|
||||||
|
},
|
||||||
|
onReceiveProgress: (count, total) {
|
||||||
|
progress = baseProgressVal +
|
||||||
|
(kPostingProgressWeight / 2) +
|
||||||
|
(count / total) * (kPostingProgressWeight / 2);
|
||||||
|
notifyListeners();
|
||||||
|
},
|
||||||
|
options: Options(
|
||||||
|
method: editingPost != null ? 'PUT' : 'POST',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
if (!context.mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
} finally {
|
||||||
|
isBusy = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void addAttachments(Iterable<PostWriteMedia> items) {
|
||||||
|
attachments.addAll(items);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setAttachmentAt(int idx, PostWriteMedia item) {
|
||||||
|
attachments[idx] = item;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeAttachmentAt(int idx) {
|
||||||
|
attachments.removeAt(idx);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setPublisher(SnPublisher? item) {
|
||||||
|
publisher = item;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setPublishedAt(DateTime? value) {
|
||||||
|
publishedAt = value;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setPublishedUntil(DateTime? value) {
|
||||||
|
publishedUntil = value;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setIsBusy(bool value) {
|
||||||
|
isBusy = value;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset() {
|
||||||
|
publishedAt = null;
|
||||||
|
publishedUntil = null;
|
||||||
|
titleController.clear();
|
||||||
|
descriptionController.clear();
|
||||||
|
contentController.clear();
|
||||||
|
attachments.clear();
|
||||||
|
editingPost = null;
|
||||||
|
replyingPost = null;
|
||||||
|
repostingPost = null;
|
||||||
|
mode = kTitleMap.keys.first;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
contentController.dispose();
|
||||||
|
titleController.dispose();
|
||||||
|
descriptionController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
import 'package:croppy/croppy.dart';
|
import 'package:croppy/croppy.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:easy_localization_loader/easy_localization_loader.dart';
|
import 'package:easy_localization_loader/easy_localization_loader.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:relative_time/relative_time.dart';
|
import 'package:relative_time/relative_time.dart';
|
||||||
import 'package:responsive_framework/responsive_framework.dart';
|
import 'package:responsive_framework/responsive_framework.dart';
|
||||||
|
import 'package:surface/providers/navigation.dart';
|
||||||
import 'package:surface/providers/sn_attachment.dart';
|
import 'package:surface/providers/sn_attachment.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
import 'package:surface/providers/theme.dart';
|
import 'package:surface/providers/theme.dart';
|
||||||
@@ -15,6 +17,10 @@ void main() async {
|
|||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
await EasyLocalization.ensureInitialized();
|
await EasyLocalization.ensureInitialized();
|
||||||
|
|
||||||
|
if (!kReleaseMode) {
|
||||||
|
debugInvertOversizedImages = true;
|
||||||
|
}
|
||||||
|
|
||||||
runApp(const SolianApp());
|
runApp(const SolianApp());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,28 +40,11 @@ class SolianApp extends StatelessWidget {
|
|||||||
providers: [
|
providers: [
|
||||||
Provider(create: (_) => SnNetworkProvider()),
|
Provider(create: (_) => SnNetworkProvider()),
|
||||||
Provider(create: (ctx) => SnAttachmentProvider(ctx)),
|
Provider(create: (ctx) => SnAttachmentProvider(ctx)),
|
||||||
|
ChangeNotifierProvider(create: (ctx) => NavigationProvider()),
|
||||||
ChangeNotifierProvider(create: (ctx) => UserProvider(ctx)),
|
ChangeNotifierProvider(create: (ctx) => UserProvider(ctx)),
|
||||||
ChangeNotifierProvider(create: (_) => ThemeProvider()),
|
ChangeNotifierProvider(create: (_) => ThemeProvider()),
|
||||||
],
|
],
|
||||||
child: Builder(builder: (context) {
|
child: AppMainContent(),
|
||||||
// Initialize some providers
|
|
||||||
context.read<UserProvider>();
|
|
||||||
|
|
||||||
final th = context.watch<ThemeProvider>();
|
|
||||||
|
|
||||||
return MaterialApp.router(
|
|
||||||
theme: th.theme.light,
|
|
||||||
darkTheme: th.theme.dark,
|
|
||||||
locale: context.locale,
|
|
||||||
supportedLocales: context.supportedLocales,
|
|
||||||
localizationsDelegates: [
|
|
||||||
CroppyLocalizations.delegate,
|
|
||||||
RelativeTimeLocalizations.delegate,
|
|
||||||
...context.localizationDelegates,
|
|
||||||
],
|
|
||||||
routerConfig: appRouter,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
breakpoints: [
|
breakpoints: [
|
||||||
@@ -66,3 +55,28 @@ class SolianApp extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class AppMainContent extends StatelessWidget {
|
||||||
|
const AppMainContent({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
context.read<NavigationProvider>();
|
||||||
|
context.read<UserProvider>();
|
||||||
|
|
||||||
|
final th = context.watch<ThemeProvider>();
|
||||||
|
|
||||||
|
return MaterialApp.router(
|
||||||
|
theme: th.theme?.light,
|
||||||
|
darkTheme: th.theme?.dark,
|
||||||
|
locale: context.locale,
|
||||||
|
supportedLocales: context.supportedLocales,
|
||||||
|
localizationsDelegates: [
|
||||||
|
CroppyLocalizations.delegate,
|
||||||
|
RelativeTimeLocalizations.delegate,
|
||||||
|
...context.localizationDelegates,
|
||||||
|
],
|
||||||
|
routerConfig: appRouter,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
112
lib/providers/navigation.dart
Normal file
112
lib/providers/navigation.dart
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
class AppNavDestination {
|
||||||
|
final String label;
|
||||||
|
final String screen;
|
||||||
|
final Widget icon;
|
||||||
|
final bool isPinned;
|
||||||
|
|
||||||
|
const AppNavDestination({
|
||||||
|
required this.label,
|
||||||
|
required this.screen,
|
||||||
|
required this.icon,
|
||||||
|
this.isPinned = false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class NavigationProvider extends ChangeNotifier {
|
||||||
|
int? _currentIndex;
|
||||||
|
|
||||||
|
int? get currentIndex => _currentIndex;
|
||||||
|
|
||||||
|
static const List<AppNavDestination> kAllDestination = [
|
||||||
|
AppNavDestination(
|
||||||
|
icon: Icon(Symbols.home, weight: 400, opticalSize: 20),
|
||||||
|
screen: 'home',
|
||||||
|
label: 'screenHome',
|
||||||
|
),
|
||||||
|
AppNavDestination(
|
||||||
|
icon: Icon(Symbols.explore, weight: 400, opticalSize: 20),
|
||||||
|
screen: 'explore',
|
||||||
|
label: 'screenExplore',
|
||||||
|
),
|
||||||
|
AppNavDestination(
|
||||||
|
icon: Icon(Symbols.account_circle, weight: 400, opticalSize: 20),
|
||||||
|
screen: 'account',
|
||||||
|
label: 'screenAccount',
|
||||||
|
),
|
||||||
|
AppNavDestination(
|
||||||
|
icon: Icon(Symbols.album, weight: 400, opticalSize: 20),
|
||||||
|
screen: 'album',
|
||||||
|
label: 'screenAlbum',
|
||||||
|
),
|
||||||
|
AppNavDestination(
|
||||||
|
icon: Icon(Symbols.chat, weight: 400, opticalSize: 20),
|
||||||
|
screen: 'chat',
|
||||||
|
label: 'screenChat',
|
||||||
|
),
|
||||||
|
];
|
||||||
|
static const List<String> kDefaultPinnedDestination = [
|
||||||
|
'home',
|
||||||
|
'explore',
|
||||||
|
'account'
|
||||||
|
];
|
||||||
|
|
||||||
|
List<AppNavDestination> destinations = [];
|
||||||
|
|
||||||
|
int get pinnedDestinationCount =>
|
||||||
|
destinations.where((ele) => ele.isPinned).length;
|
||||||
|
|
||||||
|
NavigationProvider() {
|
||||||
|
buildDestinations(kDefaultPinnedDestination);
|
||||||
|
SharedPreferences.getInstance().then((prefs) {
|
||||||
|
final pinned = prefs.getStringList("app_pinned_navigation");
|
||||||
|
if (pinned != null) buildDestinations(pinned);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void buildDestinations(List<String> pinned) {
|
||||||
|
destinations = kAllDestination
|
||||||
|
.map(
|
||||||
|
(ele) => AppNavDestination(
|
||||||
|
label: ele.label,
|
||||||
|
screen: ele.screen,
|
||||||
|
icon: ele.icon,
|
||||||
|
isPinned: pinned.contains(ele.screen),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
int getIndexInRange(int min, int max) {
|
||||||
|
return math.max(min, math.min(_currentIndex ?? 0, max));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isIndexInRange(int min, int max) {
|
||||||
|
return _currentIndex != null &&
|
||||||
|
_currentIndex! >= min &&
|
||||||
|
_currentIndex! < max;
|
||||||
|
}
|
||||||
|
|
||||||
|
void autoDetectIndex(GoRouter? state) {
|
||||||
|
if (state == null) return;
|
||||||
|
final idx = destinations.indexWhere(
|
||||||
|
(ele) =>
|
||||||
|
ele.screen ==
|
||||||
|
state.routerDelegate.currentConfiguration.last.route.name,
|
||||||
|
);
|
||||||
|
_currentIndex = idx == -1 ? null : idx;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setIndex(int idx) {
|
||||||
|
_currentIndex = idx;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
|
import 'dart:math' as math;
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
@@ -43,12 +44,19 @@ class SnAttachmentProvider {
|
|||||||
'take': pendingFetch.length,
|
'take': pendingFetch.length,
|
||||||
'id': pendingFetch.join(','),
|
'id': pendingFetch.join(','),
|
||||||
});
|
});
|
||||||
final out = resp.data['data'].map((e) => SnAttachment.fromJson(e)).toList();
|
final out = resp.data['data']
|
||||||
|
.where((e) => e['id'] != 0)
|
||||||
|
.map((e) => SnAttachment.fromJson(e))
|
||||||
|
.toList();
|
||||||
|
|
||||||
for (var i = 0; i < out.length; i++) {
|
for (final item in out) {
|
||||||
_cache[pendingFetch[i]] = out[i];
|
_cache[item.rid] = item;
|
||||||
}
|
}
|
||||||
return rids.map((rid) => _cache[rid]!).toList();
|
|
||||||
|
return rids
|
||||||
|
.where((rid) => _cache.containsKey(rid))
|
||||||
|
.map((rid) => _cache[rid]!)
|
||||||
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
static Map<String, String> mimetypeOverrides = {
|
static Map<String, String> mimetypeOverrides = {
|
||||||
@@ -165,7 +173,10 @@ class SnAttachmentProvider {
|
|||||||
for (final entry in chunks.entries) {
|
for (final entry in chunks.entries) {
|
||||||
queue.add(() async {
|
queue.add(() async {
|
||||||
final beginCursor = entry.value * chunkSize;
|
final beginCursor = entry.value * chunkSize;
|
||||||
final endCursor = (entry.value + 1) * chunkSize;
|
final endCursor = math.min<int>(
|
||||||
|
(entry.value + 1) * chunkSize,
|
||||||
|
await file.length(),
|
||||||
|
);
|
||||||
final data = Uint8List.fromList(await file
|
final data = Uint8List.fromList(await file
|
||||||
.openRead(beginCursor, endCursor)
|
.openRead(beginCursor, endCursor)
|
||||||
.expand((chunk) => chunk)
|
.expand((chunk) => chunk)
|
||||||
|
|||||||
@@ -4,25 +4,30 @@ import 'dart:developer';
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:dio_smart_retry/dio_smart_retry.dart';
|
import 'package:dio_smart_retry/dio_smart_retry.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:surface/providers/adapters/sn_network_universal.dart';
|
import 'package:surface/providers/adapters/sn_network_universal.dart';
|
||||||
|
|
||||||
const kUseLocalNetwork = true;
|
|
||||||
|
|
||||||
const kAtkStoreKey = 'nex_user_atk';
|
const kAtkStoreKey = 'nex_user_atk';
|
||||||
const kRtkStoreKey = 'nex_user_rtk';
|
const kRtkStoreKey = 'nex_user_rtk';
|
||||||
|
|
||||||
|
const kNetworkServerDefault = 'https://api.sn-next.solsynth.dev';
|
||||||
|
const kNetworkServerStoreKey = 'app_server_url';
|
||||||
|
|
||||||
|
const kNetworkServerDirectory = [
|
||||||
|
('SN Preview', 'https://api.sn-next.solsynth.dev'),
|
||||||
|
('SN Stable', 'https://api.sn.solsynth.dev'),
|
||||||
|
('Local', 'http://localhost:8001'),
|
||||||
|
];
|
||||||
|
|
||||||
class SnNetworkProvider {
|
class SnNetworkProvider {
|
||||||
late Dio client;
|
late Dio client;
|
||||||
|
|
||||||
|
late final SharedPreferences _prefs;
|
||||||
late final FlutterSecureStorage _storage = FlutterSecureStorage();
|
late final FlutterSecureStorage _storage = FlutterSecureStorage();
|
||||||
|
|
||||||
SnNetworkProvider() {
|
SnNetworkProvider() {
|
||||||
client = Dio();
|
client = Dio();
|
||||||
|
|
||||||
client.options.baseUrl = kUseLocalNetwork
|
|
||||||
? 'http://localhost:8001'
|
|
||||||
: 'https://api.sn.solsynth.dev';
|
|
||||||
|
|
||||||
client.interceptors.add(RetryInterceptor(
|
client.interceptors.add(RetryInterceptor(
|
||||||
dio: client,
|
dio: client,
|
||||||
retries: 3,
|
retries: 3,
|
||||||
@@ -86,10 +91,16 @@ class SnNetworkProvider {
|
|||||||
);
|
);
|
||||||
|
|
||||||
client = addClientAdapter(client);
|
client = addClientAdapter(client);
|
||||||
|
|
||||||
|
SharedPreferences.getInstance().then((prefs) {
|
||||||
|
_prefs = prefs;
|
||||||
|
client.options.baseUrl =
|
||||||
|
_prefs.getString(kNetworkServerStoreKey) ?? kNetworkServerDefault;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
String getAttachmentUrl(String ky) {
|
String getAttachmentUrl(String ky) {
|
||||||
if (ky.startsWith("http://")) return ky;
|
if (ky.startsWith("http")) return ky;
|
||||||
return '${client.options.baseUrl}/cgi/uc/attachments/$ky';
|
return '${client.options.baseUrl}/cgi/uc/attachments/$ky';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,9 +123,7 @@ class SnNetworkProvider {
|
|||||||
if (rtk == null) return null;
|
if (rtk == null) return null;
|
||||||
|
|
||||||
final dio = Dio();
|
final dio = Dio();
|
||||||
dio.options.baseUrl = kUseLocalNetwork
|
dio.options.baseUrl = client.options.baseUrl;
|
||||||
? 'http://localhost:8001'
|
|
||||||
: 'https://api.sn.solsynth.dev';
|
|
||||||
|
|
||||||
final resp = await dio.post('/cgi/id/auth/token', data: {
|
final resp = await dio.post('/cgi/id/auth/token', data: {
|
||||||
'grant_type': 'refresh_token',
|
'grant_type': 'refresh_token',
|
||||||
@@ -127,4 +136,8 @@ class SnNetworkProvider {
|
|||||||
|
|
||||||
return atk;
|
return atk;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setBaseUrl(String url) {
|
||||||
|
client.options.baseUrl = url;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,19 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:surface/theme.dart';
|
import 'package:surface/theme.dart';
|
||||||
|
|
||||||
class ThemeProvider extends ChangeNotifier {
|
class ThemeProvider extends ChangeNotifier {
|
||||||
late ThemeSet theme;
|
ThemeSet? theme;
|
||||||
|
|
||||||
ThemeProvider() {
|
ThemeProvider() {
|
||||||
theme = createAppThemeSet();
|
createAppThemeSet().then((value) {
|
||||||
|
theme = value;
|
||||||
|
notifyListeners();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void reloadTheme({bool? useMaterial3}) {
|
||||||
|
createAppThemeSet().then((value) {
|
||||||
|
theme = value;
|
||||||
|
notifyListeners();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,16 @@ import 'package:surface/screens/account/profile_edit.dart';
|
|||||||
import 'package:surface/screens/account/publishers/publisher_edit.dart';
|
import 'package:surface/screens/account/publishers/publisher_edit.dart';
|
||||||
import 'package:surface/screens/account/publishers/publisher_new.dart';
|
import 'package:surface/screens/account/publishers/publisher_new.dart';
|
||||||
import 'package:surface/screens/account/publishers/publishers.dart';
|
import 'package:surface/screens/account/publishers/publishers.dart';
|
||||||
|
import 'package:surface/screens/album.dart';
|
||||||
import 'package:surface/screens/auth/login.dart';
|
import 'package:surface/screens/auth/login.dart';
|
||||||
import 'package:surface/screens/auth/register.dart';
|
import 'package:surface/screens/auth/register.dart';
|
||||||
|
import 'package:surface/screens/chat.dart';
|
||||||
import 'package:surface/screens/explore.dart';
|
import 'package:surface/screens/explore.dart';
|
||||||
import 'package:surface/screens/home.dart';
|
import 'package:surface/screens/home.dart';
|
||||||
|
import 'package:surface/screens/post/post_detail.dart';
|
||||||
import 'package:surface/screens/post/post_editor.dart';
|
import 'package:surface/screens/post/post_editor.dart';
|
||||||
|
import 'package:surface/screens/settings.dart';
|
||||||
|
import 'package:surface/types/post.dart';
|
||||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
|
|
||||||
final appRouter = GoRouter(
|
final appRouter = GoRouter(
|
||||||
@@ -17,6 +22,7 @@ final appRouter = GoRouter(
|
|||||||
builder: (context, state, child) => AppScaffold(
|
builder: (context, state, child) => AppScaffold(
|
||||||
body: child,
|
body: child,
|
||||||
showBottomNavigation: true,
|
showBottomNavigation: true,
|
||||||
|
showDrawer: true,
|
||||||
),
|
),
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
@@ -34,6 +40,16 @@ final appRouter = GoRouter(
|
|||||||
name: 'account',
|
name: 'account',
|
||||||
builder: (context, state) => const AccountScreen(),
|
builder: (context, state) => const AccountScreen(),
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/chat',
|
||||||
|
name: 'chat',
|
||||||
|
builder: (context, state) => const ChatScreen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/album',
|
||||||
|
name: 'album',
|
||||||
|
builder: (context, state) => const AlbumScreen(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
ShellRoute(
|
ShellRoute(
|
||||||
@@ -57,12 +73,21 @@ final appRouter = GoRouter(
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/post/:slug',
|
||||||
|
name: 'postDetail',
|
||||||
|
builder: (context, state) => PostDetailScreen(
|
||||||
|
slug: state.pathParameters['slug']!,
|
||||||
|
preload: state.extra as SnPost?,
|
||||||
|
),
|
||||||
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
ShellRoute(
|
ShellRoute(
|
||||||
builder: (context, state, child) => AppScaffold(
|
builder: (context, state, child) => AppScaffold(
|
||||||
body: child,
|
body: child,
|
||||||
autoImplyAppBar: true,
|
autoImplyAppBar: true,
|
||||||
|
showDrawer: true,
|
||||||
),
|
),
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
@@ -99,5 +124,18 @@ final appRouter = GoRouter(
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
ShellRoute(
|
||||||
|
builder: (context, state, child) => AppScaffold(
|
||||||
|
body: child,
|
||||||
|
autoImplyAppBar: true,
|
||||||
|
),
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: '/settings',
|
||||||
|
name: 'settings',
|
||||||
|
builder: (context, state) => const SettingsScreen(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -20,6 +20,14 @@ class AccountScreen extends StatelessWidget {
|
|||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text("screenAccount").tr(),
|
title: Text("screenAccount").tr(),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Symbols.settings, fill: 1),
|
||||||
|
onPressed: () {
|
||||||
|
GoRouter.of(context).pushNamed('settings');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
body: SingleChildScrollView(
|
body: SingleChildScrollView(
|
||||||
child: ua.isAuthorized
|
child: ua.isAuthorized
|
||||||
|
|||||||
@@ -232,7 +232,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
|||||||
color:
|
color:
|
||||||
Theme.of(context).colorScheme.surfaceContainerHigh,
|
Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
child: _banner != null
|
child: _banner != null
|
||||||
? UniversalImage(
|
? AutoResizeUniversalImage(
|
||||||
sn.getAttachmentUrl(_banner!),
|
sn.getAttachmentUrl(_banner!),
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ class _AccountPublisherEditScreenState
|
|||||||
.colorScheme
|
.colorScheme
|
||||||
.surfaceContainerHigh,
|
.surfaceContainerHigh,
|
||||||
child: _banner != null
|
child: _banner != null
|
||||||
? UniversalImage(
|
? AutoResizeUniversalImage(
|
||||||
sn.getAttachmentUrl(_banner!),
|
sn.getAttachmentUrl(_banner!),
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
)
|
)
|
||||||
|
|||||||
10
lib/screens/album.dart
Normal file
10
lib/screens/album.dart
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class AlbumScreen extends StatelessWidget {
|
||||||
|
const AlbumScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const Placeholder();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -73,7 +73,7 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
onTicket: (p0) => setState(() {
|
onTicket: (p0) => setState(() {
|
||||||
_currentTicket = p0;
|
_currentTicket = p0;
|
||||||
}),
|
}),
|
||||||
onNext: (p0) => setState(() {
|
onNext: () => setState(() {
|
||||||
_period = 1;
|
_period = 1;
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -271,13 +271,14 @@ class _LoginPickerScreenState extends State<_LoginPickerScreen> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Request one-time-password code
|
// Request one-time-password code
|
||||||
sn.client.post('/cgi/id/auth/factors/$_factorPicked');
|
await sn.client.post('/cgi/id/auth/factors/$_factorPicked');
|
||||||
widget.onPickFactor(
|
widget.onPickFactor(
|
||||||
widget.factors!.where((x) => x.id == _factorPicked).first,
|
widget.factors!.where((x) => x.id == _factorPicked).first,
|
||||||
);
|
);
|
||||||
widget.onNext();
|
widget.onNext();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
context.showErrorDialog(err);
|
// ignore: use_build_context_synchronously
|
||||||
|
if (context.mounted) context.showErrorDialog(err);
|
||||||
return;
|
return;
|
||||||
} finally {
|
} finally {
|
||||||
setState(() => _isBusy = false);
|
setState(() => _isBusy = false);
|
||||||
|
|||||||
10
lib/screens/chat.dart
Normal file
10
lib/screens/chat.dart
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class ChatScreen extends StatelessWidget {
|
||||||
|
const ChatScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const Placeholder();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -168,13 +168,23 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
|||||||
SliverInfiniteList(
|
SliverInfiniteList(
|
||||||
itemCount: _posts.length,
|
itemCount: _posts.length,
|
||||||
isLoading: _isBusy,
|
isLoading: _isBusy,
|
||||||
|
centerLoading: true,
|
||||||
hasReachedMax: _postCount != null && _posts.length >= _postCount!,
|
hasReachedMax: _postCount != null && _posts.length >= _postCount!,
|
||||||
onFetchData: _fetchPosts,
|
onFetchData: _fetchPosts,
|
||||||
itemBuilder: (context, idx) {
|
itemBuilder: (context, idx) {
|
||||||
return PostItem(data: _posts[idx]);
|
return GestureDetector(
|
||||||
|
child: PostItem(data: _posts[idx]),
|
||||||
|
onTap: () {
|
||||||
|
GoRouter.of(context).pushNamed(
|
||||||
|
'postDetail',
|
||||||
|
pathParameters: {'slug': _posts[idx].id.toString()},
|
||||||
|
extra: _posts[idx],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
separatorBuilder: (context, index) => const Divider(),
|
separatorBuilder: (context, index) => const Divider(height: 1),
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
167
lib/screens/post/post_detail.dart
Normal file
167
lib/screens/post/post_detail.dart
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:surface/providers/sn_attachment.dart';
|
||||||
|
import 'package:surface/providers/sn_network.dart';
|
||||||
|
import 'package:surface/types/post.dart';
|
||||||
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
import 'package:surface/widgets/loading_indicator.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
|
import 'package:surface/widgets/post/post_comment_list.dart';
|
||||||
|
import 'package:surface/widgets/post/post_item.dart';
|
||||||
|
import 'package:surface/widgets/post/post_mini_editor.dart';
|
||||||
|
|
||||||
|
class PostDetailScreen extends StatefulWidget {
|
||||||
|
final String slug;
|
||||||
|
final SnPost? preload;
|
||||||
|
const PostDetailScreen({
|
||||||
|
super.key,
|
||||||
|
required this.slug,
|
||||||
|
this.preload,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PostDetailScreen> createState() => _PostDetailScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PostDetailScreenState extends State<PostDetailScreen> {
|
||||||
|
bool _isBusy = false;
|
||||||
|
|
||||||
|
SnPost? _data;
|
||||||
|
|
||||||
|
void _fetchPost() async {
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
final attach = context.read<SnAttachmentProvider>();
|
||||||
|
final resp = await sn.client.get('/cgi/co/posts/${widget.slug}');
|
||||||
|
if (!mounted) return;
|
||||||
|
final attachments = await attach.getMultiple(
|
||||||
|
resp.data['body']['attachments']?.cast<String>() ?? [],
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
_data = SnPost.fromJson(resp.data).copyWith(
|
||||||
|
preload: SnPostPreload(
|
||||||
|
attachments: attachments,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
} finally {
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
if (widget.preload != null) {
|
||||||
|
_data = widget.preload;
|
||||||
|
}
|
||||||
|
_fetchPost();
|
||||||
|
}
|
||||||
|
|
||||||
|
final GlobalKey<PostCommentSliverListState> _childListKey = GlobalKey();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||||
|
|
||||||
|
return AppScaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: BackButton(
|
||||||
|
onPressed: () {
|
||||||
|
if (GoRouter.of(context).canPop()) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
GoRouter.of(context).replaceNamed('explore');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
flexibleSpace: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
if (_data?.body['title'] != null)
|
||||||
|
Text(_data?.body['title'] ?? 'postNoun'.tr())
|
||||||
|
.textStyle(Theme.of(context).textTheme.titleLarge!)
|
||||||
|
.textColor(Colors.white),
|
||||||
|
if (_data?.body['title'] != null)
|
||||||
|
Text('postDetail'.tr())
|
||||||
|
.textColor(Colors.white.withAlpha((255 * 0.9).round()))
|
||||||
|
else
|
||||||
|
Text('postDetail'.tr())
|
||||||
|
.textStyle(Theme.of(context).textTheme.titleLarge!)
|
||||||
|
.textColor(Colors.white),
|
||||||
|
],
|
||||||
|
).padding(top: math.max(MediaQuery.of(context).padding.top, 8)),
|
||||||
|
),
|
||||||
|
body: CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: LoadingIndicator(isActive: _isBusy),
|
||||||
|
),
|
||||||
|
if (_data != null)
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: PostItem(
|
||||||
|
data: _data!,
|
||||||
|
showComments: false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SliverToBoxAdapter(child: Divider(height: 1)),
|
||||||
|
if (_data != null)
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.comment, size: 24),
|
||||||
|
const Gap(16),
|
||||||
|
Text('postCommentsDetailed')
|
||||||
|
.plural(_data!.metric.replyCount)
|
||||||
|
.textStyle(Theme.of(context).textTheme.titleLarge!),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 20, vertical: 12),
|
||||||
|
),
|
||||||
|
if (_data != null)
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Container(
|
||||||
|
height: 240,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.symmetric(
|
||||||
|
horizontal: BorderSide(
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
width: 1 / devicePixelRatio,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: PostMiniEditor(
|
||||||
|
postReplyId: _data!.id,
|
||||||
|
onPost: () {
|
||||||
|
_childListKey.currentState!.refresh();
|
||||||
|
setState(() {
|
||||||
|
_data = _data!.copyWith(
|
||||||
|
metric: _data!.metric.copyWith(
|
||||||
|
replyCount: _data!.metric.replyCount + 1,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_data != null)
|
||||||
|
PostCommentSliverList(
|
||||||
|
key: _childListKey,
|
||||||
|
parentPostId: _data!.id,
|
||||||
|
),
|
||||||
|
SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
@@ -8,9 +10,8 @@ import 'package:go_router/go_router.dart';
|
|||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:surface/providers/sn_attachment.dart';
|
import 'package:surface/controllers/post_write_controller.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
import 'package:surface/types/attachment.dart';
|
|
||||||
import 'package:surface/types/post.dart';
|
import 'package:surface/types/post.dart';
|
||||||
import 'package:surface/widgets/account/account_image.dart';
|
import 'package:surface/widgets/account/account_image.dart';
|
||||||
import 'package:surface/widgets/loading_indicator.dart';
|
import 'package:surface/widgets/loading_indicator.dart';
|
||||||
@@ -39,203 +40,37 @@ class PostEditorScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _PostEditorScreenState extends State<PostEditorScreen> {
|
class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||||
static const Map<String, String> _kTitleMap = {
|
final PostWriteController _writeController = PostWriteController();
|
||||||
'stories': 'writePostTypeStory',
|
|
||||||
'articles': 'writePostTypeArticle',
|
|
||||||
};
|
|
||||||
|
|
||||||
bool _isBusy = false;
|
bool _isFetching = false;
|
||||||
bool _isLoading = false;
|
bool get _isLoading => _isFetching || _writeController.isLoading;
|
||||||
|
|
||||||
SnPublisher? _publisher;
|
|
||||||
List<SnPublisher>? _publishers;
|
List<SnPublisher>? _publishers;
|
||||||
|
|
||||||
final List<XFile> _selectedMedia = List.empty(growable: true);
|
|
||||||
final List<SnAttachment> _attachments = List.empty(growable: true);
|
|
||||||
|
|
||||||
Future<void> _fetchPublishers() async {
|
Future<void> _fetchPublishers() async {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
setState(() => _isFetching = true);
|
||||||
final resp = await sn.client.get('/cgi/co/publishers');
|
|
||||||
_publishers = List<SnPublisher>.from(
|
|
||||||
resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [],
|
|
||||||
);
|
|
||||||
setState(() {
|
|
||||||
_publisher = _publishers?.first;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
SnPost? _editingOg;
|
|
||||||
SnPost? _replyingTo;
|
|
||||||
SnPost? _repostingTo;
|
|
||||||
|
|
||||||
Future<void> _fetchRelatedPost() async {
|
|
||||||
final sn = context.read<SnNetworkProvider>();
|
|
||||||
final attach = context.read<SnAttachmentProvider>();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setState(() => _isLoading = true);
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
final resp = await sn.client.get('/cgi/co/publishers');
|
||||||
if (widget.postEditId != null) {
|
_publishers = List<SnPublisher>.from(
|
||||||
final resp = await sn.client.get('/cgi/co/posts/${widget.postEditId}');
|
resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [],
|
||||||
final post = SnPost.fromJson(resp.data);
|
|
||||||
final attachments = await attach
|
|
||||||
.getMultiple(post.body['attachments']?.cast<String>() ?? []);
|
|
||||||
_title = post.body['title'];
|
|
||||||
_description = post.body['description'];
|
|
||||||
_contentController.text = post.body['content'] ?? '';
|
|
||||||
_attachments.addAll(attachments);
|
|
||||||
|
|
||||||
_editingOg = post.copyWith(
|
|
||||||
preload: SnPostPreload(
|
|
||||||
attachments: attachments,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (widget.postReplyId != null) {
|
|
||||||
final resp = await sn.client.get('/cgi/co/posts/${widget.postReplyId}');
|
|
||||||
final post = SnPost.fromJson(resp.data);
|
|
||||||
_replyingTo = post.copyWith(
|
|
||||||
preload: SnPostPreload(
|
|
||||||
attachments: await attach
|
|
||||||
.getMultiple(post.body['attachments']?.cast<String>() ?? []),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (widget.postRepostId != null) {
|
|
||||||
final resp =
|
|
||||||
await sn.client.get('/cgi/co/posts/${widget.postRepostId}');
|
|
||||||
final post = SnPost.fromJson(resp.data);
|
|
||||||
_repostingTo = post.copyWith(
|
|
||||||
preload: SnPostPreload(
|
|
||||||
attachments: await attach
|
|
||||||
.getMultiple(post.body['attachments']?.cast<String>() ?? []),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
context.showErrorDialog(err);
|
|
||||||
} finally {
|
|
||||||
setState(() => _isLoading = false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String? _title;
|
|
||||||
String? _description;
|
|
||||||
|
|
||||||
final TextEditingController _contentController = TextEditingController();
|
|
||||||
|
|
||||||
double? _progress;
|
|
||||||
|
|
||||||
static const kAttachmentProgressWeight = 0.9;
|
|
||||||
static const kPostingProgressWeight = 0.1;
|
|
||||||
|
|
||||||
void _performAction() async {
|
|
||||||
if (_isBusy || _publisher == null) return;
|
|
||||||
|
|
||||||
final sn = context.read<SnNetworkProvider>();
|
|
||||||
final attach = context.read<SnAttachmentProvider>();
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_progress = 0;
|
|
||||||
_isBusy = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Uploading attachments
|
|
||||||
try {
|
|
||||||
for (int i = 0; i < _selectedMedia.length; i++) {
|
|
||||||
final media = _selectedMedia[i];
|
|
||||||
final place = await attach.chunkedUploadInitialize(
|
|
||||||
await media.length(),
|
|
||||||
media.name,
|
|
||||||
'interactive',
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
|
|
||||||
final item = await attach.chunkedUploadParts(
|
|
||||||
media,
|
|
||||||
place.$1,
|
|
||||||
place.$2,
|
|
||||||
onProgress: (progress) {
|
|
||||||
// Calculate overall progress for attachments
|
|
||||||
setState(() {
|
|
||||||
_progress = ((i + progress) / _selectedMedia.length) *
|
|
||||||
kAttachmentProgressWeight;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
_attachments.add(item);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
if (!mounted) return;
|
|
||||||
context.showErrorDialog(err);
|
|
||||||
setState(() => _isBusy = false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(() => _progress = kAttachmentProgressWeight);
|
|
||||||
|
|
||||||
// Posting the content
|
|
||||||
try {
|
|
||||||
final baseProgressVal = _progress!;
|
|
||||||
await sn.client.request(
|
|
||||||
[
|
|
||||||
'/cgi/co/${widget.mode}',
|
|
||||||
if (widget.postEditId != null) '${widget.postEditId}',
|
|
||||||
].join('/'),
|
|
||||||
data: {
|
|
||||||
'publisher': _publisher!.id,
|
|
||||||
'content': _contentController.value.text,
|
|
||||||
'title': _title,
|
|
||||||
'description': _description,
|
|
||||||
'attachments': _attachments.map((e) => e.rid).toList(),
|
|
||||||
if (_replyingTo != null) 'reply_to': _replyingTo!.id,
|
|
||||||
if (_repostingTo != null) 'repost_to': _repostingTo!.id,
|
|
||||||
},
|
|
||||||
onSendProgress: (count, total) {
|
|
||||||
setState(() {
|
|
||||||
_progress = baseProgressVal +
|
|
||||||
(count / total) * (kPostingProgressWeight / 2);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onReceiveProgress: (count, total) {
|
|
||||||
setState(() {
|
|
||||||
_progress = baseProgressVal +
|
|
||||||
(kPostingProgressWeight / 2) +
|
|
||||||
(count / total) * (kPostingProgressWeight / 2);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
options: Options(
|
|
||||||
method: widget.postEditId != null ? 'PUT' : 'POST',
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
if (!mounted) return;
|
_writeController.setPublisher(_publishers?.firstOrNull);
|
||||||
Navigator.pop(context, true);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showErrorDialog(err);
|
context.showErrorDialog(err);
|
||||||
} finally {
|
} finally {
|
||||||
setState(() => _isBusy = false);
|
setState(() => _isFetching = false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateMeta() {
|
void _updateMeta() {
|
||||||
showModalBottomSheet<PostMetaResult?>(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => PostMetaEditor(
|
builder: (context) => PostMetaEditor(controller: _writeController),
|
||||||
initialTitle: _title,
|
|
||||||
initialDescription: _description,
|
|
||||||
),
|
|
||||||
useRootNavigator: true,
|
useRootNavigator: true,
|
||||||
).then((value) {
|
);
|
||||||
if (value is PostMetaResult) {
|
|
||||||
_title = value.title;
|
|
||||||
_description = value.description;
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final _imagePicker = ImagePicker();
|
final _imagePicker = ImagePicker();
|
||||||
@@ -243,299 +78,339 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
|||||||
void _selectMedia() async {
|
void _selectMedia() async {
|
||||||
final result = await _imagePicker.pickMultipleMedia();
|
final result = await _imagePicker.pickMultipleMedia();
|
||||||
if (result.isEmpty) return;
|
if (result.isEmpty) return;
|
||||||
_selectedMedia.addAll(result);
|
_writeController.addAttachments(
|
||||||
|
result.map((e) => PostWriteMedia.fromFile(e)),
|
||||||
|
);
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_contentController.dispose();
|
_writeController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
if (!_kTitleMap.keys.contains(widget.mode)) {
|
if (!PostWriteController.kTitleMap.keys.contains(widget.mode)) {
|
||||||
context.showErrorDialog('Unknown post type');
|
context.showErrorDialog('Unknown post type');
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
}
|
}
|
||||||
_fetchRelatedPost();
|
|
||||||
_fetchPublishers();
|
_fetchPublishers();
|
||||||
|
_writeController.fetchRelatedPost(
|
||||||
|
context,
|
||||||
|
editing: widget.postEditId,
|
||||||
|
replying: widget.postReplyId,
|
||||||
|
reposting: widget.postRepostId,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AppScaffold(
|
return ListenableBuilder(
|
||||||
appBar: AppBar(
|
listenable: _writeController,
|
||||||
leading: BackButton(
|
builder: (context, _) {
|
||||||
onPressed: () {
|
return AppScaffold(
|
||||||
Navigator.pop(context);
|
appBar: AppBar(
|
||||||
},
|
leading: BackButton(
|
||||||
),
|
onPressed: () {
|
||||||
flexibleSpace: Column(
|
Navigator.pop(context);
|
||||||
children: [
|
},
|
||||||
Text(_title ?? 'Untitled')
|
),
|
||||||
.textStyle(Theme.of(context).textTheme.titleLarge!)
|
flexibleSpace: Column(
|
||||||
.textColor(Colors.white),
|
children: [
|
||||||
Text(_kTitleMap[widget.mode]!)
|
Text(_writeController.title.isNotEmpty
|
||||||
.tr()
|
? _writeController.title
|
||||||
.textColor(Colors.white.withAlpha((255 * 0.9).round())),
|
: 'untitled'.tr())
|
||||||
],
|
.textStyle(Theme.of(context).textTheme.titleLarge!)
|
||||||
).padding(top: MediaQuery.of(context).padding.top),
|
.textColor(Colors.white),
|
||||||
actions: [
|
Text(PostWriteController.kTitleMap[widget.mode]!)
|
||||||
IconButton(
|
.tr()
|
||||||
icon: const Icon(Symbols.tune),
|
.textColor(Colors.white.withAlpha((255 * 0.9).round())),
|
||||||
onPressed: _isBusy ? null : _updateMeta,
|
],
|
||||||
|
).padding(top: math.max(MediaQuery.of(context).padding.top, 8)),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Symbols.tune),
|
||||||
|
onPressed: _writeController.isBusy ? null : _updateMeta,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
body: Column(
|
||||||
),
|
children: [
|
||||||
body: Column(
|
DropdownButtonHideUnderline(
|
||||||
children: [
|
child: DropdownButton2<SnPublisher>(
|
||||||
DropdownButtonHideUnderline(
|
isExpanded: true,
|
||||||
child: DropdownButton2<SnPublisher>(
|
hint: Text(
|
||||||
isExpanded: true,
|
'fieldPostPublisher',
|
||||||
hint: Text(
|
style: TextStyle(
|
||||||
'fieldPostPublisher',
|
fontSize: 14,
|
||||||
style: TextStyle(
|
color: Theme.of(context).hintColor,
|
||||||
fontSize: 14,
|
),
|
||||||
color: Theme.of(context).hintColor,
|
).tr(),
|
||||||
|
items: <DropdownMenuItem<SnPublisher>>[
|
||||||
|
...(_publishers?.map(
|
||||||
|
(item) => DropdownMenuItem<SnPublisher>(
|
||||||
|
enabled: _writeController.editingPost == null,
|
||||||
|
value: item,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
AccountImage(content: item.avatar, radius: 16),
|
||||||
|
const Gap(8),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(item.nick).textStyle(
|
||||||
|
Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodyMedium!),
|
||||||
|
Text('@${item.name}')
|
||||||
|
.textStyle(Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodySmall!)
|
||||||
|
.fontSize(12),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
) ??
|
||||||
|
[]),
|
||||||
|
DropdownMenuItem<SnPublisher>(
|
||||||
|
value: null,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
CircleAvatar(
|
||||||
|
radius: 16,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
foregroundColor:
|
||||||
|
Theme.of(context).colorScheme.onSurface,
|
||||||
|
child: const Icon(Symbols.add),
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('publishersNew').tr().textStyle(
|
||||||
|
Theme.of(context).textTheme.bodyMedium!),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
value: _writeController.publisher,
|
||||||
|
onChanged: (SnPublisher? value) {
|
||||||
|
if (value == null) {
|
||||||
|
GoRouter.of(context)
|
||||||
|
.pushNamed('accountPublisherNew')
|
||||||
|
.then((value) {
|
||||||
|
if (value == true) {
|
||||||
|
_publishers = null;
|
||||||
|
_fetchPublishers();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
_writeController.setPublisher(value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
buttonStyleData: const ButtonStyleData(
|
||||||
|
padding: EdgeInsets.only(right: 16),
|
||||||
|
height: 48,
|
||||||
|
),
|
||||||
|
menuItemStyleData: const MenuItemStyleData(
|
||||||
|
height: 48,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
).tr(),
|
),
|
||||||
items: <DropdownMenuItem<SnPublisher>>[
|
const Divider(height: 1),
|
||||||
...(_publishers?.map(
|
Expanded(
|
||||||
(item) => DropdownMenuItem<SnPublisher>(
|
child: SingleChildScrollView(
|
||||||
enabled: _editingOg == null,
|
padding: EdgeInsets.only(bottom: 8),
|
||||||
value: item,
|
child: Column(
|
||||||
child: Row(
|
children: [
|
||||||
|
// Replying Notice
|
||||||
|
if (_writeController.replyingPost != null)
|
||||||
|
Column(
|
||||||
children: [
|
children: [
|
||||||
AccountImage(content: item.avatar, radius: 16),
|
Theme(
|
||||||
const Gap(8),
|
data: Theme.of(context).copyWith(
|
||||||
Expanded(
|
dividerColor: Colors.transparent,
|
||||||
child: Column(
|
),
|
||||||
mainAxisSize: MainAxisSize.min,
|
child: ExpansionTile(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
minTileHeight: 48,
|
||||||
children: [
|
leading:
|
||||||
Text(item.nick).textStyle(
|
const Icon(Symbols.reply).padding(left: 4),
|
||||||
Theme.of(context).textTheme.bodyMedium!),
|
title: Text('postReplyingNotice')
|
||||||
Text('@${item.name}')
|
.fontSize(15)
|
||||||
.textStyle(Theme.of(context)
|
.tr(args: [
|
||||||
.textTheme
|
'@${_writeController.replyingPost!.publisher.name}'
|
||||||
.bodySmall!)
|
]),
|
||||||
.fontSize(12),
|
children: <Widget>[
|
||||||
|
PostItem(data: _writeController.replyingPost!)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
// Reposting Notice
|
||||||
) ??
|
if (_writeController.repostingPost != null)
|
||||||
[]),
|
Column(
|
||||||
DropdownMenuItem<SnPublisher>(
|
|
||||||
value: null,
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
CircleAvatar(
|
|
||||||
radius: 16,
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
foregroundColor:
|
|
||||||
Theme.of(context).colorScheme.onSurface,
|
|
||||||
child: const Icon(Symbols.add),
|
|
||||||
),
|
|
||||||
const Gap(8),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
children: [
|
||||||
Text('publishersNew').tr().textStyle(
|
Theme(
|
||||||
Theme.of(context).textTheme.bodyMedium!),
|
data: Theme.of(context).copyWith(
|
||||||
|
dividerColor: Colors.transparent,
|
||||||
|
),
|
||||||
|
child: ExpansionTile(
|
||||||
|
minTileHeight: 48,
|
||||||
|
leading: const Icon(Symbols.forward)
|
||||||
|
.padding(left: 4),
|
||||||
|
title: Text('postRepostingNotice')
|
||||||
|
.fontSize(15)
|
||||||
|
.tr(args: [
|
||||||
|
'@${_writeController.repostingPost!.publisher.name}'
|
||||||
|
]),
|
||||||
|
children: <Widget>[
|
||||||
|
PostItem(
|
||||||
|
data: _writeController.repostingPost!)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
// Editing Notice
|
||||||
|
if (_writeController.editingPost != null)
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
Theme(
|
||||||
|
data: Theme.of(context).copyWith(
|
||||||
|
dividerColor: Colors.transparent,
|
||||||
|
),
|
||||||
|
child: ExpansionTile(
|
||||||
|
minTileHeight: 48,
|
||||||
|
leading: const Icon(Symbols.edit_note)
|
||||||
|
.padding(left: 4),
|
||||||
|
title: Text('postEditingNotice')
|
||||||
|
.fontSize(15)
|
||||||
|
.tr(args: [
|
||||||
|
'@${_writeController.editingPost!.publisher.name}'
|
||||||
|
]),
|
||||||
|
children: <Widget>[
|
||||||
|
PostItem(data: _writeController.editingPost!)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
// Content Input Area
|
||||||
|
TextField(
|
||||||
|
controller: _writeController.contentController,
|
||||||
|
maxLines: null,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'fieldPostContent'.tr(),
|
||||||
|
hintStyle: TextStyle(fontSize: 14),
|
||||||
|
isCollapsed: true,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
),
|
||||||
|
border: InputBorder.none,
|
||||||
|
),
|
||||||
|
onTapOutside: (_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
),
|
),
|
||||||
],
|
]
|
||||||
|
.expandIndexed(
|
||||||
|
(idx, ele) => [
|
||||||
|
if (idx != 0 || _writeController.isRelatedNull)
|
||||||
|
const Gap(8),
|
||||||
|
ele,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
|
||||||
value: _publisher,
|
|
||||||
onChanged: (SnPublisher? value) {
|
|
||||||
if (value == null) {
|
|
||||||
GoRouter.of(context)
|
|
||||||
.pushNamed('accountPublisherNew')
|
|
||||||
.then((value) {
|
|
||||||
if (value == true) {
|
|
||||||
_publisher = null;
|
|
||||||
_publishers = null;
|
|
||||||
_fetchPublishers();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setState(() {
|
|
||||||
_publisher = value;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
buttonStyleData: const ButtonStyleData(
|
|
||||||
padding: EdgeInsets.only(right: 16),
|
|
||||||
height: 48,
|
|
||||||
),
|
),
|
||||||
menuItemStyleData: const MenuItemStyleData(
|
if (_writeController.attachments.isNotEmpty)
|
||||||
height: 48,
|
PostMediaPendingList(
|
||||||
),
|
controller: _writeController,
|
||||||
),
|
).padding(bottom: 8),
|
||||||
),
|
Material(
|
||||||
const Divider(height: 1),
|
elevation: 2,
|
||||||
Expanded(
|
child: Column(
|
||||||
child: SingleChildScrollView(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
padding: EdgeInsets.only(bottom: 8),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
// Replying Notice
|
|
||||||
if (_replyingTo != null)
|
|
||||||
Column(
|
|
||||||
children: [
|
|
||||||
Theme(
|
|
||||||
data: Theme.of(context).copyWith(
|
|
||||||
dividerColor: Colors.transparent,
|
|
||||||
),
|
|
||||||
child: ExpansionTile(
|
|
||||||
minTileHeight: 48,
|
|
||||||
leading: const Icon(Symbols.reply).padding(left: 4),
|
|
||||||
title: Text('postReplyingNotice')
|
|
||||||
.fontSize(15)
|
|
||||||
.tr(args: ['@${_replyingTo!.publisher.name}']),
|
|
||||||
children: <Widget>[PostItem(data: _replyingTo!)],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Divider(height: 1),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
// Reposting Notice
|
|
||||||
if (_repostingTo != null)
|
|
||||||
Column(
|
|
||||||
children: [
|
|
||||||
Theme(
|
|
||||||
data: Theme.of(context).copyWith(
|
|
||||||
dividerColor: Colors.transparent,
|
|
||||||
),
|
|
||||||
child: ExpansionTile(
|
|
||||||
minTileHeight: 48,
|
|
||||||
leading:
|
|
||||||
const Icon(Symbols.forward).padding(left: 4),
|
|
||||||
title: Text('postRepostingNotice')
|
|
||||||
.fontSize(15)
|
|
||||||
.tr(args: ['@${_repostingTo!.publisher.name}']),
|
|
||||||
children: <Widget>[PostItem(data: _repostingTo!)],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Divider(height: 1),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
// Editing Notice
|
|
||||||
if (_editingOg != null)
|
|
||||||
Column(
|
|
||||||
children: [
|
|
||||||
Theme(
|
|
||||||
data: Theme.of(context).copyWith(
|
|
||||||
dividerColor: Colors.transparent,
|
|
||||||
),
|
|
||||||
child: ExpansionTile(
|
|
||||||
minTileHeight: 48,
|
|
||||||
leading:
|
|
||||||
const Icon(Symbols.edit_note).padding(left: 4),
|
|
||||||
title: Text('postEditingNotice')
|
|
||||||
.fontSize(15)
|
|
||||||
.tr(args: ['@${_editingOg!.publisher.name}']),
|
|
||||||
children: <Widget>[PostItem(data: _editingOg!)],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Divider(height: 1),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
// Content Input Area
|
|
||||||
TextField(
|
|
||||||
controller: _contentController,
|
|
||||||
maxLines: null,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: 'fieldPostContent'.tr(),
|
|
||||||
hintStyle: TextStyle(fontSize: 14),
|
|
||||||
isCollapsed: true,
|
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 16,
|
|
||||||
),
|
|
||||||
border: InputBorder.none,
|
|
||||||
),
|
|
||||||
onTapOutside: (_) =>
|
|
||||||
FocusManager.instance.primaryFocus?.unfocus(),
|
|
||||||
)
|
|
||||||
].expand((ele) => [ele, const Gap(8)]).toList()
|
|
||||||
..removeLast(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (_selectedMedia.isNotEmpty)
|
|
||||||
PostMediaPendingList(
|
|
||||||
data: _selectedMedia,
|
|
||||||
onRemove: (idx) {
|
|
||||||
setState(() {
|
|
||||||
_selectedMedia.removeAt(idx);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
).padding(bottom: 8),
|
|
||||||
Material(
|
|
||||||
elevation: 2,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
LoadingIndicator(isActive: _isLoading),
|
|
||||||
if (_isBusy && _progress != null)
|
|
||||||
TweenAnimationBuilder<double>(
|
|
||||||
tween: Tween(begin: 0, end: 1),
|
|
||||||
duration: Duration(milliseconds: 300),
|
|
||||||
builder: (context, value, _) =>
|
|
||||||
LinearProgressIndicator(value: value, minHeight: 2),
|
|
||||||
)
|
|
||||||
else if (_isBusy)
|
|
||||||
const LinearProgressIndicator(value: null, minHeight: 2),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
LoadingIndicator(isActive: _isLoading),
|
||||||
child: ScrollConfiguration(
|
if (_writeController.isBusy &&
|
||||||
behavior: _PostEditorActionScrollBehavior(),
|
_writeController.progress != null)
|
||||||
child: SingleChildScrollView(
|
TweenAnimationBuilder<double>(
|
||||||
scrollDirection: Axis.vertical,
|
tween: Tween(begin: 0, end: _writeController.progress),
|
||||||
child: Row(
|
duration: Duration(milliseconds: 300),
|
||||||
children: [
|
builder: (context, value, _) =>
|
||||||
IconButton(
|
LinearProgressIndicator(value: value, minHeight: 2),
|
||||||
onPressed: _isBusy ? null : _selectMedia,
|
)
|
||||||
icon: Icon(
|
else if (_writeController.isBusy)
|
||||||
Symbols.add_photo_alternate,
|
const LinearProgressIndicator(value: null, minHeight: 2),
|
||||||
color: Theme.of(context).colorScheme.primary,
|
Row(
|
||||||
),
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: ScrollConfiguration(
|
||||||
|
behavior: _PostEditorActionScrollBehavior(),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.vertical,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: _writeController.isBusy
|
||||||
|
? null
|
||||||
|
: _selectMedia,
|
||||||
|
icon: Icon(
|
||||||
|
Symbols.add_photo_alternate,
|
||||||
|
color:
|
||||||
|
Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
TextButton.icon(
|
||||||
),
|
onPressed: (_writeController.isBusy ||
|
||||||
TextButton.icon(
|
_writeController.publisher == null)
|
||||||
onPressed: (_isBusy || _publisher == null)
|
? null
|
||||||
? null
|
: () {
|
||||||
: _performAction,
|
_writeController.post(context).then((_) {
|
||||||
icon: const Icon(Symbols.send),
|
if (!context.mounted) return;
|
||||||
label: Text('postPublish').tr(),
|
Navigator.pop(context, true);
|
||||||
),
|
});
|
||||||
|
},
|
||||||
|
icon: const Icon(Symbols.send),
|
||||||
|
label: Text('postPublish').tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 16),
|
||||||
],
|
],
|
||||||
).padding(horizontal: 16),
|
).padding(
|
||||||
],
|
bottom: MediaQuery.of(context).padding.bottom,
|
||||||
).padding(
|
top: 4,
|
||||||
bottom: MediaQuery.of(context).padding.bottom,
|
),
|
||||||
top: 4,
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
],
|
);
|
||||||
),
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
241
lib/screens/settings.dart
Normal file
241
lib/screens/settings.dart
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:surface/providers/sn_network.dart';
|
||||||
|
import 'package:surface/providers/theme.dart';
|
||||||
|
import 'package:surface/theme.dart';
|
||||||
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
|
|
||||||
|
class SettingsScreen extends StatefulWidget {
|
||||||
|
const SettingsScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SettingsScreen> createState() => _SettingsScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SettingsScreenState extends State<SettingsScreen> {
|
||||||
|
SharedPreferences? _prefs;
|
||||||
|
String _docBasepath = '/';
|
||||||
|
|
||||||
|
final TextEditingController _serverUrlController = TextEditingController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
getApplicationDocumentsDirectory().then((dir) {
|
||||||
|
_docBasepath = dir.path;
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
SharedPreferences.getInstance().then((prefs) {
|
||||||
|
setState(() {
|
||||||
|
_prefs = prefs;
|
||||||
|
_serverUrlController.text =
|
||||||
|
prefs.getString(kNetworkServerStoreKey) ?? kNetworkServerDefault;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_serverUrlController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
|
||||||
|
return AppScaffold(
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('settingsAppearance')
|
||||||
|
.bold()
|
||||||
|
.fontSize(17)
|
||||||
|
.tr()
|
||||||
|
.padding(horizontal: 20, bottom: 4),
|
||||||
|
if (!kIsWeb)
|
||||||
|
ListTile(
|
||||||
|
title: Text('settingsBackgroundImage').tr(),
|
||||||
|
subtitle: Text('settingsBackgroundImageDescription').tr(),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
leading: const Icon(Symbols.image),
|
||||||
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
|
onTap: () async {
|
||||||
|
final image = await ImagePicker()
|
||||||
|
.pickImage(source: ImageSource.gallery);
|
||||||
|
if (image == null) return;
|
||||||
|
|
||||||
|
await File(image.path)
|
||||||
|
.copy('$_docBasepath/app_background_image');
|
||||||
|
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (!kIsWeb)
|
||||||
|
FutureBuilder<bool>(
|
||||||
|
future:
|
||||||
|
File('$_docBasepath/app_background_image').exists(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (!snapshot.hasData || !snapshot.data!) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
title: Text('settingsBackgroundImageClear').tr(),
|
||||||
|
subtitle:
|
||||||
|
Text('settingsBackgroundImageClearDescription')
|
||||||
|
.tr(),
|
||||||
|
contentPadding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
leading: const Icon(Symbols.texture),
|
||||||
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
|
onTap: () {
|
||||||
|
File('$_docBasepath/app_background_image')
|
||||||
|
.deleteSync();
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
if (_prefs != null)
|
||||||
|
CheckboxListTile(
|
||||||
|
title: Text('settingsThemeMaterial3').tr(),
|
||||||
|
subtitle: Text('settingsThemeMaterial3Description').tr(),
|
||||||
|
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
||||||
|
secondary: const Icon(Symbols.new_releases),
|
||||||
|
value: _prefs!.getBool(kMaterialYouToggleStoreKey) ?? false,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_prefs!.setBool(
|
||||||
|
kMaterialYouToggleStoreKey,
|
||||||
|
value ?? false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
final th = context.watch<ThemeProvider>();
|
||||||
|
th.reloadTheme(useMaterial3: value ?? false);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('settingsNetwork')
|
||||||
|
.bold()
|
||||||
|
.fontSize(17)
|
||||||
|
.tr()
|
||||||
|
.padding(horizontal: 20, bottom: 4),
|
||||||
|
TextField(
|
||||||
|
controller: _serverUrlController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
labelText: 'settingsNetworkServer'.tr(),
|
||||||
|
helperText: 'settingsNetworkServerDescription'.tr(),
|
||||||
|
prefixIcon: const Icon(Symbols.dns),
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
icon: const Icon(Symbols.save),
|
||||||
|
onPressed: () {
|
||||||
|
sn.setBaseUrl(_serverUrlController.text);
|
||||||
|
_prefs?.setString(
|
||||||
|
kNetworkServerStoreKey,
|
||||||
|
_serverUrlController.text,
|
||||||
|
);
|
||||||
|
context.showSnackbar('settingsNetworkServerSaved'.tr());
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTapOutside: (_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
).padding(horizontal: 16, top: 8, bottom: 4),
|
||||||
|
ListTile(
|
||||||
|
title: Text('settingsNetworkServerPreset').tr(),
|
||||||
|
subtitle: Text('settingsNetworkServerPresetDescription').tr(),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
leading: const Icon(Symbols.lists),
|
||||||
|
trailing: DropdownButtonHideUnderline(
|
||||||
|
child: DropdownButton2<String>(
|
||||||
|
isExpanded: true,
|
||||||
|
items: [
|
||||||
|
...kNetworkServerDirectory,
|
||||||
|
if (!kNetworkServerDirectory
|
||||||
|
.map((ele) => ele.$2)
|
||||||
|
.contains(_serverUrlController.text))
|
||||||
|
('Custom', _serverUrlController.text),
|
||||||
|
]
|
||||||
|
.map(
|
||||||
|
(item) => DropdownMenuItem<String>(
|
||||||
|
value: item.$2,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(item.$1).fontSize(14),
|
||||||
|
Text(item.$2, overflow: TextOverflow.ellipsis)
|
||||||
|
.fontSize(11)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
value: _serverUrlController.text,
|
||||||
|
onChanged: (String? value) {
|
||||||
|
if (value == null) return;
|
||||||
|
_serverUrlController.text = value;
|
||||||
|
_prefs?.setString(kNetworkServerStoreKey, value);
|
||||||
|
context.showSnackbar('settingsNetworkServerSaved'.tr());
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
buttonStyleData: const ButtonStyleData(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 5,
|
||||||
|
),
|
||||||
|
height: 40,
|
||||||
|
width: 140,
|
||||||
|
),
|
||||||
|
menuItemStyleData: const MenuItemStyleData(
|
||||||
|
height: 60,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text('settingsNetworkServerReset').tr(),
|
||||||
|
subtitle: Text('settingsNetworkServerResetDescription').tr(),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
leading: const Icon(Symbols.reset_wrench),
|
||||||
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
|
onTap: () {
|
||||||
|
_serverUrlController.text = kNetworkServerDefault;
|
||||||
|
_prefs?.remove(kNetworkServerStoreKey);
|
||||||
|
context.showSnackbar('settingsNetworkServerSaved'.tr());
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
].expand((ele) => [ele, const Gap(16)]).toList(),
|
||||||
|
).padding(vertical: 20),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
const kMaterialYouToggleStoreKey = 'app_theme_material_you';
|
||||||
|
|
||||||
class ThemeSet {
|
class ThemeSet {
|
||||||
ThemeData light;
|
ThemeData light;
|
||||||
@@ -7,21 +10,35 @@ class ThemeSet {
|
|||||||
ThemeSet({required this.light, required this.dark});
|
ThemeSet({required this.light, required this.dark});
|
||||||
}
|
}
|
||||||
|
|
||||||
ThemeSet createAppThemeSet() {
|
Future<ThemeSet> createAppThemeSet({bool? useMaterial3}) async {
|
||||||
return ThemeSet(
|
return ThemeSet(
|
||||||
light: createAppTheme(Brightness.light),
|
light: await createAppTheme(Brightness.light, useMaterial3: useMaterial3),
|
||||||
dark: createAppTheme(Brightness.dark),
|
dark: await createAppTheme(Brightness.dark, useMaterial3: useMaterial3),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ThemeData createAppTheme(Brightness brightness) {
|
Future<ThemeData> createAppTheme(
|
||||||
return ThemeData(
|
Brightness brightness, {
|
||||||
useMaterial3: false,
|
bool? useMaterial3,
|
||||||
colorScheme: ColorScheme.fromSeed(
|
}) async {
|
||||||
seedColor: Colors.indigo,
|
final prefs = await SharedPreferences.getInstance();
|
||||||
brightness: brightness,
|
|
||||||
),
|
final colorScheme = ColorScheme.fromSeed(
|
||||||
|
seedColor: Colors.indigo,
|
||||||
brightness: brightness,
|
brightness: brightness,
|
||||||
iconTheme: const IconThemeData(fill: 0, weight: 400, opticalSize: 20),
|
);
|
||||||
|
|
||||||
|
return ThemeData(
|
||||||
|
useMaterial3:
|
||||||
|
useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? false),
|
||||||
|
colorScheme: colorScheme,
|
||||||
|
brightness: brightness,
|
||||||
|
iconTheme: IconThemeData(
|
||||||
|
fill: 0,
|
||||||
|
weight: 400,
|
||||||
|
opticalSize: 20,
|
||||||
|
color: colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
scaffoldBackgroundColor: Colors.transparent,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,25 +20,22 @@ class SnPost with _$SnPost {
|
|||||||
required String? aliasPrefix,
|
required String? aliasPrefix,
|
||||||
required List<dynamic> tags,
|
required List<dynamic> tags,
|
||||||
required List<dynamic> categories,
|
required List<dynamic> categories,
|
||||||
required dynamic reactions,
|
required List<SnPost>? replies,
|
||||||
required dynamic replies,
|
required int? replyId,
|
||||||
required dynamic replyId,
|
required int? repostId,
|
||||||
required dynamic repostId,
|
required SnPost? replyTo,
|
||||||
required dynamic replyTo,
|
required SnPost? repostTo,
|
||||||
required dynamic repostTo,
|
required List<int>? visibleUsersList,
|
||||||
required dynamic visibleUsersList,
|
required List<int>? invisibleUsersList,
|
||||||
required dynamic invisibleUsersList,
|
|
||||||
required int visibility,
|
required int visibility,
|
||||||
required DateTime? editedAt,
|
required DateTime? editedAt,
|
||||||
required DateTime? pinnedAt,
|
required DateTime? pinnedAt,
|
||||||
required DateTime? lockedAt,
|
required DateTime? lockedAt,
|
||||||
required bool isDraft,
|
required bool isDraft,
|
||||||
required DateTime? publishedAt,
|
required DateTime? publishedAt,
|
||||||
required dynamic publishedUntil,
|
required DateTime? publishedUntil,
|
||||||
required int totalUpvote,
|
required int totalUpvote,
|
||||||
required int totalDownvote,
|
required int totalDownvote,
|
||||||
required int? realmId,
|
|
||||||
required dynamic realm,
|
|
||||||
required int publisherId,
|
required int publisherId,
|
||||||
required SnPublisher publisher,
|
required SnPublisher publisher,
|
||||||
required SnMetric metric,
|
required SnMetric metric,
|
||||||
@@ -81,6 +78,7 @@ class SnMetric with _$SnMetric {
|
|||||||
const factory SnMetric({
|
const factory SnMetric({
|
||||||
required int replyCount,
|
required int replyCount,
|
||||||
required int reactionCount,
|
required int reactionCount,
|
||||||
|
@Default({}) Map<String, int> reactionList,
|
||||||
}) = _SnMetric;
|
}) = _SnMetric;
|
||||||
|
|
||||||
factory SnMetric.fromJson(Map<String, Object?> json) =>
|
factory SnMetric.fromJson(Map<String, Object?> json) =>
|
||||||
|
|||||||
@@ -31,25 +31,22 @@ mixin _$SnPost {
|
|||||||
String? get aliasPrefix => throw _privateConstructorUsedError;
|
String? get aliasPrefix => throw _privateConstructorUsedError;
|
||||||
List<dynamic> get tags => throw _privateConstructorUsedError;
|
List<dynamic> get tags => throw _privateConstructorUsedError;
|
||||||
List<dynamic> get categories => throw _privateConstructorUsedError;
|
List<dynamic> get categories => throw _privateConstructorUsedError;
|
||||||
dynamic get reactions => throw _privateConstructorUsedError;
|
List<SnPost>? get replies => throw _privateConstructorUsedError;
|
||||||
dynamic get replies => throw _privateConstructorUsedError;
|
int? get replyId => throw _privateConstructorUsedError;
|
||||||
dynamic get replyId => throw _privateConstructorUsedError;
|
int? get repostId => throw _privateConstructorUsedError;
|
||||||
dynamic get repostId => throw _privateConstructorUsedError;
|
SnPost? get replyTo => throw _privateConstructorUsedError;
|
||||||
dynamic get replyTo => throw _privateConstructorUsedError;
|
SnPost? get repostTo => throw _privateConstructorUsedError;
|
||||||
dynamic get repostTo => throw _privateConstructorUsedError;
|
List<int>? get visibleUsersList => throw _privateConstructorUsedError;
|
||||||
dynamic get visibleUsersList => throw _privateConstructorUsedError;
|
List<int>? get invisibleUsersList => throw _privateConstructorUsedError;
|
||||||
dynamic get invisibleUsersList => throw _privateConstructorUsedError;
|
|
||||||
int get visibility => throw _privateConstructorUsedError;
|
int get visibility => throw _privateConstructorUsedError;
|
||||||
DateTime? get editedAt => throw _privateConstructorUsedError;
|
DateTime? get editedAt => throw _privateConstructorUsedError;
|
||||||
DateTime? get pinnedAt => throw _privateConstructorUsedError;
|
DateTime? get pinnedAt => throw _privateConstructorUsedError;
|
||||||
DateTime? get lockedAt => throw _privateConstructorUsedError;
|
DateTime? get lockedAt => throw _privateConstructorUsedError;
|
||||||
bool get isDraft => throw _privateConstructorUsedError;
|
bool get isDraft => throw _privateConstructorUsedError;
|
||||||
DateTime? get publishedAt => throw _privateConstructorUsedError;
|
DateTime? get publishedAt => throw _privateConstructorUsedError;
|
||||||
dynamic get publishedUntil => throw _privateConstructorUsedError;
|
DateTime? get publishedUntil => throw _privateConstructorUsedError;
|
||||||
int get totalUpvote => throw _privateConstructorUsedError;
|
int get totalUpvote => throw _privateConstructorUsedError;
|
||||||
int get totalDownvote => throw _privateConstructorUsedError;
|
int get totalDownvote => throw _privateConstructorUsedError;
|
||||||
int? get realmId => throw _privateConstructorUsedError;
|
|
||||||
dynamic get realm => throw _privateConstructorUsedError;
|
|
||||||
int get publisherId => throw _privateConstructorUsedError;
|
int get publisherId => throw _privateConstructorUsedError;
|
||||||
SnPublisher get publisher => throw _privateConstructorUsedError;
|
SnPublisher get publisher => throw _privateConstructorUsedError;
|
||||||
SnMetric get metric => throw _privateConstructorUsedError;
|
SnMetric get metric => throw _privateConstructorUsedError;
|
||||||
@@ -81,30 +78,29 @@ abstract class $SnPostCopyWith<$Res> {
|
|||||||
String? aliasPrefix,
|
String? aliasPrefix,
|
||||||
List<dynamic> tags,
|
List<dynamic> tags,
|
||||||
List<dynamic> categories,
|
List<dynamic> categories,
|
||||||
dynamic reactions,
|
List<SnPost>? replies,
|
||||||
dynamic replies,
|
int? replyId,
|
||||||
dynamic replyId,
|
int? repostId,
|
||||||
dynamic repostId,
|
SnPost? replyTo,
|
||||||
dynamic replyTo,
|
SnPost? repostTo,
|
||||||
dynamic repostTo,
|
List<int>? visibleUsersList,
|
||||||
dynamic visibleUsersList,
|
List<int>? invisibleUsersList,
|
||||||
dynamic invisibleUsersList,
|
|
||||||
int visibility,
|
int visibility,
|
||||||
DateTime? editedAt,
|
DateTime? editedAt,
|
||||||
DateTime? pinnedAt,
|
DateTime? pinnedAt,
|
||||||
DateTime? lockedAt,
|
DateTime? lockedAt,
|
||||||
bool isDraft,
|
bool isDraft,
|
||||||
DateTime? publishedAt,
|
DateTime? publishedAt,
|
||||||
dynamic publishedUntil,
|
DateTime? publishedUntil,
|
||||||
int totalUpvote,
|
int totalUpvote,
|
||||||
int totalDownvote,
|
int totalDownvote,
|
||||||
int? realmId,
|
|
||||||
dynamic realm,
|
|
||||||
int publisherId,
|
int publisherId,
|
||||||
SnPublisher publisher,
|
SnPublisher publisher,
|
||||||
SnMetric metric,
|
SnMetric metric,
|
||||||
SnPostPreload? preload});
|
SnPostPreload? preload});
|
||||||
|
|
||||||
|
$SnPostCopyWith<$Res>? get replyTo;
|
||||||
|
$SnPostCopyWith<$Res>? get repostTo;
|
||||||
$SnPublisherCopyWith<$Res> get publisher;
|
$SnPublisherCopyWith<$Res> get publisher;
|
||||||
$SnMetricCopyWith<$Res> get metric;
|
$SnMetricCopyWith<$Res> get metric;
|
||||||
$SnPostPreloadCopyWith<$Res>? get preload;
|
$SnPostPreloadCopyWith<$Res>? get preload;
|
||||||
@@ -136,7 +132,6 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost>
|
|||||||
Object? aliasPrefix = freezed,
|
Object? aliasPrefix = freezed,
|
||||||
Object? tags = null,
|
Object? tags = null,
|
||||||
Object? categories = null,
|
Object? categories = null,
|
||||||
Object? reactions = freezed,
|
|
||||||
Object? replies = freezed,
|
Object? replies = freezed,
|
||||||
Object? replyId = freezed,
|
Object? replyId = freezed,
|
||||||
Object? repostId = freezed,
|
Object? repostId = freezed,
|
||||||
@@ -153,8 +148,6 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost>
|
|||||||
Object? publishedUntil = freezed,
|
Object? publishedUntil = freezed,
|
||||||
Object? totalUpvote = null,
|
Object? totalUpvote = null,
|
||||||
Object? totalDownvote = null,
|
Object? totalDownvote = null,
|
||||||
Object? realmId = freezed,
|
|
||||||
Object? realm = freezed,
|
|
||||||
Object? publisherId = null,
|
Object? publisherId = null,
|
||||||
Object? publisher = null,
|
Object? publisher = null,
|
||||||
Object? metric = null,
|
Object? metric = null,
|
||||||
@@ -205,38 +198,34 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost>
|
|||||||
? _value.categories
|
? _value.categories
|
||||||
: categories // ignore: cast_nullable_to_non_nullable
|
: categories // ignore: cast_nullable_to_non_nullable
|
||||||
as List<dynamic>,
|
as List<dynamic>,
|
||||||
reactions: freezed == reactions
|
|
||||||
? _value.reactions
|
|
||||||
: reactions // ignore: cast_nullable_to_non_nullable
|
|
||||||
as dynamic,
|
|
||||||
replies: freezed == replies
|
replies: freezed == replies
|
||||||
? _value.replies
|
? _value.replies
|
||||||
: replies // ignore: cast_nullable_to_non_nullable
|
: replies // ignore: cast_nullable_to_non_nullable
|
||||||
as dynamic,
|
as List<SnPost>?,
|
||||||
replyId: freezed == replyId
|
replyId: freezed == replyId
|
||||||
? _value.replyId
|
? _value.replyId
|
||||||
: replyId // ignore: cast_nullable_to_non_nullable
|
: replyId // ignore: cast_nullable_to_non_nullable
|
||||||
as dynamic,
|
as int?,
|
||||||
repostId: freezed == repostId
|
repostId: freezed == repostId
|
||||||
? _value.repostId
|
? _value.repostId
|
||||||
: repostId // ignore: cast_nullable_to_non_nullable
|
: repostId // ignore: cast_nullable_to_non_nullable
|
||||||
as dynamic,
|
as int?,
|
||||||
replyTo: freezed == replyTo
|
replyTo: freezed == replyTo
|
||||||
? _value.replyTo
|
? _value.replyTo
|
||||||
: replyTo // ignore: cast_nullable_to_non_nullable
|
: replyTo // ignore: cast_nullable_to_non_nullable
|
||||||
as dynamic,
|
as SnPost?,
|
||||||
repostTo: freezed == repostTo
|
repostTo: freezed == repostTo
|
||||||
? _value.repostTo
|
? _value.repostTo
|
||||||
: repostTo // ignore: cast_nullable_to_non_nullable
|
: repostTo // ignore: cast_nullable_to_non_nullable
|
||||||
as dynamic,
|
as SnPost?,
|
||||||
visibleUsersList: freezed == visibleUsersList
|
visibleUsersList: freezed == visibleUsersList
|
||||||
? _value.visibleUsersList
|
? _value.visibleUsersList
|
||||||
: visibleUsersList // ignore: cast_nullable_to_non_nullable
|
: visibleUsersList // ignore: cast_nullable_to_non_nullable
|
||||||
as dynamic,
|
as List<int>?,
|
||||||
invisibleUsersList: freezed == invisibleUsersList
|
invisibleUsersList: freezed == invisibleUsersList
|
||||||
? _value.invisibleUsersList
|
? _value.invisibleUsersList
|
||||||
: invisibleUsersList // ignore: cast_nullable_to_non_nullable
|
: invisibleUsersList // ignore: cast_nullable_to_non_nullable
|
||||||
as dynamic,
|
as List<int>?,
|
||||||
visibility: null == visibility
|
visibility: null == visibility
|
||||||
? _value.visibility
|
? _value.visibility
|
||||||
: visibility // ignore: cast_nullable_to_non_nullable
|
: visibility // ignore: cast_nullable_to_non_nullable
|
||||||
@@ -264,7 +253,7 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost>
|
|||||||
publishedUntil: freezed == publishedUntil
|
publishedUntil: freezed == publishedUntil
|
||||||
? _value.publishedUntil
|
? _value.publishedUntil
|
||||||
: publishedUntil // ignore: cast_nullable_to_non_nullable
|
: publishedUntil // ignore: cast_nullable_to_non_nullable
|
||||||
as dynamic,
|
as DateTime?,
|
||||||
totalUpvote: null == totalUpvote
|
totalUpvote: null == totalUpvote
|
||||||
? _value.totalUpvote
|
? _value.totalUpvote
|
||||||
: totalUpvote // ignore: cast_nullable_to_non_nullable
|
: totalUpvote // ignore: cast_nullable_to_non_nullable
|
||||||
@@ -273,14 +262,6 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost>
|
|||||||
? _value.totalDownvote
|
? _value.totalDownvote
|
||||||
: totalDownvote // ignore: cast_nullable_to_non_nullable
|
: totalDownvote // ignore: cast_nullable_to_non_nullable
|
||||||
as int,
|
as int,
|
||||||
realmId: freezed == realmId
|
|
||||||
? _value.realmId
|
|
||||||
: realmId // ignore: cast_nullable_to_non_nullable
|
|
||||||
as int?,
|
|
||||||
realm: freezed == realm
|
|
||||||
? _value.realm
|
|
||||||
: realm // ignore: cast_nullable_to_non_nullable
|
|
||||||
as dynamic,
|
|
||||||
publisherId: null == publisherId
|
publisherId: null == publisherId
|
||||||
? _value.publisherId
|
? _value.publisherId
|
||||||
: publisherId // ignore: cast_nullable_to_non_nullable
|
: publisherId // ignore: cast_nullable_to_non_nullable
|
||||||
@@ -300,6 +281,34 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost>
|
|||||||
) as $Val);
|
) as $Val);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a copy of SnPost
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnPostCopyWith<$Res>? get replyTo {
|
||||||
|
if (_value.replyTo == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $SnPostCopyWith<$Res>(_value.replyTo!, (value) {
|
||||||
|
return _then(_value.copyWith(replyTo: value) as $Val);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a copy of SnPost
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnPostCopyWith<$Res>? get repostTo {
|
||||||
|
if (_value.repostTo == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $SnPostCopyWith<$Res>(_value.repostTo!, (value) {
|
||||||
|
return _then(_value.copyWith(repostTo: value) as $Val);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a copy of SnPost
|
/// Create a copy of SnPost
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@override
|
@override
|
||||||
@@ -354,30 +363,31 @@ abstract class _$$SnPostImplCopyWith<$Res> implements $SnPostCopyWith<$Res> {
|
|||||||
String? aliasPrefix,
|
String? aliasPrefix,
|
||||||
List<dynamic> tags,
|
List<dynamic> tags,
|
||||||
List<dynamic> categories,
|
List<dynamic> categories,
|
||||||
dynamic reactions,
|
List<SnPost>? replies,
|
||||||
dynamic replies,
|
int? replyId,
|
||||||
dynamic replyId,
|
int? repostId,
|
||||||
dynamic repostId,
|
SnPost? replyTo,
|
||||||
dynamic replyTo,
|
SnPost? repostTo,
|
||||||
dynamic repostTo,
|
List<int>? visibleUsersList,
|
||||||
dynamic visibleUsersList,
|
List<int>? invisibleUsersList,
|
||||||
dynamic invisibleUsersList,
|
|
||||||
int visibility,
|
int visibility,
|
||||||
DateTime? editedAt,
|
DateTime? editedAt,
|
||||||
DateTime? pinnedAt,
|
DateTime? pinnedAt,
|
||||||
DateTime? lockedAt,
|
DateTime? lockedAt,
|
||||||
bool isDraft,
|
bool isDraft,
|
||||||
DateTime? publishedAt,
|
DateTime? publishedAt,
|
||||||
dynamic publishedUntil,
|
DateTime? publishedUntil,
|
||||||
int totalUpvote,
|
int totalUpvote,
|
||||||
int totalDownvote,
|
int totalDownvote,
|
||||||
int? realmId,
|
|
||||||
dynamic realm,
|
|
||||||
int publisherId,
|
int publisherId,
|
||||||
SnPublisher publisher,
|
SnPublisher publisher,
|
||||||
SnMetric metric,
|
SnMetric metric,
|
||||||
SnPostPreload? preload});
|
SnPostPreload? preload});
|
||||||
|
|
||||||
|
@override
|
||||||
|
$SnPostCopyWith<$Res>? get replyTo;
|
||||||
|
@override
|
||||||
|
$SnPostCopyWith<$Res>? get repostTo;
|
||||||
@override
|
@override
|
||||||
$SnPublisherCopyWith<$Res> get publisher;
|
$SnPublisherCopyWith<$Res> get publisher;
|
||||||
@override
|
@override
|
||||||
@@ -410,7 +420,6 @@ class __$$SnPostImplCopyWithImpl<$Res>
|
|||||||
Object? aliasPrefix = freezed,
|
Object? aliasPrefix = freezed,
|
||||||
Object? tags = null,
|
Object? tags = null,
|
||||||
Object? categories = null,
|
Object? categories = null,
|
||||||
Object? reactions = freezed,
|
|
||||||
Object? replies = freezed,
|
Object? replies = freezed,
|
||||||
Object? replyId = freezed,
|
Object? replyId = freezed,
|
||||||
Object? repostId = freezed,
|
Object? repostId = freezed,
|
||||||
@@ -427,8 +436,6 @@ class __$$SnPostImplCopyWithImpl<$Res>
|
|||||||
Object? publishedUntil = freezed,
|
Object? publishedUntil = freezed,
|
||||||
Object? totalUpvote = null,
|
Object? totalUpvote = null,
|
||||||
Object? totalDownvote = null,
|
Object? totalDownvote = null,
|
||||||
Object? realmId = freezed,
|
|
||||||
Object? realm = freezed,
|
|
||||||
Object? publisherId = null,
|
Object? publisherId = null,
|
||||||
Object? publisher = null,
|
Object? publisher = null,
|
||||||
Object? metric = null,
|
Object? metric = null,
|
||||||
@@ -479,38 +486,34 @@ class __$$SnPostImplCopyWithImpl<$Res>
|
|||||||
? _value._categories
|
? _value._categories
|
||||||
: categories // ignore: cast_nullable_to_non_nullable
|
: categories // ignore: cast_nullable_to_non_nullable
|
||||||
as List<dynamic>,
|
as List<dynamic>,
|
||||||
reactions: freezed == reactions
|
|
||||||
? _value.reactions
|
|
||||||
: reactions // ignore: cast_nullable_to_non_nullable
|
|
||||||
as dynamic,
|
|
||||||
replies: freezed == replies
|
replies: freezed == replies
|
||||||
? _value.replies
|
? _value._replies
|
||||||
: replies // ignore: cast_nullable_to_non_nullable
|
: replies // ignore: cast_nullable_to_non_nullable
|
||||||
as dynamic,
|
as List<SnPost>?,
|
||||||
replyId: freezed == replyId
|
replyId: freezed == replyId
|
||||||
? _value.replyId
|
? _value.replyId
|
||||||
: replyId // ignore: cast_nullable_to_non_nullable
|
: replyId // ignore: cast_nullable_to_non_nullable
|
||||||
as dynamic,
|
as int?,
|
||||||
repostId: freezed == repostId
|
repostId: freezed == repostId
|
||||||
? _value.repostId
|
? _value.repostId
|
||||||
: repostId // ignore: cast_nullable_to_non_nullable
|
: repostId // ignore: cast_nullable_to_non_nullable
|
||||||
as dynamic,
|
as int?,
|
||||||
replyTo: freezed == replyTo
|
replyTo: freezed == replyTo
|
||||||
? _value.replyTo
|
? _value.replyTo
|
||||||
: replyTo // ignore: cast_nullable_to_non_nullable
|
: replyTo // ignore: cast_nullable_to_non_nullable
|
||||||
as dynamic,
|
as SnPost?,
|
||||||
repostTo: freezed == repostTo
|
repostTo: freezed == repostTo
|
||||||
? _value.repostTo
|
? _value.repostTo
|
||||||
: repostTo // ignore: cast_nullable_to_non_nullable
|
: repostTo // ignore: cast_nullable_to_non_nullable
|
||||||
as dynamic,
|
as SnPost?,
|
||||||
visibleUsersList: freezed == visibleUsersList
|
visibleUsersList: freezed == visibleUsersList
|
||||||
? _value.visibleUsersList
|
? _value._visibleUsersList
|
||||||
: visibleUsersList // ignore: cast_nullable_to_non_nullable
|
: visibleUsersList // ignore: cast_nullable_to_non_nullable
|
||||||
as dynamic,
|
as List<int>?,
|
||||||
invisibleUsersList: freezed == invisibleUsersList
|
invisibleUsersList: freezed == invisibleUsersList
|
||||||
? _value.invisibleUsersList
|
? _value._invisibleUsersList
|
||||||
: invisibleUsersList // ignore: cast_nullable_to_non_nullable
|
: invisibleUsersList // ignore: cast_nullable_to_non_nullable
|
||||||
as dynamic,
|
as List<int>?,
|
||||||
visibility: null == visibility
|
visibility: null == visibility
|
||||||
? _value.visibility
|
? _value.visibility
|
||||||
: visibility // ignore: cast_nullable_to_non_nullable
|
: visibility // ignore: cast_nullable_to_non_nullable
|
||||||
@@ -538,7 +541,7 @@ class __$$SnPostImplCopyWithImpl<$Res>
|
|||||||
publishedUntil: freezed == publishedUntil
|
publishedUntil: freezed == publishedUntil
|
||||||
? _value.publishedUntil
|
? _value.publishedUntil
|
||||||
: publishedUntil // ignore: cast_nullable_to_non_nullable
|
: publishedUntil // ignore: cast_nullable_to_non_nullable
|
||||||
as dynamic,
|
as DateTime?,
|
||||||
totalUpvote: null == totalUpvote
|
totalUpvote: null == totalUpvote
|
||||||
? _value.totalUpvote
|
? _value.totalUpvote
|
||||||
: totalUpvote // ignore: cast_nullable_to_non_nullable
|
: totalUpvote // ignore: cast_nullable_to_non_nullable
|
||||||
@@ -547,14 +550,6 @@ class __$$SnPostImplCopyWithImpl<$Res>
|
|||||||
? _value.totalDownvote
|
? _value.totalDownvote
|
||||||
: totalDownvote // ignore: cast_nullable_to_non_nullable
|
: totalDownvote // ignore: cast_nullable_to_non_nullable
|
||||||
as int,
|
as int,
|
||||||
realmId: freezed == realmId
|
|
||||||
? _value.realmId
|
|
||||||
: realmId // ignore: cast_nullable_to_non_nullable
|
|
||||||
as int?,
|
|
||||||
realm: freezed == realm
|
|
||||||
? _value.realm
|
|
||||||
: realm // ignore: cast_nullable_to_non_nullable
|
|
||||||
as dynamic,
|
|
||||||
publisherId: null == publisherId
|
publisherId: null == publisherId
|
||||||
? _value.publisherId
|
? _value.publisherId
|
||||||
: publisherId // ignore: cast_nullable_to_non_nullable
|
: publisherId // ignore: cast_nullable_to_non_nullable
|
||||||
@@ -590,14 +585,13 @@ class _$SnPostImpl extends _SnPost {
|
|||||||
required this.aliasPrefix,
|
required this.aliasPrefix,
|
||||||
required final List<dynamic> tags,
|
required final List<dynamic> tags,
|
||||||
required final List<dynamic> categories,
|
required final List<dynamic> categories,
|
||||||
required this.reactions,
|
required final List<SnPost>? replies,
|
||||||
required this.replies,
|
|
||||||
required this.replyId,
|
required this.replyId,
|
||||||
required this.repostId,
|
required this.repostId,
|
||||||
required this.replyTo,
|
required this.replyTo,
|
||||||
required this.repostTo,
|
required this.repostTo,
|
||||||
required this.visibleUsersList,
|
required final List<int>? visibleUsersList,
|
||||||
required this.invisibleUsersList,
|
required final List<int>? invisibleUsersList,
|
||||||
required this.visibility,
|
required this.visibility,
|
||||||
required this.editedAt,
|
required this.editedAt,
|
||||||
required this.pinnedAt,
|
required this.pinnedAt,
|
||||||
@@ -607,8 +601,6 @@ class _$SnPostImpl extends _SnPost {
|
|||||||
required this.publishedUntil,
|
required this.publishedUntil,
|
||||||
required this.totalUpvote,
|
required this.totalUpvote,
|
||||||
required this.totalDownvote,
|
required this.totalDownvote,
|
||||||
required this.realmId,
|
|
||||||
required this.realm,
|
|
||||||
required this.publisherId,
|
required this.publisherId,
|
||||||
required this.publisher,
|
required this.publisher,
|
||||||
required this.metric,
|
required this.metric,
|
||||||
@@ -616,6 +608,9 @@ class _$SnPostImpl extends _SnPost {
|
|||||||
: _body = body,
|
: _body = body,
|
||||||
_tags = tags,
|
_tags = tags,
|
||||||
_categories = categories,
|
_categories = categories,
|
||||||
|
_replies = replies,
|
||||||
|
_visibleUsersList = visibleUsersList,
|
||||||
|
_invisibleUsersList = invisibleUsersList,
|
||||||
super._();
|
super._();
|
||||||
|
|
||||||
factory _$SnPostImpl.fromJson(Map<String, dynamic> json) =>
|
factory _$SnPostImpl.fromJson(Map<String, dynamic> json) =>
|
||||||
@@ -661,22 +656,46 @@ class _$SnPostImpl extends _SnPost {
|
|||||||
return EqualUnmodifiableListView(_categories);
|
return EqualUnmodifiableListView(_categories);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final List<SnPost>? _replies;
|
||||||
@override
|
@override
|
||||||
final dynamic reactions;
|
List<SnPost>? get replies {
|
||||||
|
final value = _replies;
|
||||||
|
if (value == null) return null;
|
||||||
|
if (_replies is EqualUnmodifiableListView) return _replies;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableListView(value);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final dynamic replies;
|
final int? replyId;
|
||||||
@override
|
@override
|
||||||
final dynamic replyId;
|
final int? repostId;
|
||||||
@override
|
@override
|
||||||
final dynamic repostId;
|
final SnPost? replyTo;
|
||||||
@override
|
@override
|
||||||
final dynamic replyTo;
|
final SnPost? repostTo;
|
||||||
|
final List<int>? _visibleUsersList;
|
||||||
@override
|
@override
|
||||||
final dynamic repostTo;
|
List<int>? get visibleUsersList {
|
||||||
|
final value = _visibleUsersList;
|
||||||
|
if (value == null) return null;
|
||||||
|
if (_visibleUsersList is EqualUnmodifiableListView)
|
||||||
|
return _visibleUsersList;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableListView(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<int>? _invisibleUsersList;
|
||||||
@override
|
@override
|
||||||
final dynamic visibleUsersList;
|
List<int>? get invisibleUsersList {
|
||||||
@override
|
final value = _invisibleUsersList;
|
||||||
final dynamic invisibleUsersList;
|
if (value == null) return null;
|
||||||
|
if (_invisibleUsersList is EqualUnmodifiableListView)
|
||||||
|
return _invisibleUsersList;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableListView(value);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final int visibility;
|
final int visibility;
|
||||||
@override
|
@override
|
||||||
@@ -690,16 +709,12 @@ class _$SnPostImpl extends _SnPost {
|
|||||||
@override
|
@override
|
||||||
final DateTime? publishedAt;
|
final DateTime? publishedAt;
|
||||||
@override
|
@override
|
||||||
final dynamic publishedUntil;
|
final DateTime? publishedUntil;
|
||||||
@override
|
@override
|
||||||
final int totalUpvote;
|
final int totalUpvote;
|
||||||
@override
|
@override
|
||||||
final int totalDownvote;
|
final int totalDownvote;
|
||||||
@override
|
@override
|
||||||
final int? realmId;
|
|
||||||
@override
|
|
||||||
final dynamic realm;
|
|
||||||
@override
|
|
||||||
final int publisherId;
|
final int publisherId;
|
||||||
@override
|
@override
|
||||||
final SnPublisher publisher;
|
final SnPublisher publisher;
|
||||||
@@ -710,7 +725,7 @@ class _$SnPostImpl extends _SnPost {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'SnPost(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, body: $body, language: $language, alias: $alias, aliasPrefix: $aliasPrefix, tags: $tags, categories: $categories, reactions: $reactions, replies: $replies, replyId: $replyId, repostId: $repostId, replyTo: $replyTo, repostTo: $repostTo, visibleUsersList: $visibleUsersList, invisibleUsersList: $invisibleUsersList, visibility: $visibility, editedAt: $editedAt, pinnedAt: $pinnedAt, lockedAt: $lockedAt, isDraft: $isDraft, publishedAt: $publishedAt, publishedUntil: $publishedUntil, totalUpvote: $totalUpvote, totalDownvote: $totalDownvote, realmId: $realmId, realm: $realm, publisherId: $publisherId, publisher: $publisher, metric: $metric, preload: $preload)';
|
return 'SnPost(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, body: $body, language: $language, alias: $alias, aliasPrefix: $aliasPrefix, tags: $tags, categories: $categories, replies: $replies, replyId: $replyId, repostId: $repostId, replyTo: $replyTo, repostTo: $repostTo, visibleUsersList: $visibleUsersList, invisibleUsersList: $invisibleUsersList, visibility: $visibility, editedAt: $editedAt, pinnedAt: $pinnedAt, lockedAt: $lockedAt, isDraft: $isDraft, publishedAt: $publishedAt, publishedUntil: $publishedUntil, totalUpvote: $totalUpvote, totalDownvote: $totalDownvote, publisherId: $publisherId, publisher: $publisher, metric: $metric, preload: $preload)';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -735,16 +750,17 @@ class _$SnPostImpl extends _SnPost {
|
|||||||
const DeepCollectionEquality().equals(other._tags, _tags) &&
|
const DeepCollectionEquality().equals(other._tags, _tags) &&
|
||||||
const DeepCollectionEquality()
|
const DeepCollectionEquality()
|
||||||
.equals(other._categories, _categories) &&
|
.equals(other._categories, _categories) &&
|
||||||
const DeepCollectionEquality().equals(other.reactions, reactions) &&
|
const DeepCollectionEquality().equals(other._replies, _replies) &&
|
||||||
const DeepCollectionEquality().equals(other.replies, replies) &&
|
(identical(other.replyId, replyId) || other.replyId == replyId) &&
|
||||||
const DeepCollectionEquality().equals(other.replyId, replyId) &&
|
(identical(other.repostId, repostId) ||
|
||||||
const DeepCollectionEquality().equals(other.repostId, repostId) &&
|
other.repostId == repostId) &&
|
||||||
const DeepCollectionEquality().equals(other.replyTo, replyTo) &&
|
(identical(other.replyTo, replyTo) || other.replyTo == replyTo) &&
|
||||||
const DeepCollectionEquality().equals(other.repostTo, repostTo) &&
|
(identical(other.repostTo, repostTo) ||
|
||||||
|
other.repostTo == repostTo) &&
|
||||||
const DeepCollectionEquality()
|
const DeepCollectionEquality()
|
||||||
.equals(other.visibleUsersList, visibleUsersList) &&
|
.equals(other._visibleUsersList, _visibleUsersList) &&
|
||||||
const DeepCollectionEquality()
|
const DeepCollectionEquality()
|
||||||
.equals(other.invisibleUsersList, invisibleUsersList) &&
|
.equals(other._invisibleUsersList, _invisibleUsersList) &&
|
||||||
(identical(other.visibility, visibility) ||
|
(identical(other.visibility, visibility) ||
|
||||||
other.visibility == visibility) &&
|
other.visibility == visibility) &&
|
||||||
(identical(other.editedAt, editedAt) ||
|
(identical(other.editedAt, editedAt) ||
|
||||||
@@ -756,14 +772,12 @@ class _$SnPostImpl extends _SnPost {
|
|||||||
(identical(other.isDraft, isDraft) || other.isDraft == isDraft) &&
|
(identical(other.isDraft, isDraft) || other.isDraft == isDraft) &&
|
||||||
(identical(other.publishedAt, publishedAt) ||
|
(identical(other.publishedAt, publishedAt) ||
|
||||||
other.publishedAt == publishedAt) &&
|
other.publishedAt == publishedAt) &&
|
||||||
const DeepCollectionEquality()
|
(identical(other.publishedUntil, publishedUntil) ||
|
||||||
.equals(other.publishedUntil, publishedUntil) &&
|
other.publishedUntil == publishedUntil) &&
|
||||||
(identical(other.totalUpvote, totalUpvote) ||
|
(identical(other.totalUpvote, totalUpvote) ||
|
||||||
other.totalUpvote == totalUpvote) &&
|
other.totalUpvote == totalUpvote) &&
|
||||||
(identical(other.totalDownvote, totalDownvote) ||
|
(identical(other.totalDownvote, totalDownvote) ||
|
||||||
other.totalDownvote == totalDownvote) &&
|
other.totalDownvote == totalDownvote) &&
|
||||||
(identical(other.realmId, realmId) || other.realmId == realmId) &&
|
|
||||||
const DeepCollectionEquality().equals(other.realm, realm) &&
|
|
||||||
(identical(other.publisherId, publisherId) ||
|
(identical(other.publisherId, publisherId) ||
|
||||||
other.publisherId == publisherId) &&
|
other.publisherId == publisherId) &&
|
||||||
(identical(other.publisher, publisher) ||
|
(identical(other.publisher, publisher) ||
|
||||||
@@ -787,25 +801,22 @@ class _$SnPostImpl extends _SnPost {
|
|||||||
aliasPrefix,
|
aliasPrefix,
|
||||||
const DeepCollectionEquality().hash(_tags),
|
const DeepCollectionEquality().hash(_tags),
|
||||||
const DeepCollectionEquality().hash(_categories),
|
const DeepCollectionEquality().hash(_categories),
|
||||||
const DeepCollectionEquality().hash(reactions),
|
const DeepCollectionEquality().hash(_replies),
|
||||||
const DeepCollectionEquality().hash(replies),
|
replyId,
|
||||||
const DeepCollectionEquality().hash(replyId),
|
repostId,
|
||||||
const DeepCollectionEquality().hash(repostId),
|
replyTo,
|
||||||
const DeepCollectionEquality().hash(replyTo),
|
repostTo,
|
||||||
const DeepCollectionEquality().hash(repostTo),
|
const DeepCollectionEquality().hash(_visibleUsersList),
|
||||||
const DeepCollectionEquality().hash(visibleUsersList),
|
const DeepCollectionEquality().hash(_invisibleUsersList),
|
||||||
const DeepCollectionEquality().hash(invisibleUsersList),
|
|
||||||
visibility,
|
visibility,
|
||||||
editedAt,
|
editedAt,
|
||||||
pinnedAt,
|
pinnedAt,
|
||||||
lockedAt,
|
lockedAt,
|
||||||
isDraft,
|
isDraft,
|
||||||
publishedAt,
|
publishedAt,
|
||||||
const DeepCollectionEquality().hash(publishedUntil),
|
publishedUntil,
|
||||||
totalUpvote,
|
totalUpvote,
|
||||||
totalDownvote,
|
totalDownvote,
|
||||||
realmId,
|
|
||||||
const DeepCollectionEquality().hash(realm),
|
|
||||||
publisherId,
|
publisherId,
|
||||||
publisher,
|
publisher,
|
||||||
metric,
|
metric,
|
||||||
@@ -841,25 +852,22 @@ abstract class _SnPost extends SnPost {
|
|||||||
required final String? aliasPrefix,
|
required final String? aliasPrefix,
|
||||||
required final List<dynamic> tags,
|
required final List<dynamic> tags,
|
||||||
required final List<dynamic> categories,
|
required final List<dynamic> categories,
|
||||||
required final dynamic reactions,
|
required final List<SnPost>? replies,
|
||||||
required final dynamic replies,
|
required final int? replyId,
|
||||||
required final dynamic replyId,
|
required final int? repostId,
|
||||||
required final dynamic repostId,
|
required final SnPost? replyTo,
|
||||||
required final dynamic replyTo,
|
required final SnPost? repostTo,
|
||||||
required final dynamic repostTo,
|
required final List<int>? visibleUsersList,
|
||||||
required final dynamic visibleUsersList,
|
required final List<int>? invisibleUsersList,
|
||||||
required final dynamic invisibleUsersList,
|
|
||||||
required final int visibility,
|
required final int visibility,
|
||||||
required final DateTime? editedAt,
|
required final DateTime? editedAt,
|
||||||
required final DateTime? pinnedAt,
|
required final DateTime? pinnedAt,
|
||||||
required final DateTime? lockedAt,
|
required final DateTime? lockedAt,
|
||||||
required final bool isDraft,
|
required final bool isDraft,
|
||||||
required final DateTime? publishedAt,
|
required final DateTime? publishedAt,
|
||||||
required final dynamic publishedUntil,
|
required final DateTime? publishedUntil,
|
||||||
required final int totalUpvote,
|
required final int totalUpvote,
|
||||||
required final int totalDownvote,
|
required final int totalDownvote,
|
||||||
required final int? realmId,
|
|
||||||
required final dynamic realm,
|
|
||||||
required final int publisherId,
|
required final int publisherId,
|
||||||
required final SnPublisher publisher,
|
required final SnPublisher publisher,
|
||||||
required final SnMetric metric,
|
required final SnMetric metric,
|
||||||
@@ -891,21 +899,19 @@ abstract class _SnPost extends SnPost {
|
|||||||
@override
|
@override
|
||||||
List<dynamic> get categories;
|
List<dynamic> get categories;
|
||||||
@override
|
@override
|
||||||
dynamic get reactions;
|
List<SnPost>? get replies;
|
||||||
@override
|
@override
|
||||||
dynamic get replies;
|
int? get replyId;
|
||||||
@override
|
@override
|
||||||
dynamic get replyId;
|
int? get repostId;
|
||||||
@override
|
@override
|
||||||
dynamic get repostId;
|
SnPost? get replyTo;
|
||||||
@override
|
@override
|
||||||
dynamic get replyTo;
|
SnPost? get repostTo;
|
||||||
@override
|
@override
|
||||||
dynamic get repostTo;
|
List<int>? get visibleUsersList;
|
||||||
@override
|
@override
|
||||||
dynamic get visibleUsersList;
|
List<int>? get invisibleUsersList;
|
||||||
@override
|
|
||||||
dynamic get invisibleUsersList;
|
|
||||||
@override
|
@override
|
||||||
int get visibility;
|
int get visibility;
|
||||||
@override
|
@override
|
||||||
@@ -919,16 +925,12 @@ abstract class _SnPost extends SnPost {
|
|||||||
@override
|
@override
|
||||||
DateTime? get publishedAt;
|
DateTime? get publishedAt;
|
||||||
@override
|
@override
|
||||||
dynamic get publishedUntil;
|
DateTime? get publishedUntil;
|
||||||
@override
|
@override
|
||||||
int get totalUpvote;
|
int get totalUpvote;
|
||||||
@override
|
@override
|
||||||
int get totalDownvote;
|
int get totalDownvote;
|
||||||
@override
|
@override
|
||||||
int? get realmId;
|
|
||||||
@override
|
|
||||||
dynamic get realm;
|
|
||||||
@override
|
|
||||||
int get publisherId;
|
int get publisherId;
|
||||||
@override
|
@override
|
||||||
SnPublisher get publisher;
|
SnPublisher get publisher;
|
||||||
@@ -1356,6 +1358,7 @@ SnMetric _$SnMetricFromJson(Map<String, dynamic> json) {
|
|||||||
mixin _$SnMetric {
|
mixin _$SnMetric {
|
||||||
int get replyCount => throw _privateConstructorUsedError;
|
int get replyCount => throw _privateConstructorUsedError;
|
||||||
int get reactionCount => throw _privateConstructorUsedError;
|
int get reactionCount => throw _privateConstructorUsedError;
|
||||||
|
Map<String, int> get reactionList => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
/// Serializes this SnMetric to a JSON map.
|
/// Serializes this SnMetric to a JSON map.
|
||||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
@@ -1372,7 +1375,7 @@ abstract class $SnMetricCopyWith<$Res> {
|
|||||||
factory $SnMetricCopyWith(SnMetric value, $Res Function(SnMetric) then) =
|
factory $SnMetricCopyWith(SnMetric value, $Res Function(SnMetric) then) =
|
||||||
_$SnMetricCopyWithImpl<$Res, SnMetric>;
|
_$SnMetricCopyWithImpl<$Res, SnMetric>;
|
||||||
@useResult
|
@useResult
|
||||||
$Res call({int replyCount, int reactionCount});
|
$Res call({int replyCount, int reactionCount, Map<String, int> reactionList});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@@ -1392,6 +1395,7 @@ class _$SnMetricCopyWithImpl<$Res, $Val extends SnMetric>
|
|||||||
$Res call({
|
$Res call({
|
||||||
Object? replyCount = null,
|
Object? replyCount = null,
|
||||||
Object? reactionCount = null,
|
Object? reactionCount = null,
|
||||||
|
Object? reactionList = null,
|
||||||
}) {
|
}) {
|
||||||
return _then(_value.copyWith(
|
return _then(_value.copyWith(
|
||||||
replyCount: null == replyCount
|
replyCount: null == replyCount
|
||||||
@@ -1402,6 +1406,10 @@ class _$SnMetricCopyWithImpl<$Res, $Val extends SnMetric>
|
|||||||
? _value.reactionCount
|
? _value.reactionCount
|
||||||
: reactionCount // ignore: cast_nullable_to_non_nullable
|
: reactionCount // ignore: cast_nullable_to_non_nullable
|
||||||
as int,
|
as int,
|
||||||
|
reactionList: null == reactionList
|
||||||
|
? _value.reactionList
|
||||||
|
: reactionList // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<String, int>,
|
||||||
) as $Val);
|
) as $Val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1414,7 +1422,7 @@ abstract class _$$SnMetricImplCopyWith<$Res>
|
|||||||
__$$SnMetricImplCopyWithImpl<$Res>;
|
__$$SnMetricImplCopyWithImpl<$Res>;
|
||||||
@override
|
@override
|
||||||
@useResult
|
@useResult
|
||||||
$Res call({int replyCount, int reactionCount});
|
$Res call({int replyCount, int reactionCount, Map<String, int> reactionList});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@@ -1432,6 +1440,7 @@ class __$$SnMetricImplCopyWithImpl<$Res>
|
|||||||
$Res call({
|
$Res call({
|
||||||
Object? replyCount = null,
|
Object? replyCount = null,
|
||||||
Object? reactionCount = null,
|
Object? reactionCount = null,
|
||||||
|
Object? reactionList = null,
|
||||||
}) {
|
}) {
|
||||||
return _then(_$SnMetricImpl(
|
return _then(_$SnMetricImpl(
|
||||||
replyCount: null == replyCount
|
replyCount: null == replyCount
|
||||||
@@ -1442,6 +1451,10 @@ class __$$SnMetricImplCopyWithImpl<$Res>
|
|||||||
? _value.reactionCount
|
? _value.reactionCount
|
||||||
: reactionCount // ignore: cast_nullable_to_non_nullable
|
: reactionCount // ignore: cast_nullable_to_non_nullable
|
||||||
as int,
|
as int,
|
||||||
|
reactionList: null == reactionList
|
||||||
|
? _value._reactionList
|
||||||
|
: reactionList // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<String, int>,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1449,7 +1462,11 @@ class __$$SnMetricImplCopyWithImpl<$Res>
|
|||||||
/// @nodoc
|
/// @nodoc
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class _$SnMetricImpl implements _SnMetric {
|
class _$SnMetricImpl implements _SnMetric {
|
||||||
const _$SnMetricImpl({required this.replyCount, required this.reactionCount});
|
const _$SnMetricImpl(
|
||||||
|
{required this.replyCount,
|
||||||
|
required this.reactionCount,
|
||||||
|
final Map<String, int> reactionList = const {}})
|
||||||
|
: _reactionList = reactionList;
|
||||||
|
|
||||||
factory _$SnMetricImpl.fromJson(Map<String, dynamic> json) =>
|
factory _$SnMetricImpl.fromJson(Map<String, dynamic> json) =>
|
||||||
_$$SnMetricImplFromJson(json);
|
_$$SnMetricImplFromJson(json);
|
||||||
@@ -1458,10 +1475,18 @@ class _$SnMetricImpl implements _SnMetric {
|
|||||||
final int replyCount;
|
final int replyCount;
|
||||||
@override
|
@override
|
||||||
final int reactionCount;
|
final int reactionCount;
|
||||||
|
final Map<String, int> _reactionList;
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
Map<String, int> get reactionList {
|
||||||
|
if (_reactionList is EqualUnmodifiableMapView) return _reactionList;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableMapView(_reactionList);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'SnMetric(replyCount: $replyCount, reactionCount: $reactionCount)';
|
return 'SnMetric(replyCount: $replyCount, reactionCount: $reactionCount, reactionList: $reactionList)';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -1472,12 +1497,15 @@ class _$SnMetricImpl implements _SnMetric {
|
|||||||
(identical(other.replyCount, replyCount) ||
|
(identical(other.replyCount, replyCount) ||
|
||||||
other.replyCount == replyCount) &&
|
other.replyCount == replyCount) &&
|
||||||
(identical(other.reactionCount, reactionCount) ||
|
(identical(other.reactionCount, reactionCount) ||
|
||||||
other.reactionCount == reactionCount));
|
other.reactionCount == reactionCount) &&
|
||||||
|
const DeepCollectionEquality()
|
||||||
|
.equals(other._reactionList, _reactionList));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType, replyCount, reactionCount);
|
int get hashCode => Object.hash(runtimeType, replyCount, reactionCount,
|
||||||
|
const DeepCollectionEquality().hash(_reactionList));
|
||||||
|
|
||||||
/// Create a copy of SnMetric
|
/// Create a copy of SnMetric
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@@ -1498,7 +1526,8 @@ class _$SnMetricImpl implements _SnMetric {
|
|||||||
abstract class _SnMetric implements SnMetric {
|
abstract class _SnMetric implements SnMetric {
|
||||||
const factory _SnMetric(
|
const factory _SnMetric(
|
||||||
{required final int replyCount,
|
{required final int replyCount,
|
||||||
required final int reactionCount}) = _$SnMetricImpl;
|
required final int reactionCount,
|
||||||
|
final Map<String, int> reactionList}) = _$SnMetricImpl;
|
||||||
|
|
||||||
factory _SnMetric.fromJson(Map<String, dynamic> json) =
|
factory _SnMetric.fromJson(Map<String, dynamic> json) =
|
||||||
_$SnMetricImpl.fromJson;
|
_$SnMetricImpl.fromJson;
|
||||||
@@ -1507,6 +1536,8 @@ abstract class _SnMetric implements SnMetric {
|
|||||||
int get replyCount;
|
int get replyCount;
|
||||||
@override
|
@override
|
||||||
int get reactionCount;
|
int get reactionCount;
|
||||||
|
@override
|
||||||
|
Map<String, int> get reactionList;
|
||||||
|
|
||||||
/// Create a copy of SnMetric
|
/// Create a copy of SnMetric
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
|||||||
@@ -20,14 +20,23 @@ _$SnPostImpl _$$SnPostImplFromJson(Map<String, dynamic> json) => _$SnPostImpl(
|
|||||||
aliasPrefix: json['alias_prefix'] as String?,
|
aliasPrefix: json['alias_prefix'] as String?,
|
||||||
tags: json['tags'] as List<dynamic>,
|
tags: json['tags'] as List<dynamic>,
|
||||||
categories: json['categories'] as List<dynamic>,
|
categories: json['categories'] as List<dynamic>,
|
||||||
reactions: json['reactions'],
|
replies: (json['replies'] as List<dynamic>?)
|
||||||
replies: json['replies'],
|
?.map((e) => SnPost.fromJson(e as Map<String, dynamic>))
|
||||||
replyId: json['reply_id'],
|
.toList(),
|
||||||
repostId: json['repost_id'],
|
replyId: (json['reply_id'] as num?)?.toInt(),
|
||||||
replyTo: json['reply_to'],
|
repostId: (json['repost_id'] as num?)?.toInt(),
|
||||||
repostTo: json['repost_to'],
|
replyTo: json['reply_to'] == null
|
||||||
visibleUsersList: json['visible_users_list'],
|
? null
|
||||||
invisibleUsersList: json['invisible_users_list'],
|
: SnPost.fromJson(json['reply_to'] as Map<String, dynamic>),
|
||||||
|
repostTo: json['repost_to'] == null
|
||||||
|
? null
|
||||||
|
: SnPost.fromJson(json['repost_to'] as Map<String, dynamic>),
|
||||||
|
visibleUsersList: (json['visible_users_list'] as List<dynamic>?)
|
||||||
|
?.map((e) => (e as num).toInt())
|
||||||
|
.toList(),
|
||||||
|
invisibleUsersList: (json['invisible_users_list'] as List<dynamic>?)
|
||||||
|
?.map((e) => (e as num).toInt())
|
||||||
|
.toList(),
|
||||||
visibility: (json['visibility'] as num).toInt(),
|
visibility: (json['visibility'] as num).toInt(),
|
||||||
editedAt: json['edited_at'] == null
|
editedAt: json['edited_at'] == null
|
||||||
? null
|
? null
|
||||||
@@ -42,11 +51,11 @@ _$SnPostImpl _$$SnPostImplFromJson(Map<String, dynamic> json) => _$SnPostImpl(
|
|||||||
publishedAt: json['published_at'] == null
|
publishedAt: json['published_at'] == null
|
||||||
? null
|
? null
|
||||||
: DateTime.parse(json['published_at'] as String),
|
: DateTime.parse(json['published_at'] as String),
|
||||||
publishedUntil: json['published_until'],
|
publishedUntil: json['published_until'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['published_until'] as String),
|
||||||
totalUpvote: (json['total_upvote'] as num).toInt(),
|
totalUpvote: (json['total_upvote'] as num).toInt(),
|
||||||
totalDownvote: (json['total_downvote'] as num).toInt(),
|
totalDownvote: (json['total_downvote'] as num).toInt(),
|
||||||
realmId: (json['realm_id'] as num?)?.toInt(),
|
|
||||||
realm: json['realm'],
|
|
||||||
publisherId: (json['publisher_id'] as num).toInt(),
|
publisherId: (json['publisher_id'] as num).toInt(),
|
||||||
publisher:
|
publisher:
|
||||||
SnPublisher.fromJson(json['publisher'] as Map<String, dynamic>),
|
SnPublisher.fromJson(json['publisher'] as Map<String, dynamic>),
|
||||||
@@ -69,12 +78,11 @@ Map<String, dynamic> _$$SnPostImplToJson(_$SnPostImpl instance) =>
|
|||||||
'alias_prefix': instance.aliasPrefix,
|
'alias_prefix': instance.aliasPrefix,
|
||||||
'tags': instance.tags,
|
'tags': instance.tags,
|
||||||
'categories': instance.categories,
|
'categories': instance.categories,
|
||||||
'reactions': instance.reactions,
|
'replies': instance.replies?.map((e) => e.toJson()).toList(),
|
||||||
'replies': instance.replies,
|
|
||||||
'reply_id': instance.replyId,
|
'reply_id': instance.replyId,
|
||||||
'repost_id': instance.repostId,
|
'repost_id': instance.repostId,
|
||||||
'reply_to': instance.replyTo,
|
'reply_to': instance.replyTo?.toJson(),
|
||||||
'repost_to': instance.repostTo,
|
'repost_to': instance.repostTo?.toJson(),
|
||||||
'visible_users_list': instance.visibleUsersList,
|
'visible_users_list': instance.visibleUsersList,
|
||||||
'invisible_users_list': instance.invisibleUsersList,
|
'invisible_users_list': instance.invisibleUsersList,
|
||||||
'visibility': instance.visibility,
|
'visibility': instance.visibility,
|
||||||
@@ -83,11 +91,9 @@ Map<String, dynamic> _$$SnPostImplToJson(_$SnPostImpl instance) =>
|
|||||||
'locked_at': instance.lockedAt?.toIso8601String(),
|
'locked_at': instance.lockedAt?.toIso8601String(),
|
||||||
'is_draft': instance.isDraft,
|
'is_draft': instance.isDraft,
|
||||||
'published_at': instance.publishedAt?.toIso8601String(),
|
'published_at': instance.publishedAt?.toIso8601String(),
|
||||||
'published_until': instance.publishedUntil,
|
'published_until': instance.publishedUntil?.toIso8601String(),
|
||||||
'total_upvote': instance.totalUpvote,
|
'total_upvote': instance.totalUpvote,
|
||||||
'total_downvote': instance.totalDownvote,
|
'total_downvote': instance.totalDownvote,
|
||||||
'realm_id': instance.realmId,
|
|
||||||
'realm': instance.realm,
|
|
||||||
'publisher_id': instance.publisherId,
|
'publisher_id': instance.publisherId,
|
||||||
'publisher': instance.publisher.toJson(),
|
'publisher': instance.publisher.toJson(),
|
||||||
'metric': instance.metric.toJson(),
|
'metric': instance.metric.toJson(),
|
||||||
@@ -129,12 +135,17 @@ _$SnMetricImpl _$$SnMetricImplFromJson(Map<String, dynamic> json) =>
|
|||||||
_$SnMetricImpl(
|
_$SnMetricImpl(
|
||||||
replyCount: (json['reply_count'] as num).toInt(),
|
replyCount: (json['reply_count'] as num).toInt(),
|
||||||
reactionCount: (json['reaction_count'] as num).toInt(),
|
reactionCount: (json['reaction_count'] as num).toInt(),
|
||||||
|
reactionList: (json['reaction_list'] as Map<String, dynamic>?)?.map(
|
||||||
|
(k, e) => MapEntry(k, (e as num).toInt()),
|
||||||
|
) ??
|
||||||
|
const {},
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$$SnMetricImplToJson(_$SnMetricImpl instance) =>
|
Map<String, dynamic> _$$SnMetricImplToJson(_$SnMetricImpl instance) =>
|
||||||
<String, dynamic>{
|
<String, dynamic>{
|
||||||
'reply_count': instance.replyCount,
|
'reply_count': instance.replyCount,
|
||||||
'reaction_count': instance.reactionCount,
|
'reaction_count': instance.reactionCount,
|
||||||
|
'reaction_list': instance.reactionList,
|
||||||
};
|
};
|
||||||
|
|
||||||
_$SnPublisherImpl _$$SnPublisherImplFromJson(Map<String, dynamic> json) =>
|
_$SnPublisherImpl _$$SnPublisherImplFromJson(Map<String, dynamic> json) =>
|
||||||
|
|||||||
20
lib/types/reaction.dart
Normal file
20
lib/types/reaction.dart
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
class ReactInfo {
|
||||||
|
final String icon;
|
||||||
|
final int attitude;
|
||||||
|
|
||||||
|
const ReactInfo({required this.icon, required this.attitude});
|
||||||
|
}
|
||||||
|
|
||||||
|
const Map<String, ReactInfo> kTemplateReactions = {
|
||||||
|
'thumb_up': ReactInfo(icon: '👍', attitude: 1),
|
||||||
|
'thumb_down': ReactInfo(icon: '👎', attitude: 2),
|
||||||
|
'just_okay': ReactInfo(icon: '😅', attitude: 0),
|
||||||
|
'cry': ReactInfo(icon: '😭', attitude: 0),
|
||||||
|
'confuse': ReactInfo(icon: '🧐', attitude: 0),
|
||||||
|
'clap': ReactInfo(icon: '👏', attitude: 1),
|
||||||
|
'laugh': ReactInfo(icon: '😂', attitude: 1),
|
||||||
|
'angry': ReactInfo(icon: '😡', attitude: 2),
|
||||||
|
'party': ReactInfo(icon: '🎉', attitude: 1),
|
||||||
|
'joy': ReactInfo(icon: '🤣', attitude: 1),
|
||||||
|
'pray': ReactInfo(icon: '🙏', attitude: 1),
|
||||||
|
};
|
||||||
@@ -35,6 +35,7 @@ class AccountImage extends StatelessWidget {
|
|||||||
UniversalImage.provider(url),
|
UniversalImage.provider(url),
|
||||||
width: ((radius ?? 20) * devicePixelRatio * 2).round(),
|
width: ((radius ?? 20) * devicePixelRatio * 2).round(),
|
||||||
height: ((radius ?? 20) * devicePixelRatio * 2).round(),
|
height: ((radius ?? 20) * devicePixelRatio * 2).round(),
|
||||||
|
policy: ResizeImagePolicy.fit,
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
child: (content?.isEmpty ?? true)
|
child: (content?.isEmpty ?? true)
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ class AttachmentDetailPopup extends StatelessWidget {
|
|||||||
child: Hero(
|
child: Hero(
|
||||||
tag: 'attachment-${data.rid}-${heroTag ?? uuid.v4()}',
|
tag: 'attachment-${data.rid}-${heroTag ?? uuid.v4()}',
|
||||||
child: PhotoView(
|
child: PhotoView(
|
||||||
|
key: Key('attachment-detail-${data.rid}-$heroTag'),
|
||||||
|
backgroundDecoration: BoxDecoration(
|
||||||
|
color: Colors.black.withOpacity(0.7),
|
||||||
|
),
|
||||||
imageProvider: UniversalImage.provider(
|
imageProvider: UniversalImage.provider(
|
||||||
sn.getAttachmentUrl(data.rid),
|
sn.getAttachmentUrl(data.rid),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:dismissible_page/dismissible_page.dart';
|
import 'package:dismissible_page/dismissible_page.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
import 'package:surface/types/attachment.dart';
|
import 'package:surface/types/attachment.dart';
|
||||||
import 'package:surface/widgets/attachment/attachment_detail.dart';
|
import 'package:surface/widgets/attachment/attachment_detail.dart';
|
||||||
@@ -23,8 +29,9 @@ class AttachmentItem extends StatelessWidget {
|
|||||||
case 'image':
|
case 'image':
|
||||||
return Hero(
|
return Hero(
|
||||||
tag: 'attachment-${data.rid}-$heroTag',
|
tag: 'attachment-${data.rid}-$heroTag',
|
||||||
child: UniversalImage(
|
child: AutoResizeUniversalImage(
|
||||||
sn.getAttachmentUrl(data.rid),
|
sn.getAttachmentUrl(data.rid),
|
||||||
|
key: Key('attachment-${data.rid}-$heroTag'),
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -38,6 +45,12 @@ class AttachmentItem extends StatelessWidget {
|
|||||||
final uuid = Uuid();
|
final uuid = Uuid();
|
||||||
final heroTag = uuid.v4();
|
final heroTag = uuid.v4();
|
||||||
|
|
||||||
|
if (data.isMature) {
|
||||||
|
return _AttachmentItemSensitiveBlur(
|
||||||
|
child: _buildContent(context, heroTag),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (isExpandable) {
|
if (isExpandable) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
child: _buildContent(context, heroTag),
|
child: _buildContent(context, heroTag),
|
||||||
@@ -53,3 +66,87 @@ class AttachmentItem extends StatelessWidget {
|
|||||||
return _buildContent(context, heroTag);
|
return _buildContent(context, heroTag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _AttachmentItemSensitiveBlur extends StatefulWidget {
|
||||||
|
final Widget child;
|
||||||
|
const _AttachmentItemSensitiveBlur({super.key, required this.child});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_AttachmentItemSensitiveBlur> createState() =>
|
||||||
|
_AttachmentItemSensitiveBlurState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AttachmentItemSensitiveBlurState
|
||||||
|
extends State<_AttachmentItemSensitiveBlur> {
|
||||||
|
bool _doesShow = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
widget.child,
|
||||||
|
ClipRect(
|
||||||
|
child: BackdropFilter(
|
||||||
|
filter: ImageFilter.blur(sigmaX: 40, sigmaY: 40),
|
||||||
|
child: Container(
|
||||||
|
color: Colors.black.withOpacity(0.5),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Symbols.visibility_off,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 32,
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
Text('sensitiveContent')
|
||||||
|
.tr()
|
||||||
|
.fontSize(20)
|
||||||
|
.textColor(Colors.white)
|
||||||
|
.bold(),
|
||||||
|
Text('sensitiveContentDescription')
|
||||||
|
.tr()
|
||||||
|
.fontSize(14)
|
||||||
|
.textColor(Colors.white.withOpacity(0.8)),
|
||||||
|
const Gap(16),
|
||||||
|
InkWell(
|
||||||
|
child: Text('sensitiveContentReveal')
|
||||||
|
.tr()
|
||||||
|
.textColor(Colors.white),
|
||||||
|
onTap: () {
|
||||||
|
setState(() => _doesShow = !_doesShow);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.opacity(_doesShow ? 0 : 1, animate: true)
|
||||||
|
.animate(const Duration(milliseconds: 300), Curves.easeInOut),
|
||||||
|
if (_doesShow)
|
||||||
|
Positioned(
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
child: InkWell(
|
||||||
|
child: Icon(
|
||||||
|
Symbols.visibility_off,
|
||||||
|
color: Colors.white,
|
||||||
|
shadows: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.5),
|
||||||
|
blurRadius: 3,
|
||||||
|
offset: Offset(0, 1.5),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(all: 12),
|
||||||
|
onTap: () {
|
||||||
|
setState(() => _doesShow = !_doesShow);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,15 +10,15 @@ import 'package:surface/widgets/attachment/attachment_item.dart';
|
|||||||
class AttachmentList extends StatelessWidget {
|
class AttachmentList extends StatelessWidget {
|
||||||
final List<SnAttachment> data;
|
final List<SnAttachment> data;
|
||||||
final bool? bordered;
|
final bool? bordered;
|
||||||
final double? maxListHeight;
|
final double? maxHeight;
|
||||||
const AttachmentList({
|
const AttachmentList({
|
||||||
super.key,
|
super.key,
|
||||||
required this.data,
|
required this.data,
|
||||||
this.bordered,
|
this.bordered,
|
||||||
this.maxListHeight,
|
this.maxHeight,
|
||||||
});
|
});
|
||||||
|
|
||||||
static const double kMaxListItemWidth = 520;
|
static const double kMaxItemWidth = 520;
|
||||||
static const BorderRadius kDefaultRadius =
|
static const BorderRadius kDefaultRadius =
|
||||||
BorderRadius.all(Radius.circular(8));
|
BorderRadius.all(Radius.circular(8));
|
||||||
|
|
||||||
@@ -33,9 +33,10 @@ class AttachmentList extends StatelessWidget {
|
|||||||
if (ResponsiveBreakpoints.of(context).largerThan(MOBILE)) {
|
if (ResponsiveBreakpoints.of(context).largerThan(MOBILE)) {
|
||||||
return Container(
|
return Container(
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
|
maxHeight: maxHeight ?? double.infinity,
|
||||||
maxWidth: math.min(
|
maxWidth: math.min(
|
||||||
MediaQuery.of(context).size.width - 20,
|
MediaQuery.of(context).size.width - 20,
|
||||||
kMaxListItemWidth,
|
kMaxItemWidth,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -64,7 +65,7 @@ class AttachmentList extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
constraints: BoxConstraints(maxHeight: maxListHeight ?? 320),
|
constraints: BoxConstraints(maxHeight: maxHeight ?? 320),
|
||||||
child: ScrollConfiguration(
|
child: ScrollConfiguration(
|
||||||
behavior: _AttachmentListScrollBehavior(),
|
behavior: _AttachmentListScrollBehavior(),
|
||||||
child: ListView.separated(
|
child: ListView.separated(
|
||||||
@@ -73,9 +74,10 @@ class AttachmentList extends StatelessWidget {
|
|||||||
itemBuilder: (context, idx) {
|
itemBuilder: (context, idx) {
|
||||||
return Container(
|
return Container(
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
|
maxHeight: maxHeight ?? double.infinity,
|
||||||
maxWidth: math.min(
|
maxWidth: math.min(
|
||||||
MediaQuery.of(context).size.width - 20,
|
MediaQuery.of(context).size.width - 20,
|
||||||
kMaxListItemWidth,
|
kMaxItemWidth,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
class AppBackground extends StatelessWidget {
|
class AppBackground extends StatelessWidget {
|
||||||
final Widget child;
|
final Widget child;
|
||||||
@@ -6,6 +10,52 @@ class AppBackground extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ScaffoldMessenger(child: child);
|
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||||
|
|
||||||
|
return ScaffoldMessenger(
|
||||||
|
child: FutureBuilder(
|
||||||
|
future:
|
||||||
|
kIsWeb ? Future.value(null) : getApplicationDocumentsDirectory(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.hasData) {
|
||||||
|
final path = '${snapshot.data!.path}/app_background_image';
|
||||||
|
final file = File(path);
|
||||||
|
if (file.existsSync()) {
|
||||||
|
return Container(
|
||||||
|
color: Theme.of(context).colorScheme.surface,
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
backgroundBlendMode: BlendMode.darken,
|
||||||
|
color: Theme.of(context).colorScheme.surface,
|
||||||
|
image: DecorationImage(
|
||||||
|
opacity: 0.2,
|
||||||
|
image: ResizeImage(
|
||||||
|
FileImage(file),
|
||||||
|
width: (constraints.maxWidth * devicePixelRatio)
|
||||||
|
.round(),
|
||||||
|
height: (constraints.maxHeight * devicePixelRatio)
|
||||||
|
.round(),
|
||||||
|
policy: ResizeImagePolicy.fit,
|
||||||
|
),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Material(
|
||||||
|
color: Theme.of(context).colorScheme.surface,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:surface/widgets/navigation/app_destinations.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:surface/providers/navigation.dart';
|
||||||
|
|
||||||
class AppBottomNavigationBar extends StatefulWidget {
|
class AppBottomNavigationBar extends StatefulWidget {
|
||||||
const AppBottomNavigationBar({super.key});
|
const AppBottomNavigationBar({super.key});
|
||||||
@@ -10,23 +12,46 @@ class AppBottomNavigationBar extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _AppBottomNavigationBarState extends State<AppBottomNavigationBar> {
|
class _AppBottomNavigationBarState extends State<AppBottomNavigationBar> {
|
||||||
int _currentIndex = 0;
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
context
|
||||||
|
.read<NavigationProvider>()
|
||||||
|
.autoDetectIndex(GoRouter.maybeOf(context));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BottomNavigationBar(
|
final nav = context.watch<NavigationProvider>();
|
||||||
currentIndex: _currentIndex,
|
|
||||||
type: BottomNavigationBarType.fixed,
|
return ListenableBuilder(
|
||||||
showUnselectedLabels: false,
|
listenable: nav,
|
||||||
items: appDestinations.map((ele) {
|
builder: (context, _) {
|
||||||
return BottomNavigationBarItem(
|
if (!nav.isIndexInRange(0, nav.pinnedDestinationCount)) {
|
||||||
icon: ele.icon,
|
return const SizedBox.shrink();
|
||||||
label: ele.label,
|
}
|
||||||
|
|
||||||
|
final destinations = [
|
||||||
|
...nav.destinations.where((ele) => ele.isPinned),
|
||||||
|
];
|
||||||
|
|
||||||
|
return BottomNavigationBar(
|
||||||
|
currentIndex: nav.getIndexInRange(0, nav.pinnedDestinationCount),
|
||||||
|
type: BottomNavigationBarType.fixed,
|
||||||
|
showUnselectedLabels: false,
|
||||||
|
items: destinations.map((ele) {
|
||||||
|
return BottomNavigationBarItem(
|
||||||
|
icon: ele.icon,
|
||||||
|
label: ele.label.tr(),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
onTap: (idx) {
|
||||||
|
nav.setIndex(idx);
|
||||||
|
GoRouter.of(context).goNamed(destinations[idx].screen);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}).toList(),
|
|
||||||
onTap: (idx) {
|
|
||||||
setState(() => _currentIndex = idx);
|
|
||||||
GoRouter.of(context).goNamed(appDestinations[idx].screen);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
|
||||||
|
|
||||||
class AppNavDestination {
|
|
||||||
final String label;
|
|
||||||
final String screen;
|
|
||||||
final Widget icon;
|
|
||||||
|
|
||||||
AppNavDestination({
|
|
||||||
required this.label,
|
|
||||||
required this.screen,
|
|
||||||
required this.icon,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
List<AppNavDestination> appDestinations = [
|
|
||||||
AppNavDestination(
|
|
||||||
icon: Icon(Symbols.home, weight: 400, opticalSize: 20),
|
|
||||||
screen: 'home',
|
|
||||||
label: tr('screenHome'),
|
|
||||||
),
|
|
||||||
AppNavDestination(
|
|
||||||
icon: Icon(Symbols.explore, weight: 400, opticalSize: 20),
|
|
||||||
screen: 'explore',
|
|
||||||
label: tr('screenExplore'),
|
|
||||||
),
|
|
||||||
AppNavDestination(
|
|
||||||
icon: Icon(Symbols.account_circle, weight: 400, opticalSize: 20),
|
|
||||||
screen: 'account',
|
|
||||||
label: tr('screenAccount'),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
85
lib/widgets/navigation/app_drawer_navigation.dart
Normal file
85
lib/widgets/navigation/app_drawer_navigation.dart
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:responsive_framework/responsive_framework.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:surface/providers/navigation.dart';
|
||||||
|
|
||||||
|
class AppNavigationDrawer extends StatefulWidget {
|
||||||
|
const AppNavigationDrawer({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AppNavigationDrawer> createState() => _AppNavigationDrawerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
context
|
||||||
|
.read<NavigationProvider>()
|
||||||
|
.autoDetectIndex(GoRouter.maybeOf(context));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final nav = context.watch<NavigationProvider>();
|
||||||
|
|
||||||
|
final backgroundColor = ResponsiveBreakpoints.of(context).largerThan(MOBILE)
|
||||||
|
? Theme.of(context).colorScheme.surface
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return ListenableBuilder(
|
||||||
|
listenable: nav,
|
||||||
|
builder: (context, _) {
|
||||||
|
final destinations = [
|
||||||
|
...nav.destinations.where((ele) => ele.isPinned),
|
||||||
|
...nav.destinations.where((ele) => !ele.isPinned),
|
||||||
|
];
|
||||||
|
|
||||||
|
return NavigationDrawer(
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
selectedIndex: nav.currentIndex,
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('Solar Network').bold(),
|
||||||
|
Text('Solar Network 2.0α').fontSize(12).textColor(
|
||||||
|
Theme.of(context).colorScheme.onSurface.withOpacity(0.5)),
|
||||||
|
],
|
||||||
|
).padding(
|
||||||
|
horizontal: 32,
|
||||||
|
top: math.max(MediaQuery.of(context).padding.top, 16),
|
||||||
|
bottom: 16,
|
||||||
|
),
|
||||||
|
...destinations.where((ele) => ele.isPinned).map((ele) {
|
||||||
|
return NavigationDrawerDestination(
|
||||||
|
icon: ele.icon,
|
||||||
|
label: Text(ele.label).tr(),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
const Divider(),
|
||||||
|
...destinations.where((ele) => !ele.isPinned).map((ele) {
|
||||||
|
return NavigationDrawerDestination(
|
||||||
|
icon: ele.icon,
|
||||||
|
label: Text(ele.label).tr(),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
onDestinationSelected: (idx) {
|
||||||
|
nav.setIndex(idx);
|
||||||
|
GoRouter.of(context).goNamed(destinations[idx].screen);
|
||||||
|
Scaffold.of(context).closeDrawer();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import 'package:responsive_framework/responsive_framework.dart';
|
|||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/navigation/app_background.dart';
|
import 'package:surface/widgets/navigation/app_background.dart';
|
||||||
import 'package:surface/widgets/navigation/app_bottom_navigation.dart';
|
import 'package:surface/widgets/navigation/app_bottom_navigation.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_drawer_navigation.dart';
|
||||||
|
|
||||||
class AppScaffold extends StatelessWidget {
|
class AppScaffold extends StatelessWidget {
|
||||||
final PreferredSizeWidget? appBar;
|
final PreferredSizeWidget? appBar;
|
||||||
@@ -14,6 +15,7 @@ class AppScaffold extends StatelessWidget {
|
|||||||
final Widget? body;
|
final Widget? body;
|
||||||
final bool autoImplyAppBar;
|
final bool autoImplyAppBar;
|
||||||
final bool showBottomNavigation;
|
final bool showBottomNavigation;
|
||||||
|
final bool showDrawer;
|
||||||
const AppScaffold({
|
const AppScaffold({
|
||||||
super.key,
|
super.key,
|
||||||
this.appBar,
|
this.appBar,
|
||||||
@@ -23,17 +25,21 @@ class AppScaffold extends StatelessWidget {
|
|||||||
this.body,
|
this.body,
|
||||||
this.autoImplyAppBar = false,
|
this.autoImplyAppBar = false,
|
||||||
this.showBottomNavigation = false,
|
this.showBottomNavigation = false,
|
||||||
|
this.showDrawer = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final isShowDrawer = showDrawer
|
||||||
|
? ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE)
|
||||||
|
: false;
|
||||||
final isShowBottomNavigation = (showBottomNavigation)
|
final isShowBottomNavigation = (showBottomNavigation)
|
||||||
? ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE)
|
? ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE)
|
||||||
: false;
|
: false;
|
||||||
|
|
||||||
final state = GoRouter.maybeOf(context);
|
final state = GoRouter.maybeOf(context);
|
||||||
|
|
||||||
return AppBackground(
|
final innerWidget = AppBackground(
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: appBar ??
|
appBar: appBar ??
|
||||||
(autoImplyAppBar
|
(autoImplyAppBar
|
||||||
@@ -50,9 +56,22 @@ class AppScaffold extends StatelessWidget {
|
|||||||
body: body,
|
body: body,
|
||||||
floatingActionButtonLocation: floatingActionButtonLocation,
|
floatingActionButtonLocation: floatingActionButtonLocation,
|
||||||
floatingActionButton: floatingActionButton,
|
floatingActionButton: floatingActionButton,
|
||||||
|
drawer: isShowDrawer ? AppNavigationDrawer() : null,
|
||||||
bottomNavigationBar:
|
bottomNavigationBar:
|
||||||
isShowBottomNavigation ? AppBottomNavigationBar() : null,
|
isShowBottomNavigation ? AppBottomNavigationBar() : null,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (showDrawer) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
AppNavigationDrawer(),
|
||||||
|
VerticalDivider(width: 1, color: Theme.of(context).dividerColor),
|
||||||
|
Expanded(child: innerWidget),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return innerWidget;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
171
lib/widgets/post/post_comment_list.dart
Normal file
171
lib/widgets/post/post_comment_list.dart
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:surface/providers/sn_attachment.dart';
|
||||||
|
import 'package:surface/providers/sn_network.dart';
|
||||||
|
import 'package:surface/types/post.dart';
|
||||||
|
import 'package:surface/widgets/post/post_item.dart';
|
||||||
|
import 'package:surface/widgets/post/post_mini_editor.dart';
|
||||||
|
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||||
|
|
||||||
|
class PostCommentSliverList extends StatefulWidget {
|
||||||
|
final int parentPostId;
|
||||||
|
const PostCommentSliverList({super.key, required this.parentPostId});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PostCommentSliverList> createState() => PostCommentSliverListState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class PostCommentSliverListState extends State<PostCommentSliverList> {
|
||||||
|
bool _isBusy = true;
|
||||||
|
|
||||||
|
final List<SnPost> _posts = List.empty(growable: true);
|
||||||
|
int? _postCount;
|
||||||
|
|
||||||
|
Future<void> _fetchPosts() async {
|
||||||
|
if (_postCount != null && _posts.length >= _postCount!) return;
|
||||||
|
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
final resp = await sn.client.get(
|
||||||
|
'/cgi/co/posts/${widget.parentPostId}/replies',
|
||||||
|
queryParameters: {
|
||||||
|
'take': 10,
|
||||||
|
'offset': _posts.length,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
final List<SnPost> out =
|
||||||
|
List.from(resp.data['data']?.map((e) => SnPost.fromJson(e)) ?? []);
|
||||||
|
|
||||||
|
Set<String> rids = {};
|
||||||
|
for (var i = 0; i < out.length; i++) {
|
||||||
|
rids.addAll(out[i].body['attachments']?.cast<String>() ?? []);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
final attach = context.read<SnAttachmentProvider>();
|
||||||
|
final attachments = await attach.getMultiple(rids.toList());
|
||||||
|
for (var i = 0; i < out.length; i++) {
|
||||||
|
out[i] = out[i].copyWith(
|
||||||
|
preload: SnPostPreload(
|
||||||
|
attachments: attachments
|
||||||
|
.where(
|
||||||
|
(ele) => out[i].body['attachments']?.contains(ele.rid) ?? false,
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_postCount = resp.data['count'];
|
||||||
|
_posts.addAll(out);
|
||||||
|
|
||||||
|
if (mounted) setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> refresh() async {
|
||||||
|
_posts.clear();
|
||||||
|
_fetchPosts();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_fetchPosts();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SliverInfiniteList(
|
||||||
|
itemCount: _posts.length,
|
||||||
|
isLoading: _isBusy,
|
||||||
|
hasReachedMax: _postCount != null && _posts.length >= _postCount!,
|
||||||
|
onFetchData: _fetchPosts,
|
||||||
|
itemBuilder: (context, idx) {
|
||||||
|
return GestureDetector(
|
||||||
|
child: PostItem(data: _posts[idx]),
|
||||||
|
onTap: () {
|
||||||
|
GoRouter.of(context).pushNamed(
|
||||||
|
'postDetail',
|
||||||
|
pathParameters: {'slug': _posts[idx].id.toString()},
|
||||||
|
extra: _posts[idx],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
separatorBuilder: (context, index) => const Divider(height: 1),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PostCommentListPopup extends StatefulWidget {
|
||||||
|
final int postId;
|
||||||
|
final int commentCount;
|
||||||
|
const PostCommentListPopup({
|
||||||
|
super.key,
|
||||||
|
required this.postId,
|
||||||
|
this.commentCount = 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PostCommentListPopup> createState() => _PostCommentListPopupState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PostCommentListPopupState extends State<PostCommentListPopup> {
|
||||||
|
final GlobalKey<PostCommentSliverListState> _childListKey = GlobalKey();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.comment, size: 24),
|
||||||
|
const Gap(16),
|
||||||
|
Text('postCommentsDetailed')
|
||||||
|
.plural(widget.commentCount)
|
||||||
|
.textStyle(Theme.of(context).textTheme.titleLarge!),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||||
|
Expanded(
|
||||||
|
child: CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Container(
|
||||||
|
height: 240,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.symmetric(
|
||||||
|
horizontal: BorderSide(
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
width: 1 / devicePixelRatio,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: PostMiniEditor(
|
||||||
|
postReplyId: widget.postId,
|
||||||
|
onPost: () {
|
||||||
|
_childListKey.currentState!.refresh();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
PostCommentSliverList(
|
||||||
|
key: _childListKey,
|
||||||
|
parentPostId: widget.postId,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart';
|
|||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:relative_time/relative_time.dart';
|
import 'package:relative_time/relative_time.dart';
|
||||||
|
import 'package:responsive_framework/responsive_framework.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:surface/providers/userinfo.dart';
|
import 'package:surface/providers/userinfo.dart';
|
||||||
import 'package:surface/types/post.dart';
|
import 'package:surface/types/post.dart';
|
||||||
@@ -11,20 +12,145 @@ import 'package:surface/widgets/account/account_image.dart';
|
|||||||
import 'package:surface/widgets/attachment/attachment_list.dart';
|
import 'package:surface/widgets/attachment/attachment_list.dart';
|
||||||
import 'package:surface/widgets/markdown_content.dart';
|
import 'package:surface/widgets/markdown_content.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:surface/widgets/post/post_comment_list.dart';
|
||||||
|
import 'package:surface/widgets/post/post_reaction.dart';
|
||||||
|
|
||||||
class PostItem extends StatelessWidget {
|
class PostItem extends StatelessWidget {
|
||||||
final SnPost data;
|
final SnPost data;
|
||||||
const PostItem({super.key, required this.data});
|
final bool showReactions;
|
||||||
|
final bool showComments;
|
||||||
|
final Function(SnPost data)? onChanged;
|
||||||
|
const PostItem({
|
||||||
|
super.key,
|
||||||
|
required this.data,
|
||||||
|
this.showReactions = true,
|
||||||
|
this.showComments = true,
|
||||||
|
this.onChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
void _onChanged(SnPost data) {
|
||||||
|
if (onChanged != null) onChanged!(data);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final isListAttachments =
|
||||||
|
ResponsiveBreakpoints.of(context).largerThan(MOBILE) ||
|
||||||
|
(data.preload?.attachments?.length ?? 0) > 1;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_PostContentHeader(data: data),
|
_PostContentHeader(data: data).padding(horizontal: 12, vertical: 8),
|
||||||
_PostContentBody(data: data.body).padding(horizontal: 16, bottom: 6),
|
_PostContentBody(data: data.body).padding(horizontal: 16, bottom: 6),
|
||||||
|
if (data.repostTo != null)
|
||||||
|
_PostQuoteContent(child: data.repostTo!).padding(
|
||||||
|
horizontal: 12,
|
||||||
|
),
|
||||||
if (data.preload?.attachments?.isNotEmpty ?? true)
|
if (data.preload?.attachments?.isNotEmpty ?? true)
|
||||||
AttachmentList(data: data.preload!.attachments!, bordered: true),
|
AttachmentList(
|
||||||
|
data: data.preload!.attachments!,
|
||||||
|
bordered: true,
|
||||||
|
maxHeight: 520,
|
||||||
|
).padding(horizontal: isListAttachments ? 12 : 0),
|
||||||
|
_PostBottomAction(
|
||||||
|
data: data,
|
||||||
|
showComments: showComments,
|
||||||
|
showReactions: showReactions,
|
||||||
|
onChanged: _onChanged,
|
||||||
|
).padding(left: 12, right: 18),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PostBottomAction extends StatelessWidget {
|
||||||
|
final SnPost data;
|
||||||
|
final bool showComments;
|
||||||
|
final bool showReactions;
|
||||||
|
final Function(SnPost data) onChanged;
|
||||||
|
const _PostBottomAction({
|
||||||
|
required this.data,
|
||||||
|
required this.showComments,
|
||||||
|
required this.showReactions,
|
||||||
|
required this.onChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final iconColor = Theme.of(context).colorScheme.onSurface.withAlpha(
|
||||||
|
(255 * 0.8).round(),
|
||||||
|
);
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
if (showReactions)
|
||||||
|
InkWell(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Symbols.add_reaction, size: 20, color: iconColor),
|
||||||
|
const Gap(8),
|
||||||
|
if (data.totalDownvote > 0 || data.totalUpvote > 0)
|
||||||
|
Text('postReactionPoints').plural(
|
||||||
|
data.totalUpvote - data.totalDownvote,
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Text('postReact').tr(),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 8, vertical: 8),
|
||||||
|
onTap: () {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => PostReactionPopup(
|
||||||
|
data: data,
|
||||||
|
onChanged: (value, isPositive, delta) {
|
||||||
|
onChanged(data.copyWith(
|
||||||
|
totalUpvote: isPositive
|
||||||
|
? data.totalUpvote + delta
|
||||||
|
: data.totalUpvote,
|
||||||
|
totalDownvote: !isPositive
|
||||||
|
? data.totalDownvote + delta
|
||||||
|
: data.totalDownvote,
|
||||||
|
metric: data.metric.copyWith(reactionList: value),
|
||||||
|
));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (showComments)
|
||||||
|
InkWell(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Symbols.comment, size: 20, color: iconColor),
|
||||||
|
const Gap(8),
|
||||||
|
Text('postComments').plural(data.metric.replyCount),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 8, vertical: 8),
|
||||||
|
onTap: () {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
useRootNavigator: true,
|
||||||
|
builder: (context) => PostCommentListPopup(
|
||||||
|
postId: data.id,
|
||||||
|
commentCount: data.metric.replyCount,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
].expand((ele) => [ele, const Gap(8)]).toList()
|
||||||
|
..removeLast(),
|
||||||
|
),
|
||||||
|
InkWell(
|
||||||
|
child: Icon(
|
||||||
|
Symbols.share,
|
||||||
|
size: 20,
|
||||||
|
color: iconColor,
|
||||||
|
).padding(horizontal: 8, vertical: 8),
|
||||||
|
onTap: () {},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -32,7 +158,13 @@ class PostItem extends StatelessWidget {
|
|||||||
|
|
||||||
class _PostContentHeader extends StatelessWidget {
|
class _PostContentHeader extends StatelessWidget {
|
||||||
final SnPost data;
|
final SnPost data;
|
||||||
const _PostContentHeader({required this.data});
|
final bool isCompact;
|
||||||
|
final bool showActions;
|
||||||
|
const _PostContentHeader({
|
||||||
|
required this.data,
|
||||||
|
this.isCompact = false,
|
||||||
|
this.showActions = true,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -41,13 +173,16 @@ class _PostContentHeader extends StatelessWidget {
|
|||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
AccountImage(content: data.publisher.avatar),
|
AccountImage(
|
||||||
const Gap(12),
|
content: data.publisher.avatar,
|
||||||
Expanded(
|
radius: isCompact ? 12 : 20,
|
||||||
child: Column(
|
),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
Gap(isCompact ? 8 : 12),
|
||||||
|
if (isCompact)
|
||||||
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text(data.publisher.nick).bold(),
|
Text(data.publisher.nick).bold(),
|
||||||
|
const Gap(4),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text('@${data.publisher.name}').fontSize(13),
|
Text('@${data.publisher.name}').fontSize(13),
|
||||||
@@ -58,88 +193,106 @@ class _PostContentHeader extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
).opacity(0.8),
|
).opacity(0.8),
|
||||||
],
|
],
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(data.publisher.nick).bold(),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text('@${data.publisher.name}').fontSize(13),
|
||||||
|
const Gap(4),
|
||||||
|
Text(RelativeTime(context).format(
|
||||||
|
data.publishedAt ?? data.createdAt,
|
||||||
|
)).fontSize(13),
|
||||||
|
],
|
||||||
|
).opacity(0.8),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
if (showActions)
|
||||||
PopupMenuButton(
|
PopupMenuButton(
|
||||||
icon: const Icon(Symbols.more_horiz),
|
icon: const Icon(Symbols.more_horiz),
|
||||||
style: const ButtonStyle(
|
style: const ButtonStyle(
|
||||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||||
),
|
),
|
||||||
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
|
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
|
||||||
if (isAuthor)
|
if (isAuthor)
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.edit),
|
||||||
|
const Gap(16),
|
||||||
|
Text('edit').tr(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
GoRouter.of(context).pushNamed(
|
||||||
|
'postEditor',
|
||||||
|
pathParameters: {'mode': data.typePlural},
|
||||||
|
queryParameters: {'editing': data.id.toString()},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (isAuthor)
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.delete),
|
||||||
|
const Gap(16),
|
||||||
|
Text('delete').tr(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (isAuthor) const PopupMenuDivider(),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Symbols.edit),
|
const Icon(Symbols.reply),
|
||||||
const Gap(16),
|
const Gap(16),
|
||||||
Text('edit').tr(),
|
Text('reply').tr(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
GoRouter.of(context).pushNamed(
|
GoRouter.of(context).pushNamed(
|
||||||
'postEditor',
|
'postEditor',
|
||||||
pathParameters: {'mode': data.typePlural},
|
pathParameters: {'mode': data.typePlural},
|
||||||
queryParameters: {'editing': data.id.toString()},
|
queryParameters: {'replying': data.id.toString()},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (isAuthor)
|
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Symbols.delete),
|
const Icon(Symbols.forward),
|
||||||
const Gap(16),
|
const Gap(16),
|
||||||
Text('delete').tr(),
|
Text('repost').tr(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
GoRouter.of(context).pushNamed(
|
||||||
|
'postEditor',
|
||||||
|
pathParameters: {'mode': data.typePlural},
|
||||||
|
queryParameters: {'reposting': data.id.toString()},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const PopupMenuDivider(),
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.flag),
|
||||||
|
const Gap(16),
|
||||||
|
Text('report').tr(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (isAuthor) const PopupMenuDivider(),
|
],
|
||||||
PopupMenuItem(
|
),
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Symbols.reply),
|
|
||||||
const Gap(16),
|
|
||||||
Text('reply').tr(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
GoRouter.of(context).pushNamed(
|
|
||||||
'postEditor',
|
|
||||||
pathParameters: {'mode': data.typePlural},
|
|
||||||
queryParameters: {'replying': data.id.toString()},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
PopupMenuItem(
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Symbols.forward),
|
|
||||||
const Gap(16),
|
|
||||||
Text('repost').tr(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
GoRouter.of(context).pushNamed(
|
|
||||||
'postEditor',
|
|
||||||
pathParameters: {'mode': data.typePlural},
|
|
||||||
queryParameters: {'reposting': data.id.toString()},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const PopupMenuDivider(),
|
|
||||||
PopupMenuItem(
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Symbols.flag),
|
|
||||||
const Gap(16),
|
|
||||||
Text('report').tr(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
).padding(horizontal: 12, vertical: 8);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,3 +306,29 @@ class _PostContentBody extends StatelessWidget {
|
|||||||
return MarkdownTextContent(content: data['content']);
|
return MarkdownTextContent(content: data['content']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _PostQuoteContent extends StatelessWidget {
|
||||||
|
final SnPost child;
|
||||||
|
const _PostQuoteContent({super.key, required this.child});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_PostContentHeader(data: child, isCompact: true, showActions: false)
|
||||||
|
.padding(bottom: 4),
|
||||||
|
_PostContentBody(data: child.body),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,67 +1,180 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:cross_file/cross_file.dart';
|
import 'package:croppy/croppy.dart';
|
||||||
|
import 'package:dismissible_page/dismissible_page.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_context_menu/flutter_context_menu.dart';
|
import 'package:flutter_context_menu/flutter_context_menu.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:surface/controllers/post_write_controller.dart';
|
||||||
|
import 'package:surface/providers/sn_network.dart';
|
||||||
|
import 'package:surface/widgets/attachment/attachment_detail.dart';
|
||||||
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
|
||||||
class PostMediaPendingList extends StatelessWidget {
|
class PostMediaPendingList extends StatelessWidget {
|
||||||
final List<XFile> data;
|
final PostWriteController controller;
|
||||||
final Function(int idx)? onRemove;
|
const PostMediaPendingList({super.key, required this.controller});
|
||||||
const PostMediaPendingList({
|
|
||||||
super.key,
|
void _cropImage(BuildContext context, int idx) async {
|
||||||
required this.data,
|
final media = controller.attachments[idx];
|
||||||
this.onRemove,
|
final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS))
|
||||||
});
|
? await showCupertinoImageCropper(
|
||||||
|
// ignore: use_build_context_synchronously
|
||||||
|
context,
|
||||||
|
// ignore: use_build_context_synchronously
|
||||||
|
imageProvider: media.getImageProvider(context)!,
|
||||||
|
)
|
||||||
|
: await showMaterialImageCropper(
|
||||||
|
// ignore: use_build_context_synchronously
|
||||||
|
context,
|
||||||
|
// ignore: use_build_context_synchronously
|
||||||
|
imageProvider: media.getImageProvider(context)!,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result == null) return;
|
||||||
|
if (!context.mounted) return;
|
||||||
|
|
||||||
|
controller.setIsBusy(true);
|
||||||
|
|
||||||
|
final rawBytes =
|
||||||
|
(await result.uiImage.toByteData(format: ImageByteFormat.png))!
|
||||||
|
.buffer
|
||||||
|
.asUint8List();
|
||||||
|
controller.setAttachmentAt(
|
||||||
|
idx,
|
||||||
|
PostWriteMedia.fromBytes(rawBytes, media.name, media.type),
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.setIsBusy(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _deleteAttachment(BuildContext context, int idx) async {
|
||||||
|
final media = controller.attachments[idx];
|
||||||
|
if (media.attachment == null) return;
|
||||||
|
|
||||||
|
controller.setIsBusy(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
await sn.client.delete('/cgi/uc/attachments/${media.attachment!.id}');
|
||||||
|
controller.removeAttachmentAt(idx);
|
||||||
|
} catch (err) {
|
||||||
|
if (!context.mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
} finally {
|
||||||
|
controller.setIsBusy(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||||
constraints: const BoxConstraints(maxHeight: 120),
|
|
||||||
child: ListView.separated(
|
return ListenableBuilder(
|
||||||
scrollDirection: Axis.horizontal,
|
listenable: controller,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
builder: (context, _) {
|
||||||
separatorBuilder: (context, index) => const Gap(8),
|
return Container(
|
||||||
itemCount: data.length,
|
constraints: const BoxConstraints(maxHeight: 120),
|
||||||
itemBuilder: (context, idx) {
|
child: ListView.separated(
|
||||||
final file = data[idx];
|
scrollDirection: Axis.horizontal,
|
||||||
return ContextMenuRegion(
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
contextMenu: ContextMenu(
|
separatorBuilder: (context, index) => const Gap(8),
|
||||||
entries: [
|
itemCount: controller.attachments.length,
|
||||||
if (onRemove != null)
|
itemBuilder: (context, idx) {
|
||||||
MenuItem(
|
final media = controller.attachments[idx];
|
||||||
label: 'delete'.tr(),
|
return ContextMenuRegion(
|
||||||
icon: Symbols.delete,
|
contextMenu: ContextMenu(
|
||||||
onSelected: () {
|
entries: [
|
||||||
onRemove!(idx);
|
if (media.type == PostWriteMediaType.image &&
|
||||||
},
|
media.attachment != null)
|
||||||
|
MenuItem(
|
||||||
|
label: 'preview'.tr(),
|
||||||
|
icon: Symbols.preview,
|
||||||
|
onSelected: () {
|
||||||
|
context.pushTransparentRoute(
|
||||||
|
AttachmentDetailPopup(data: media.attachment!),
|
||||||
|
rootNavigator: true,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (media.type == PostWriteMediaType.image &&
|
||||||
|
media.attachment == null)
|
||||||
|
MenuItem(
|
||||||
|
label: 'crop'.tr(),
|
||||||
|
icon: Symbols.crop,
|
||||||
|
onSelected: () => _cropImage(context, idx),
|
||||||
|
),
|
||||||
|
if (media.attachment != null)
|
||||||
|
MenuItem(
|
||||||
|
label: 'delete'.tr(),
|
||||||
|
icon: Symbols.delete,
|
||||||
|
onSelected: controller.isBusy
|
||||||
|
? null
|
||||||
|
: () => _deleteAttachment(context, idx),
|
||||||
|
),
|
||||||
|
if (media.attachment == null)
|
||||||
|
MenuItem(
|
||||||
|
label: 'delete'.tr(),
|
||||||
|
icon: Symbols.delete,
|
||||||
|
onSelected: () {
|
||||||
|
controller.removeAttachmentAt(idx);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else
|
||||||
|
MenuItem(
|
||||||
|
label: 'unlink'.tr(),
|
||||||
|
icon: Symbols.link_off,
|
||||||
|
onSelected: () {
|
||||||
|
controller.removeAttachmentAt(idx);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: 1,
|
||||||
|
child: switch (media.type) {
|
||||||
|
PostWriteMediaType.image =>
|
||||||
|
LayoutBuilder(builder: (context, constraints) {
|
||||||
|
return Image(
|
||||||
|
image: media.getImageProvider(
|
||||||
|
context,
|
||||||
|
width: (constraints.maxWidth * devicePixelRatio)
|
||||||
|
.round(),
|
||||||
|
height:
|
||||||
|
(constraints.maxHeight * devicePixelRatio)
|
||||||
|
.round(),
|
||||||
|
)!,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
_ => Container(
|
||||||
|
color: Theme.of(context).colorScheme.surface,
|
||||||
|
child: const Icon(Symbols.docs).center(),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(
|
|
||||||
color: Theme.of(context).dividerColor,
|
|
||||||
width: 1,
|
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(8),
|
);
|
||||||
),
|
},
|
||||||
child: ClipRRect(
|
),
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
);
|
||||||
child: AspectRatio(
|
},
|
||||||
aspectRatio: 1,
|
|
||||||
child: kIsWeb
|
|
||||||
? Image.network(file.path, fit: BoxFit.cover)
|
|
||||||
: Image.file(File(file.path), fit: BoxFit.cover),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,87 +1,130 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:surface/controllers/post_write_controller.dart';
|
||||||
|
|
||||||
class PostMetaResult {
|
class PostMetaEditor extends StatelessWidget {
|
||||||
final String title;
|
final PostWriteController controller;
|
||||||
final String description;
|
const PostMetaEditor({super.key, required this.controller});
|
||||||
|
|
||||||
PostMetaResult({required this.title, required this.description});
|
Future<DateTime?> _selectDate(
|
||||||
}
|
BuildContext context, {
|
||||||
|
DateTime? initialDateTime,
|
||||||
class PostMetaEditor extends StatefulWidget {
|
}) async {
|
||||||
final String? initialTitle;
|
DateTime? picked;
|
||||||
final String? initialDescription;
|
await showCupertinoModalPopup(
|
||||||
const PostMetaEditor({super.key, this.initialTitle, this.initialDescription});
|
context: context,
|
||||||
|
builder: (BuildContext context) => Container(
|
||||||
@override
|
height: 216,
|
||||||
State<PostMetaEditor> createState() => _PostMetaEditorState();
|
padding: const EdgeInsets.only(top: 6.0),
|
||||||
}
|
margin: EdgeInsets.only(
|
||||||
|
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||||
class _PostMetaEditorState extends State<PostMetaEditor> {
|
),
|
||||||
final TextEditingController _titleController = TextEditingController();
|
color: Theme.of(context).colorScheme.surface,
|
||||||
final TextEditingController _descriptionController = TextEditingController();
|
child: SafeArea(
|
||||||
|
top: false,
|
||||||
void _applyChanges() {
|
child: CupertinoDatePicker(
|
||||||
Navigator.pop(
|
initialDateTime: initialDateTime,
|
||||||
context,
|
mode: CupertinoDatePickerMode.dateAndTime,
|
||||||
PostMetaResult(
|
use24hFormat: true,
|
||||||
title: _titleController.text,
|
onDateTimeChanged: (DateTime newDate) {
|
||||||
description: _descriptionController.text,
|
picked = newDate;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
return picked;
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_titleController.text = widget.initialTitle ?? '';
|
|
||||||
_descriptionController.text = widget.initialDescription ?? '';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_titleController.dispose();
|
|
||||||
_descriptionController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
final dateFormatter = DateFormat('y/M/d HH:mm:ss');
|
||||||
children: [
|
return ListenableBuilder(
|
||||||
TextField(
|
listenable: controller,
|
||||||
controller: _titleController,
|
builder: (context, _) {
|
||||||
decoration: InputDecoration(
|
return Column(
|
||||||
labelText: 'fieldPostTitle'.tr(),
|
|
||||||
border: UnderlineInputBorder(),
|
|
||||||
),
|
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
|
||||||
),
|
|
||||||
const Gap(4),
|
|
||||||
TextField(
|
|
||||||
controller: _descriptionController,
|
|
||||||
maxLines: null,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: 'fieldPostDescription'.tr(),
|
|
||||||
border: UnderlineInputBorder(),
|
|
||||||
),
|
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
|
||||||
),
|
|
||||||
const Gap(12),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
children: [
|
children: [
|
||||||
ElevatedButton.icon(
|
TextField(
|
||||||
onPressed: _applyChanges,
|
controller: controller.titleController,
|
||||||
icon: const Icon(Symbols.save),
|
decoration: InputDecoration(
|
||||||
label: Text('apply').tr(),
|
labelText: 'fieldPostTitle'.tr(),
|
||||||
|
border: UnderlineInputBorder(),
|
||||||
|
),
|
||||||
|
onTapOutside: (_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
).padding(horizontal: 24),
|
||||||
|
if (controller.mode == 'article') const Gap(4),
|
||||||
|
if (controller.mode == 'article')
|
||||||
|
TextField(
|
||||||
|
controller: controller.descriptionController,
|
||||||
|
maxLines: null,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'fieldPostDescription'.tr(),
|
||||||
|
border: UnderlineInputBorder(),
|
||||||
|
),
|
||||||
|
onTapOutside: (_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
).padding(horizontal: 24),
|
||||||
|
const Gap(12),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Symbols.event_available),
|
||||||
|
title: Text('postPublishedAt').tr(),
|
||||||
|
subtitle: Text(
|
||||||
|
controller.publishedAt != null
|
||||||
|
? dateFormatter.format(controller.publishedAt!)
|
||||||
|
: 'unset'.tr(),
|
||||||
|
),
|
||||||
|
trailing: controller.publishedAt != null
|
||||||
|
? IconButton(
|
||||||
|
icon: const Icon(Symbols.cancel),
|
||||||
|
onPressed: () {
|
||||||
|
controller.setPublishedAt(null);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
contentPadding: const EdgeInsets.only(left: 24, right: 18),
|
||||||
|
onTap: () {
|
||||||
|
_selectDate(
|
||||||
|
context,
|
||||||
|
initialDateTime: controller.publishedAt,
|
||||||
|
).then((value) {
|
||||||
|
controller.setPublishedAt(value);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Symbols.event_busy),
|
||||||
|
title: Text('postPublishedUntil').tr(),
|
||||||
|
subtitle: Text(
|
||||||
|
controller.publishedUntil != null
|
||||||
|
? dateFormatter.format(controller.publishedUntil!)
|
||||||
|
: 'unset'.tr(),
|
||||||
|
),
|
||||||
|
trailing: controller.publishedUntil != null
|
||||||
|
? IconButton(
|
||||||
|
icon: const Icon(Symbols.cancel),
|
||||||
|
onPressed: () {
|
||||||
|
controller.setPublishedUntil(null);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
contentPadding: const EdgeInsets.only(left: 24, right: 18),
|
||||||
|
onTap: () {
|
||||||
|
_selectDate(
|
||||||
|
context,
|
||||||
|
initialDateTime: controller.publishedUntil,
|
||||||
|
).then((value) {
|
||||||
|
controller.setPublishedUntil(value);
|
||||||
|
});
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
).padding(vertical: 8);
|
||||||
],
|
},
|
||||||
).padding(horizontal: 24, vertical: 8);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
236
lib/widgets/post/post_mini_editor.dart
Normal file
236
lib/widgets/post/post_mini_editor.dart
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:surface/controllers/post_write_controller.dart';
|
||||||
|
import 'package:surface/providers/sn_network.dart';
|
||||||
|
import 'package:surface/types/post.dart';
|
||||||
|
import 'package:surface/widgets/account/account_image.dart';
|
||||||
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
import 'package:surface/widgets/loading_indicator.dart';
|
||||||
|
|
||||||
|
class PostMiniEditor extends StatefulWidget {
|
||||||
|
final int? postReplyId;
|
||||||
|
final Function? onPost;
|
||||||
|
const PostMiniEditor({super.key, this.postReplyId, this.onPost});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PostMiniEditor> createState() => _PostMiniEditorState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PostMiniEditorState extends State<PostMiniEditor> {
|
||||||
|
final PostWriteController _writeController = PostWriteController();
|
||||||
|
|
||||||
|
bool _isFetching = false;
|
||||||
|
bool get _isLoading => _isFetching || _writeController.isLoading;
|
||||||
|
|
||||||
|
List<SnPublisher>? _publishers;
|
||||||
|
|
||||||
|
Future<void> _fetchPublishers() async {
|
||||||
|
setState(() => _isFetching = true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
final resp = await sn.client.get('/cgi/co/publishers');
|
||||||
|
_publishers = List<SnPublisher>.from(
|
||||||
|
resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [],
|
||||||
|
);
|
||||||
|
_writeController.setPublisher(_publishers?.firstOrNull);
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
} finally {
|
||||||
|
setState(() => _isFetching = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_fetchPublishers();
|
||||||
|
_writeController.fetchRelatedPost(
|
||||||
|
context,
|
||||||
|
replying: widget.postReplyId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_writeController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListenableBuilder(
|
||||||
|
listenable: _writeController,
|
||||||
|
builder: (context, _) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
DropdownButtonHideUnderline(
|
||||||
|
child: DropdownButton2<SnPublisher>(
|
||||||
|
isExpanded: true,
|
||||||
|
hint: Text(
|
||||||
|
'fieldPostPublisher',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Theme.of(context).hintColor,
|
||||||
|
),
|
||||||
|
).tr(),
|
||||||
|
items: <DropdownMenuItem<SnPublisher>>[
|
||||||
|
...(_publishers?.map(
|
||||||
|
(item) => DropdownMenuItem<SnPublisher>(
|
||||||
|
enabled: _writeController.editingPost == null,
|
||||||
|
value: item,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
AccountImage(content: item.avatar, radius: 16),
|
||||||
|
const Gap(8),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(item.nick).textStyle(
|
||||||
|
Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodyMedium!),
|
||||||
|
Text('@${item.name}')
|
||||||
|
.textStyle(Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodySmall!)
|
||||||
|
.fontSize(12),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
) ??
|
||||||
|
[]),
|
||||||
|
DropdownMenuItem<SnPublisher>(
|
||||||
|
value: null,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
CircleAvatar(
|
||||||
|
radius: 16,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
foregroundColor:
|
||||||
|
Theme.of(context).colorScheme.onSurface,
|
||||||
|
child: const Icon(Symbols.add),
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('publishersNew').tr().textStyle(
|
||||||
|
Theme.of(context).textTheme.bodyMedium!),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
value: _writeController.publisher,
|
||||||
|
onChanged: (SnPublisher? value) {
|
||||||
|
if (value == null) {
|
||||||
|
GoRouter.of(context)
|
||||||
|
.pushNamed('accountPublisherNew')
|
||||||
|
.then((value) {
|
||||||
|
if (value == true) {
|
||||||
|
_publishers = null;
|
||||||
|
_fetchPublishers();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
_writeController.setPublisher(value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
buttonStyleData: const ButtonStyleData(
|
||||||
|
padding: EdgeInsets.only(right: 16),
|
||||||
|
height: 48,
|
||||||
|
),
|
||||||
|
menuItemStyleData: const MenuItemStyleData(
|
||||||
|
height: 48,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
const Gap(8),
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
controller: _writeController.contentController,
|
||||||
|
maxLines: null,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'fieldPostContent'.tr(),
|
||||||
|
hintStyle: TextStyle(fontSize: 14),
|
||||||
|
isCollapsed: true,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
),
|
||||||
|
border: InputBorder.none,
|
||||||
|
),
|
||||||
|
onTapOutside: (_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
LoadingIndicator(isActive: _isLoading),
|
||||||
|
if (_writeController.isBusy && _writeController.progress != null)
|
||||||
|
TweenAnimationBuilder<double>(
|
||||||
|
tween: Tween(begin: 0, end: _writeController.progress),
|
||||||
|
duration: Duration(milliseconds: 300),
|
||||||
|
builder: (context, value, _) =>
|
||||||
|
LinearProgressIndicator(value: value, minHeight: 2),
|
||||||
|
)
|
||||||
|
else if (_writeController.isBusy)
|
||||||
|
const LinearProgressIndicator(value: null, minHeight: 2),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
Symbols.launch,
|
||||||
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
GoRouter.of(context).pushNamed(
|
||||||
|
'postEditor',
|
||||||
|
pathParameters: {'mode': 'stories'},
|
||||||
|
queryParameters: {
|
||||||
|
if (widget.postReplyId != null)
|
||||||
|
'replying': widget.postReplyId.toString(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: (_writeController.isBusy ||
|
||||||
|
_writeController.publisher == null)
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
_writeController.post(context).then((_) {
|
||||||
|
if (!context.mounted) return;
|
||||||
|
if (widget.onPost != null) widget.onPost!();
|
||||||
|
context.showSnackbar('postPosted'.tr());
|
||||||
|
_writeController.reset();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: const Icon(Symbols.send),
|
||||||
|
label: Text('postPublish').tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(left: 12, right: 16, bottom: 4),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
128
lib/widgets/post/post_reaction.dart
Normal file
128
lib/widgets/post/post_reaction.dart
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:surface/providers/sn_network.dart';
|
||||||
|
import 'package:surface/types/post.dart';
|
||||||
|
import 'package:surface/types/reaction.dart';
|
||||||
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
|
||||||
|
class PostReactionPopup extends StatefulWidget {
|
||||||
|
final SnPost data;
|
||||||
|
final Function(Map<String, int> value, bool isPositive, int delta)? onChanged;
|
||||||
|
const PostReactionPopup({super.key, required this.data, this.onChanged});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PostReactionPopup> createState() => _PostReactionPopupState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PostReactionPopupState extends State<PostReactionPopup> {
|
||||||
|
bool _isSubmitting = false;
|
||||||
|
late Map<String, int> _reactions;
|
||||||
|
|
||||||
|
Future<void> _reactPost(String symbol, int attitude) async {
|
||||||
|
if (_isSubmitting) return;
|
||||||
|
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
setState(() => _isSubmitting = true);
|
||||||
|
final resp = await sn.client.post(
|
||||||
|
'/cgi/co/posts/${widget.data.id}/react',
|
||||||
|
data: {
|
||||||
|
'symbol': symbol,
|
||||||
|
'attitude': attitude,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (resp.statusCode == 201) {
|
||||||
|
_reactions[symbol] = (_reactions[symbol] ?? 0) + 1;
|
||||||
|
// ignore: use_build_context_synchronously
|
||||||
|
if (context.mounted) context.showSnackbar('postReactCompleted'.tr());
|
||||||
|
if (widget.onChanged != null) {
|
||||||
|
widget.onChanged!(
|
||||||
|
_reactions,
|
||||||
|
kTemplateReactions[symbol]!.attitude == 1,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (resp.statusCode == 204) {
|
||||||
|
_reactions[symbol] = (_reactions[symbol] ?? 0) - 1;
|
||||||
|
// ignore: use_build_context_synchronously
|
||||||
|
if (context.mounted) context.showSnackbar('postReactUncompleted'.tr());
|
||||||
|
if (widget.onChanged != null) {
|
||||||
|
widget.onChanged!(
|
||||||
|
_reactions,
|
||||||
|
kTemplateReactions[symbol]!.attitude == 1,
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// ignore: use_build_context_synchronously
|
||||||
|
if (context.mounted) context.showErrorDialog(err);
|
||||||
|
} finally {
|
||||||
|
setState(() => _isSubmitting = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_reactions = Map.from(widget.data.metric.reactionList);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.mood, size: 24),
|
||||||
|
const Gap(16),
|
||||||
|
Text('postReactions')
|
||||||
|
.tr()
|
||||||
|
.textStyle(Theme.of(context).textTheme.titleLarge!),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||||
|
Expanded(
|
||||||
|
child: GridView.count(
|
||||||
|
crossAxisSpacing: 4,
|
||||||
|
mainAxisSpacing: 4,
|
||||||
|
crossAxisCount: 4,
|
||||||
|
children: kTemplateReactions.entries.map((e) {
|
||||||
|
return InkWell(
|
||||||
|
onTap: () {
|
||||||
|
if (widget.onChanged == null) return;
|
||||||
|
_reactPost(e.key, e.value.attitude).then((_) {
|
||||||
|
if (context.mounted) Navigator.pop(context);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(e.value.icon).fontSize(40),
|
||||||
|
Text(
|
||||||
|
e.key,
|
||||||
|
style: const TextStyle(fontFamily: 'monospace'),
|
||||||
|
),
|
||||||
|
const Gap(6),
|
||||||
|
Text(
|
||||||
|
'x${_reactions[e.key]?.toString() ?? '0'}',
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,67 +30,21 @@ class UniversalImage extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||||
|
final double? resizeHeight =
|
||||||
|
cacheHeight != null ? (cacheHeight! * devicePixelRatio) : null;
|
||||||
|
final double? resizeWidth =
|
||||||
|
cacheWidth != null ? (cacheWidth! * devicePixelRatio) : null;
|
||||||
|
|
||||||
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS || Platform.isMacOS)) {
|
return Image(
|
||||||
return CachedNetworkImage(
|
image: ResizeImage(
|
||||||
imageUrl: url,
|
UniversalImage.provider(url),
|
||||||
width: width,
|
width: resizeWidth?.round(),
|
||||||
height: height,
|
height: resizeHeight?.round(),
|
||||||
fit: fit,
|
policy: ResizeImagePolicy.fit,
|
||||||
memCacheHeight: cacheHeight != null
|
),
|
||||||
? (cacheHeight! * devicePixelRatio).round()
|
|
||||||
: null,
|
|
||||||
memCacheWidth: cacheWidth != null
|
|
||||||
? (cacheWidth! * devicePixelRatio).round()
|
|
||||||
: null,
|
|
||||||
progressIndicatorBuilder: noProgressIndicator
|
|
||||||
? null
|
|
||||||
: (context, url, downloadProgress) => Center(
|
|
||||||
child: TweenAnimationBuilder(
|
|
||||||
tween: Tween(
|
|
||||||
begin: 0,
|
|
||||||
end: downloadProgress.progress ?? 0,
|
|
||||||
),
|
|
||||||
duration: const Duration(milliseconds: 300),
|
|
||||||
builder: (context, value, _) => CircularProgressIndicator(
|
|
||||||
value: downloadProgress.progress != null
|
|
||||||
? value.toDouble()
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
errorWidget: noErrorWidget
|
|
||||||
? null
|
|
||||||
: (context, url, error) {
|
|
||||||
return Container(
|
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
constraints: const BoxConstraints(maxWidth: 280),
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
AnimateWidgetExtensions(Icon(Symbols.close, size: 24))
|
|
||||||
.animate(onPlay: (e) => e.repeat(reverse: true))
|
|
||||||
.fade(duration: 500.ms),
|
|
||||||
Text(
|
|
||||||
error.toString(),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).center(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return Image.network(
|
|
||||||
url,
|
|
||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
fit: fit,
|
fit: fit,
|
||||||
cacheHeight: cacheHeight != null
|
|
||||||
? (cacheHeight! * devicePixelRatio).round()
|
|
||||||
: null,
|
|
||||||
cacheWidth:
|
|
||||||
cacheWidth != null ? (cacheWidth! * devicePixelRatio).round() : null,
|
|
||||||
loadingBuilder: noProgressIndicator
|
loadingBuilder: noProgressIndicator
|
||||||
? null
|
? null
|
||||||
: (BuildContext context, Widget child,
|
: (BuildContext context, Widget child,
|
||||||
@@ -146,3 +100,37 @@ class UniversalImage extends StatelessWidget {
|
|||||||
return NetworkImage(url);
|
return NetworkImage(url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class AutoResizeUniversalImage extends StatelessWidget {
|
||||||
|
final String url;
|
||||||
|
final double? width, height;
|
||||||
|
final BoxFit? fit;
|
||||||
|
final bool noProgressIndicator;
|
||||||
|
final bool noErrorWidget;
|
||||||
|
|
||||||
|
const AutoResizeUniversalImage(
|
||||||
|
this.url, {
|
||||||
|
super.key,
|
||||||
|
this.width,
|
||||||
|
this.height,
|
||||||
|
this.fit,
|
||||||
|
this.noProgressIndicator = false,
|
||||||
|
this.noErrorWidget = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return LayoutBuilder(builder: (context, constraints) {
|
||||||
|
return UniversalImage(
|
||||||
|
url,
|
||||||
|
fit: fit,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
noProgressIndicator: noProgressIndicator,
|
||||||
|
noErrorWidget: noErrorWidget,
|
||||||
|
cacheHeight: constraints.maxHeight,
|
||||||
|
cacheWidth: constraints.maxWidth,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import Foundation
|
|||||||
|
|
||||||
import connectivity_plus
|
import connectivity_plus
|
||||||
import file_selector_macos
|
import file_selector_macos
|
||||||
import flutter_image_compress_macos
|
|
||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
import sqflite_darwin
|
import sqflite_darwin
|
||||||
@@ -16,7 +15,6 @@ import url_launcher_macos
|
|||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
|
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
|
||||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||||
FlutterImageCompressMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterImageCompressMacosPlugin"))
|
|
||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||||
|
|||||||
120
pubspec.lock
120
pubspec.lock
@@ -5,23 +5,23 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: _fe_analyzer_shared
|
name: _fe_analyzer_shared
|
||||||
sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab"
|
sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "76.0.0"
|
version: "72.0.0"
|
||||||
_macros:
|
_macros:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: dart
|
description: dart
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.3.3"
|
version: "0.3.2"
|
||||||
analyzer:
|
analyzer:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: analyzer
|
name: analyzer
|
||||||
sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e"
|
sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.11.0"
|
version: "6.7.0"
|
||||||
animations:
|
animations:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -199,13 +199,13 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "4.10.1"
|
version: "4.10.1"
|
||||||
collection:
|
collection:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: collection
|
name: collection
|
||||||
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
|
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.19.0"
|
version: "1.18.0"
|
||||||
connectivity_plus:
|
connectivity_plus:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -322,10 +322,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: dio_smart_retry
|
name: dio_smart_retry
|
||||||
sha256: c8e20da5f49289fa7dce5c9c6b5b120928e3661aefa0fa2d206ea6d93f580928
|
sha256: "3d71450c19b4d91ef4c7d726a55a284bfc11eb3634f1f25006cdfab3f8595653"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.0.1"
|
version: "6.0.0"
|
||||||
dio_web_adapter:
|
dio_web_adapter:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -410,10 +410,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: file_picker
|
name: file_picker
|
||||||
sha256: aac85f20436608e01a6ffd1fdd4e746a7f33c93a2c83752e626bdfaea139b877
|
sha256: "16dc141db5a2ccc6520ebb6a2eb5945b1b09e95085c021d9f914f8ded7f1465c"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.1.3"
|
version: "8.1.4"
|
||||||
file_selector_linux:
|
file_selector_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -491,54 +491,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.0"
|
version: "2.3.0"
|
||||||
flutter_image_compress:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: flutter_image_compress
|
|
||||||
sha256: "45a3071868092a61b11044c70422b04d39d4d9f2ef536f3c5b11fb65a1e7dd90"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.3.0"
|
|
||||||
flutter_image_compress_common:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: flutter_image_compress_common
|
|
||||||
sha256: "7f79bc6c8a363063620b4e372fa86bc691e1cb28e58048cd38e030692fbd99ee"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.5"
|
|
||||||
flutter_image_compress_macos:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: flutter_image_compress_macos
|
|
||||||
sha256: "26df6385512e92b3789dc76b613b54b55c457a7f1532e59078b04bf189782d47"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.2"
|
|
||||||
flutter_image_compress_ohos:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: flutter_image_compress_ohos
|
|
||||||
sha256: e76b92bbc830ee08f5b05962fc78a532011fcd2041f620b5400a593e96da3f51
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.0.3"
|
|
||||||
flutter_image_compress_platform_interface:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: flutter_image_compress_platform_interface
|
|
||||||
sha256: "579cb3947fd4309103afe6442a01ca01e1e6f93dc53bb4cbd090e8ce34a41889"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.5"
|
|
||||||
flutter_image_compress_web:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: flutter_image_compress_web
|
|
||||||
sha256: f02fe352b17f82b72f481de45add240db062a2585850bea1667e82cc4cd6c311
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.1.4+1"
|
|
||||||
flutter_lints:
|
flutter_lints:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@@ -694,10 +646,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: http_parser
|
name: http_parser
|
||||||
sha256: "76d306a1c3afb33fe82e2bbacad62a61f409b5634c915fceb0d799de1a913360"
|
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.1"
|
version: "4.0.2"
|
||||||
http_profile:
|
http_profile:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -838,18 +790,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker
|
name: leak_tracker
|
||||||
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
|
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.0.8"
|
version: "10.0.5"
|
||||||
leak_tracker_flutter_testing:
|
leak_tracker_flutter_testing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker_flutter_testing
|
name: leak_tracker_flutter_testing
|
||||||
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
|
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.9"
|
version: "3.0.5"
|
||||||
leak_tracker_testing:
|
leak_tracker_testing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -870,10 +822,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: lints
|
name: lints
|
||||||
sha256: "4a16b3f03741e1252fda5de3ce712666d010ba2122f8e912c94f9f7b90e1a4c3"
|
sha256: "3315600f3fb3b135be672bf4a178c55f274bebe368325ae18462c89ac1e3b413"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.1.0"
|
version: "5.0.0"
|
||||||
logging:
|
logging:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -886,10 +838,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: macros
|
name: macros
|
||||||
sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656"
|
sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.3-main.0"
|
version: "0.1.2-main.4"
|
||||||
markdown:
|
markdown:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -931,7 +883,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "1.15.0"
|
version: "1.15.0"
|
||||||
mime:
|
mime:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: mime
|
name: mime
|
||||||
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
|
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
|
||||||
@@ -987,7 +939,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.0"
|
version: "1.9.0"
|
||||||
path_provider:
|
path_provider:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: path_provider
|
name: path_provider
|
||||||
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||||
@@ -1182,10 +1134,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: shelf
|
name: shelf
|
||||||
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
|
sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.2"
|
version: "1.4.1"
|
||||||
shelf_web_socket:
|
shelf_web_socket:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1198,7 +1150,7 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.99"
|
||||||
source_gen:
|
source_gen:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1275,10 +1227,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: stack_trace
|
name: stack_trace
|
||||||
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
|
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.12.0"
|
version: "1.11.1"
|
||||||
stream_channel:
|
stream_channel:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1299,10 +1251,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: string_scanner
|
name: string_scanner
|
||||||
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
|
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.0"
|
version: "1.2.0"
|
||||||
styled_widget:
|
styled_widget:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -1339,10 +1291,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
|
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.3"
|
version: "0.7.2"
|
||||||
timing:
|
timing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1459,10 +1411,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vm_service
|
name: vm_service
|
||||||
sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b
|
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "14.3.0"
|
version: "14.2.5"
|
||||||
watcher:
|
watcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1528,5 +1480,5 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.2"
|
version: "3.1.2"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.6.0-0 <4.0.0"
|
dart: ">=3.5.4 <4.0.0"
|
||||||
flutter: ">=3.24.0"
|
flutter: ">=3.24.0"
|
||||||
|
|||||||
10
pubspec.yaml
10
pubspec.yaml
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 1.0.0+1
|
version: 2.0.0+2
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.5.4
|
sdk: ^3.5.4
|
||||||
@@ -44,7 +44,7 @@ dependencies:
|
|||||||
animations: ^2.0.11
|
animations: ^2.0.11
|
||||||
dio: ^5.7.0
|
dio: ^5.7.0
|
||||||
native_dio_adapter: ^1.3.0
|
native_dio_adapter: ^1.3.0
|
||||||
dio_smart_retry: ^7.0.1
|
dio_smart_retry: ^6.0.0
|
||||||
very_good_infinite_list: ^0.9.0
|
very_good_infinite_list: ^0.9.0
|
||||||
freezed_annotation: ^2.4.4
|
freezed_annotation: ^2.4.4
|
||||||
json_annotation: ^4.9.0
|
json_annotation: ^4.9.0
|
||||||
@@ -62,7 +62,6 @@ dependencies:
|
|||||||
image_picker: ^1.1.2
|
image_picker: ^1.1.2
|
||||||
cross_file: ^0.3.4+2
|
cross_file: ^0.3.4+2
|
||||||
file_picker: ^8.1.3
|
file_picker: ^8.1.3
|
||||||
flutter_image_compress: ^2.3.0
|
|
||||||
croppy: ^1.3.1
|
croppy: ^1.3.1
|
||||||
flutter_expandable_fab: ^2.3.0
|
flutter_expandable_fab: ^2.3.0
|
||||||
dropdown_button2: ^2.3.9
|
dropdown_button2: ^2.3.9
|
||||||
@@ -71,6 +70,9 @@ dependencies:
|
|||||||
uuid: ^4.5.1
|
uuid: ^4.5.1
|
||||||
photo_view: ^0.15.0
|
photo_view: ^0.15.0
|
||||||
shared_preferences: ^2.3.3
|
shared_preferences: ^2.3.3
|
||||||
|
path_provider: ^2.1.5
|
||||||
|
collection: ^1.18.0
|
||||||
|
mime: ^2.0.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@@ -164,4 +166,4 @@ flutter_native_splash:
|
|||||||
color_dark: "#000000"
|
color_dark: "#000000"
|
||||||
branding: "assets/icon/branding-light.png"
|
branding: "assets/icon/branding-light.png"
|
||||||
branding_dark: "assets/icon/branding-dark.png"
|
branding_dark: "assets/icon/branding-dark.png"
|
||||||
branding_bottom_padding: 24
|
branding_bottom_padding: 24
|
||||||
|
|||||||
9
roadsign.toml
Normal file
9
roadsign.toml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
id = "solian-next"
|
||||||
|
|
||||||
|
[[locations]]
|
||||||
|
id = "solian-next"
|
||||||
|
host = ["sn-next.solsynth.dev"]
|
||||||
|
path = ["/"]
|
||||||
|
[[locations.destinations]]
|
||||||
|
id = "solian-next-web"
|
||||||
|
uri = "files:///workdir/solian-next?fallback=index.html&index=index.html"
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
<!-- Favicon -->
|
<!-- Favicon -->
|
||||||
<link rel="icon" type="image/png" href="favicon.png">
|
<link rel="icon" type="image/png" href="favicon.png">
|
||||||
|
|
||||||
<title>surface</title>
|
<title>Solian</title>
|
||||||
<link rel="manifest" href="manifest.json">
|
<link rel="manifest" href="manifest.json">
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user