Compare commits
24 Commits
3ffe3cb50f
...
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 | |||
| c1e10916ee | |||
| 0a8c9fb208 |
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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
{
|
{
|
||||||
|
"nextVersionAlert": "Heavy Development Alert",
|
||||||
|
"nextVersionNotice": "You are using Solian 2.0 Preview, which is the first version of Solian 2.0. The current stable branch (sn.solsynth.dev) is 1.4. This version is still under heavy development, some features may not be stable, and not all features are supported. You can roll back to 1.4.X version via TestFlight, or continue to experience the new version (sn-next.solsynth.dev).",
|
||||||
"screen": "Screen",
|
"screen": "Screen",
|
||||||
"screenHome": "Home",
|
"screenHome": "Home",
|
||||||
"screenExplore": "Explore",
|
"screenExplore": "Explore",
|
||||||
@@ -12,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",
|
||||||
@@ -23,15 +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",
|
||||||
|
"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",
|
||||||
@@ -74,5 +88,48 @@
|
|||||||
"fieldPostTitle": "Title",
|
"fieldPostTitle": "Title",
|
||||||
"fieldPostDescription": "Description",
|
"fieldPostDescription": "Description",
|
||||||
"postPublish": "Publish",
|
"postPublish": "Publish",
|
||||||
"postEditingNotice": "You're about to editing a post that posted {}."
|
"postPosted": "Post has been posted.",
|
||||||
|
"postPublishedAt": "Published At",
|
||||||
|
"postPublishedUntil": "Published Until",
|
||||||
|
"postEditingNotice": "You're about to editing 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 {}.",
|
||||||
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
{
|
{
|
||||||
|
"nextVersionAlert": "高强度开发提示",
|
||||||
|
"nextVersionNotice": "您正在使用的是 Solian 2.0 的抢先体验版本,目前稳定分支(sn.solsynth.dev)版本为 1.4。该版本还在持续的开发中,部分功能可能不稳定,也并非所有功能都支持了。您可以通过 TestFlight 回滚到 1.4.X 或者继续体验新版本(sn-next.solsynth.dev)。",
|
||||||
"screen": "页面",
|
"screen": "页面",
|
||||||
"screenHome": "首页",
|
"screenHome": "首页",
|
||||||
"screenExplore": "探索",
|
"screenExplore": "探索",
|
||||||
@@ -12,6 +14,9 @@
|
|||||||
"screenAccountPublisherNew": "新建发布者",
|
"screenAccountPublisherNew": "新建发布者",
|
||||||
"screenAccountPublisherEdit": "编辑发布者",
|
"screenAccountPublisherEdit": "编辑发布者",
|
||||||
"screenAccountProfileEdit": "编辑资料",
|
"screenAccountProfileEdit": "编辑资料",
|
||||||
|
"screenSettings": "设置",
|
||||||
|
"screenAlbum": "相册",
|
||||||
|
"screenChat": "聊天",
|
||||||
"dialogOkay": "好的",
|
"dialogOkay": "好的",
|
||||||
"dialogCancel": "取消",
|
"dialogCancel": "取消",
|
||||||
"dialogConfirm": "确认",
|
"dialogConfirm": "确认",
|
||||||
@@ -31,7 +36,16 @@
|
|||||||
"create": "创建",
|
"create": "创建",
|
||||||
"preview": "预览",
|
"preview": "预览",
|
||||||
"delete": "删除",
|
"delete": "删除",
|
||||||
|
"unlink": "解除链接",
|
||||||
|
"crop": "裁剪",
|
||||||
|
"compress": "压缩",
|
||||||
"report": "检举",
|
"report": "检举",
|
||||||
|
"repost": "转帖",
|
||||||
|
"reply": "回贴",
|
||||||
|
"unset": "未设置",
|
||||||
|
"untitled": "无题",
|
||||||
|
"postDetail": "帖子详情",
|
||||||
|
"postNoun": "帖子",
|
||||||
"fieldUsername": "用户名",
|
"fieldUsername": "用户名",
|
||||||
"fieldNickname": "显示名",
|
"fieldNickname": "显示名",
|
||||||
"fieldEmail": "电子邮箱地址",
|
"fieldEmail": "电子邮箱地址",
|
||||||
@@ -74,5 +88,48 @@
|
|||||||
"fieldPostTitle": "标题",
|
"fieldPostTitle": "标题",
|
||||||
"fieldPostDescription": "描述",
|
"fieldPostDescription": "描述",
|
||||||
"postPublish": "发布",
|
"postPublish": "发布",
|
||||||
"postEditingNotice": "你正在修改由 {} 发布的帖子。"
|
"postPublishedAt": "发布于",
|
||||||
|
"postPublishedUntil": "取消发布于",
|
||||||
|
"postEditingNotice": "你正在修改由 {} 发布的帖子。",
|
||||||
|
"postReplyingNotice": "你正在回复由 {} 发布的帖子。",
|
||||||
|
"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),
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class HomeScreen extends StatefulWidget {
|
class HomeScreen extends StatefulWidget {
|
||||||
const HomeScreen({super.key});
|
const HomeScreen({super.key});
|
||||||
@@ -16,6 +18,23 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text("screenHome").tr(),
|
title: Text("screenHome").tr(),
|
||||||
),
|
),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
MaterialBanner(
|
||||||
|
leading: const Icon(Symbols.construction),
|
||||||
|
content: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('nextVersionAlert').tr().bold(),
|
||||||
|
Text('nextVersionNotice').tr(),
|
||||||
|
],
|
||||||
|
).padding(vertical: 16),
|
||||||
|
actions: [
|
||||||
|
const SizedBox(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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,201 +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(),
|
|
||||||
},
|
|
||||||
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();
|
||||||
@@ -241,262 +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(
|
|
||||||
top: _editingOg == null ? 8 : 0,
|
|
||||||
bottom: 8,
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
// 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),
|
|
||||||
const Gap(8)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
// 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(),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
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: _isBusy),
|
|
||||||
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,
|
||||||
@@ -149,12 +144,10 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost>
|
|||||||
Object? pinnedAt = freezed,
|
Object? pinnedAt = freezed,
|
||||||
Object? lockedAt = freezed,
|
Object? lockedAt = freezed,
|
||||||
Object? isDraft = null,
|
Object? isDraft = null,
|
||||||
Object? publishedAt = null,
|
Object? publishedAt = freezed,
|
||||||
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
|
||||||
@@ -257,14 +246,14 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost>
|
|||||||
? _value.isDraft
|
? _value.isDraft
|
||||||
: isDraft // ignore: cast_nullable_to_non_nullable
|
: isDraft // ignore: cast_nullable_to_non_nullable
|
||||||
as bool,
|
as bool,
|
||||||
publishedAt: null == publishedAt
|
publishedAt: freezed == publishedAt
|
||||||
? _value.publishedAt
|
? _value.publishedAt
|
||||||
: publishedAt // ignore: cast_nullable_to_non_nullable
|
: publishedAt // ignore: cast_nullable_to_non_nullable
|
||||||
as DateTime,
|
as DateTime?,
|
||||||
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,
|
||||||
@@ -423,12 +432,10 @@ class __$$SnPostImplCopyWithImpl<$Res>
|
|||||||
Object? pinnedAt = freezed,
|
Object? pinnedAt = freezed,
|
||||||
Object? lockedAt = freezed,
|
Object? lockedAt = freezed,
|
||||||
Object? isDraft = null,
|
Object? isDraft = null,
|
||||||
Object? publishedAt = null,
|
Object? publishedAt = freezed,
|
||||||
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
|
||||||
@@ -531,14 +534,14 @@ class __$$SnPostImplCopyWithImpl<$Res>
|
|||||||
? _value.isDraft
|
? _value.isDraft
|
||||||
: isDraft // ignore: cast_nullable_to_non_nullable
|
: isDraft // ignore: cast_nullable_to_non_nullable
|
||||||
as bool,
|
as bool,
|
||||||
publishedAt: null == publishedAt
|
publishedAt: freezed == publishedAt
|
||||||
? _value.publishedAt
|
? _value.publishedAt
|
||||||
: publishedAt // ignore: cast_nullable_to_non_nullable
|
: publishedAt // ignore: cast_nullable_to_non_nullable
|
||||||
as DateTime,
|
as DateTime?,
|
||||||
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
|
||||||
@@ -688,18 +707,14 @@ class _$SnPostImpl extends _SnPost {
|
|||||||
@override
|
@override
|
||||||
final bool isDraft;
|
final bool isDraft;
|
||||||
@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
|
||||||
@@ -917,18 +923,14 @@ abstract class _SnPost extends SnPost {
|
|||||||
@override
|
@override
|
||||||
bool get isDraft;
|
bool get isDraft;
|
||||||
@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
|
||||||
@@ -39,12 +48,14 @@ _$SnPostImpl _$$SnPostImplFromJson(Map<String, dynamic> json) => _$SnPostImpl(
|
|||||||
? null
|
? null
|
||||||
: DateTime.parse(json['locked_at'] as String),
|
: DateTime.parse(json['locked_at'] as String),
|
||||||
isDraft: json['is_draft'] as bool,
|
isDraft: json['is_draft'] as bool,
|
||||||
publishedAt: DateTime.parse(json['published_at'] as String),
|
publishedAt: json['published_at'] == null
|
||||||
publishedUntil: json['published_until'],
|
? null
|
||||||
|
: DateTime.parse(json['published_at'] as String),
|
||||||
|
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>),
|
||||||
@@ -67,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,
|
||||||
@@ -80,12 +90,10 @@ Map<String, dynamic> _$$SnPostImplToJson(_$SnPostImpl instance) =>
|
|||||||
'pinned_at': instance.pinnedAt?.toIso8601String(),
|
'pinned_at': instance.pinnedAt?.toIso8601String(),
|
||||||
'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(),
|
||||||
@@ -127,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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,27 +2,155 @@ 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:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.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/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/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: () {},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -30,75 +158,141 @@ 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) {
|
||||||
|
final ua = context.read<UserProvider>();
|
||||||
|
final isAuthor = ua.isAuthorized && data.publisher.accountId == ua.user!.id;
|
||||||
|
|
||||||
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),
|
||||||
const Gap(4),
|
const Gap(4),
|
||||||
Text(RelativeTime(context).format(data.publishedAt))
|
Text(RelativeTime(context).format(
|
||||||
.fontSize(13),
|
data.publishedAt ?? data.createdAt,
|
||||||
|
)).fontSize(13),
|
||||||
],
|
],
|
||||||
).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>[
|
||||||
|
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(
|
||||||
|
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(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
|
|
||||||
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()},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
PopupMenuItem(
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Symbols.delete),
|
|
||||||
const Gap(16),
|
|
||||||
Text('delete').tr(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const PopupMenuDivider(),
|
|
||||||
PopupMenuItem(
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Symbols.flag),
|
|
||||||
const Gap(16),
|
|
||||||
Text('report').tr(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
).padding(horizontal: 12, vertical: 8);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,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