Compare commits
87 Commits
44f2c5fe0e
...
2.2.2+57
| Author | SHA1 | Date | |
|---|---|---|---|
| 0dcfcaad56 | |||
| 687e720956 | |||
| 180876949e | |||
| 9718965809 | |||
| 5377161fb0 | |||
| 963e538ae5 | |||
| a355e3bf90 | |||
| cb4a2598c8 | |||
| 950612dc07 | |||
| cbd1eaf1af | |||
| ac41cbd99f | |||
| 9f9c90abc4 | |||
| 87029e3538 | |||
| 127d9adc09 | |||
| c82dc7ad85 | |||
| 36bcff7a7c | |||
| 38201b547a | |||
| ed0334fcda | |||
| fbb486b90b | |||
| 9b34f385d5 | |||
| bb7b731602 | |||
| 19076f8136 | |||
| dc77a936ce | |||
| 7f58710c6f | |||
| 068ddcdcdc | |||
| f4e9252ca0 | |||
| 3b1e918117 | |||
| ed7981fdaf | |||
| 9698ca53e4 | |||
| ddc1dc7daf | |||
| 1625a957f8 | |||
| 2dc50d627e | |||
| 2ffde9a3dd | |||
| 5967a91ae1 | |||
| 32c1effcb5 | |||
| 9d0e19c56f | |||
| acf4e634fe | |||
| 25942c2338 | |||
| a4f81f6ba1 | |||
| c1b9090e51 | |||
| f494f70003 | |||
| fb2a55a909 | |||
| 4edfa7fd50 | |||
| d699cac9b1 | |||
| c0428e12c1 | |||
| 55f434ff05 | |||
| f2b3bdda2d | |||
| 1f6bf33b0e | |||
| e2027b1a32 | |||
| 2b3a58b55e | |||
| 6ac536412a | |||
| 52f8ffe4e4 | |||
| aca81431aa | |||
| 1fadd850b7 | |||
| ed2a9a21b6 | |||
| 57279eb3e4 | |||
| c403a2914a | |||
| bcb176344c | |||
| ecf362cffc | |||
| f4ab7671d8 | |||
| a2a3018917 | |||
| 0bdb664000 | |||
| 9c3b61ce57 | |||
| d06df3d278 | |||
| 547ba19e61 | |||
| cb05ff2e9e | |||
| f614da7918 | |||
| a3c8dafff9 | |||
| fa978a7cd1 | |||
| aaa0a562b4 | |||
| 590a4ce2a6 | |||
| f26edce071 | |||
| 603799ea32 | |||
| a32baf7798 | |||
| 498c9af663 | |||
| 202dbff6d3 | |||
| 96fd64d85d | |||
| e236b7f98b | |||
| 5c7929e618 | |||
| 7ba5260246 | |||
| a6d4947a23 | |||
| 7fbd4e9647 | |||
| 95d926b29f | |||
| f6cf6d0440 | |||
| e503c3f02f | |||
| d4fbdd397e | |||
| 03943a7138 |
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"sync": {
|
"sync": {
|
||||||
"region": "solian-next",
|
"region": "solian",
|
||||||
"configPath": "roadsign.toml"
|
"configPath": "roadsign.toml"
|
||||||
},
|
},
|
||||||
"deployments": [
|
"deployments": [
|
||||||
{
|
{
|
||||||
"region": "solian-next",
|
"region": "solian",
|
||||||
"site": "solian-next-web",
|
"site": "solian-web",
|
||||||
"path": "build/web"
|
"path": "build/web"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -17,7 +17,12 @@
|
|||||||
android:label="Solian"
|
android:label="Solian"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:enableOnBackInvokedCallback="true"
|
||||||
android:requestLegacyExternalStorage="true">
|
android:requestLegacyExternalStorage="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="flutterEmbedding"
|
||||||
|
android:value="2" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
|||||||
26
api/Paperclip/Activate Boost.bru
Normal file
26
api/Paperclip/Activate Boost.bru
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
meta {
|
||||||
|
name: Activate Boost
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{endpoint}}/cgi/uc/boosts/1/activate
|
||||||
|
body: none
|
||||||
|
auth: inherit
|
||||||
|
}
|
||||||
|
|
||||||
|
body:json {
|
||||||
|
{
|
||||||
|
"client_id": "{{third_client_id}}",
|
||||||
|
"client_secret":"{{third_client_tk}}",
|
||||||
|
"type": "general",
|
||||||
|
"subject": "Merry Christmas!",
|
||||||
|
"subtitle": "一条来自 Solar Network 团队的信息",
|
||||||
|
"content": "今天是 12 月 25 日 (UTC+8),小羊祝您圣诞快乐 🎄",
|
||||||
|
"metadata": {
|
||||||
|
"image": "6EqsYQwmFRCkbmhR"
|
||||||
|
},
|
||||||
|
"priority": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
19
api/Paperclip/Stickers/Create Sticker Pack.bru
Normal file
19
api/Paperclip/Stickers/Create Sticker Pack.bru
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
meta {
|
||||||
|
name: Create Sticker Pack
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{endpoint}}/cgi/uc/stickers/packs
|
||||||
|
body: json
|
||||||
|
auth: inherit
|
||||||
|
}
|
||||||
|
|
||||||
|
body:json {
|
||||||
|
{
|
||||||
|
"prefix": "cat",
|
||||||
|
"name": "Solar Network full of Cats!",
|
||||||
|
"description": "The sticker packs is full of stickers which related with cats!"
|
||||||
|
}
|
||||||
|
}
|
||||||
20
api/Paperclip/Stickers/Create Sticker.bru
Normal file
20
api/Paperclip/Stickers/Create Sticker.bru
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
meta {
|
||||||
|
name: Create Sticker
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{endpoint}}/cgi/uc/stickers
|
||||||
|
body: json
|
||||||
|
auth: inherit
|
||||||
|
}
|
||||||
|
|
||||||
|
body:json {
|
||||||
|
{
|
||||||
|
"alias": "AteChip",
|
||||||
|
"name": "Cat ate chips",
|
||||||
|
"attachment_id": "d0b692cc64054463",
|
||||||
|
"pack_id": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,11 +7,7 @@ meta {
|
|||||||
post {
|
post {
|
||||||
url: {{endpoint}}/cgi/id/dev/notify/all
|
url: {{endpoint}}/cgi/id/dev/notify/all
|
||||||
body: json
|
body: json
|
||||||
auth: bearer
|
auth: inherit
|
||||||
}
|
|
||||||
|
|
||||||
auth:bearer {
|
|
||||||
token: {{atk}}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body:json {
|
body:json {
|
||||||
|
|||||||
11
api/Reader/List News Sources.bru
Normal file
11
api/Reader/List News Sources.bru
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
meta {
|
||||||
|
name: List News Sources
|
||||||
|
type: http
|
||||||
|
seq: 3
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: {{endpoint}}/cgi/re/well-known/sources
|
||||||
|
body: none
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
17
api/Reader/List News.bru
Normal file
17
api/Reader/List News.bru
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
meta {
|
||||||
|
name: List News
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: {{endpoint}}/cgi/re/news?take=10&offset=0&source=shadiao
|
||||||
|
body: none
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
params:query {
|
||||||
|
take: 10
|
||||||
|
offset: 0
|
||||||
|
source: shadiao
|
||||||
|
}
|
||||||
18
api/Reader/Trigger Scan News.bru
Normal file
18
api/Reader/Trigger Scan News.bru
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
meta {
|
||||||
|
name: Trigger Scan News
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{endpoint}}/cgi/re/admin/scan
|
||||||
|
body: json
|
||||||
|
auth: inherit
|
||||||
|
}
|
||||||
|
|
||||||
|
body:json {
|
||||||
|
{
|
||||||
|
"sources": ["taiwan-ltn"],
|
||||||
|
"eager": true
|
||||||
|
}
|
||||||
|
}
|
||||||
7
api/collection.bru
Normal file
7
api/collection.bru
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
auth {
|
||||||
|
mode: bearer
|
||||||
|
}
|
||||||
|
|
||||||
|
auth:bearer {
|
||||||
|
token: {{atk}}
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
"screenAccountProfileEdit": "Edit Profile",
|
"screenAccountProfileEdit": "Edit Profile",
|
||||||
"screenAbuseReport": "Abuse Reports",
|
"screenAbuseReport": "Abuse Reports",
|
||||||
"screenSettings": "Settings",
|
"screenSettings": "Settings",
|
||||||
|
"screenNews": "News",
|
||||||
"screenAlbum": "Album",
|
"screenAlbum": "Album",
|
||||||
"screenChat": "Chat",
|
"screenChat": "Chat",
|
||||||
"screenChatManage": "Edit Channel",
|
"screenChatManage": "Edit Channel",
|
||||||
@@ -181,6 +182,8 @@
|
|||||||
"settingsAppearance": "Appearance",
|
"settingsAppearance": "Appearance",
|
||||||
"settingsAppBarTransparent": "Transparent App Bar",
|
"settingsAppBarTransparent": "Transparent App Bar",
|
||||||
"settingsAppBarTransparentDescription": "Enable transparent effect for the app bar.",
|
"settingsAppBarTransparentDescription": "Enable transparent effect for the app bar.",
|
||||||
|
"settingsDrawerPreferCollapse": "Prefer Drawer Collapse",
|
||||||
|
"settingsDrawerPreferCollapseDescription": "Make the drawer to collapse even when the screen is wide enough.",
|
||||||
"settingsBackgroundImage": "Background Image",
|
"settingsBackgroundImage": "Background Image",
|
||||||
"settingsBackgroundImageDescription": "Set the background image that will be applied globally.",
|
"settingsBackgroundImageDescription": "Set the background image that will be applied globally.",
|
||||||
"settingsBackgroundImageClear": "Clear Existing Background Image",
|
"settingsBackgroundImageClear": "Clear Existing Background Image",
|
||||||
@@ -191,6 +194,13 @@
|
|||||||
"settingsColorSchemeDescription": "Set the application primary color.",
|
"settingsColorSchemeDescription": "Set the application primary color.",
|
||||||
"settingsColorSeed": "Color Seed",
|
"settingsColorSeed": "Color Seed",
|
||||||
"settingsColorSeedDescription": "Select one of the present color schemes.",
|
"settingsColorSeedDescription": "Select one of the present color schemes.",
|
||||||
|
"settingsFeatures": "Features",
|
||||||
|
"settingsNotifyWithHaptic": "Haptic when Notified",
|
||||||
|
"settingsNotifyWithHapticDescription": "Vibrate lightly when a new notification appears in the foreground.",
|
||||||
|
"settingsExpandPostLink": "Expand Post Link",
|
||||||
|
"settingsExpandPostLinkDescription": "Expand the post link in the post list.",
|
||||||
|
"settingsExpandChatLink": "Expand Chat Link",
|
||||||
|
"settingsExpandChatLinkDescription": "Expand the chat link in the chat list.",
|
||||||
"settingsNetwork": "Network",
|
"settingsNetwork": "Network",
|
||||||
"settingsNetworkServer": "HyperNet Server",
|
"settingsNetworkServer": "HyperNet Server",
|
||||||
"settingsNetworkServerDescription": "Set the HyperNet server address, choose ours or build your own.",
|
"settingsNetworkServerDescription": "Set the HyperNet server address, choose ours or build your own.",
|
||||||
@@ -213,8 +223,9 @@
|
|||||||
"sensitiveContentCollapsed": "Sensitive content has been collapsed.",
|
"sensitiveContentCollapsed": "Sensitive content has been collapsed.",
|
||||||
"sensitiveContentDescription": "This content has been marked as sensitive, and may not be suitable for all viewers.",
|
"sensitiveContentDescription": "This content has been marked as sensitive, and may not be suitable for all viewers.",
|
||||||
"sensitiveContentReveal": "Reveal",
|
"sensitiveContentReveal": "Reveal",
|
||||||
"serverConnecting": "Connecting to server...",
|
"serverConnecting": "Connecting...",
|
||||||
"serverDisconnected": "Lost connection from server",
|
"serverDisconnected": "Connection Lost",
|
||||||
|
"serverConnected": "Connected",
|
||||||
"fieldChatAlias": "Channel Alias",
|
"fieldChatAlias": "Channel Alias",
|
||||||
"fieldChatAliasHint": "The unique channel alias within the site, used to represent the channel in URL, leave blank to auto generate. Should be URL-Safe.",
|
"fieldChatAliasHint": "The unique channel alias within the site, used to represent the channel in URL, leave blank to auto generate. Should be URL-Safe.",
|
||||||
"fieldChatName": "Name",
|
"fieldChatName": "Name",
|
||||||
@@ -281,18 +292,25 @@
|
|||||||
"one": "{} attachment",
|
"one": "{} attachment",
|
||||||
"other": "{} attachments"
|
"other": "{} attachments"
|
||||||
},
|
},
|
||||||
|
"messageTyping": {
|
||||||
|
"one": "{} is typing...",
|
||||||
|
"other": "{} are typing..."
|
||||||
|
},
|
||||||
"fieldAttachmentRandomId": "Random ID",
|
"fieldAttachmentRandomId": "Random ID",
|
||||||
|
"fieldAttachmentAlt": "Alternative text",
|
||||||
"addAttachmentFromAlbum": "Add from album",
|
"addAttachmentFromAlbum": "Add from album",
|
||||||
"addAttachmentFromClipboard": "Paste file",
|
"addAttachmentFromClipboard": "Paste file",
|
||||||
"addAttachmentFromCameraPhoto": "Take photo",
|
"addAttachmentFromCameraPhoto": "Take photo",
|
||||||
"addAttachmentFromCameraVideo": "Take video",
|
"addAttachmentFromCameraVideo": "Take video",
|
||||||
"addAttachmentFromRandomId": "Link via RID",
|
"addAttachmentFromRandomId": "Link via RID",
|
||||||
|
"attachmentDetailInfo": "Attachment details",
|
||||||
"attachmentPastedImage": "Pasted Image",
|
"attachmentPastedImage": "Pasted Image",
|
||||||
"attachmentInsertLink": "Insert Link",
|
"attachmentInsertLink": "Insert Link",
|
||||||
"attachmentSetAsPostThumbnail": "Set as post thumbnail",
|
"attachmentSetAsPostThumbnail": "Set as post thumbnail",
|
||||||
"attachmentUnsetAsPostThumbnail": "Unset as post thumbnail",
|
"attachmentUnsetAsPostThumbnail": "Unset as post thumbnail",
|
||||||
"attachmentCompressVideo": "Re-encode video",
|
"attachmentCompressVideo": "Re-encode video",
|
||||||
"attachmentSetThumbnail": "Set thumbnail",
|
"attachmentSetThumbnail": "Set thumbnail",
|
||||||
|
"attachmentSetAlt": "Set alternative text",
|
||||||
"attachmentCopyRandomId": "Copy RID",
|
"attachmentCopyRandomId": "Copy RID",
|
||||||
"attachmentUpload": "Upload",
|
"attachmentUpload": "Upload",
|
||||||
"attachmentInputDialog": "Upload attachments",
|
"attachmentInputDialog": "Upload attachments",
|
||||||
@@ -309,7 +327,15 @@
|
|||||||
"attachmentCompressQualityHint": "Solar Network doesn't prevent you from uploading large files, high resolution, high bitrate videos. But for your network conditions, we suggest you choose a suitable compression quality.",
|
"attachmentCompressQualityHint": "Solar Network doesn't prevent you from uploading large files, high resolution, high bitrate videos. But for your network conditions, we suggest you choose a suitable compression quality.",
|
||||||
"attachmentUploaded": "Uploaded",
|
"attachmentUploaded": "Uploaded",
|
||||||
"attachmentPending": "Pending",
|
"attachmentPending": "Pending",
|
||||||
"attachmentCopyCompressed": "Has compressed copy",
|
"attachmentCopyCompressed": "Copy compressed",
|
||||||
|
"attachmentGotBoosted": "Boosted",
|
||||||
|
"attachmentBoost": "Boost",
|
||||||
|
"attachmentCreateBoost": "Create Boost",
|
||||||
|
"attachmentBoostHint": "Boost is a feature that allows you to upload attachments to a server closer to your audience or a faster content network. This feature is currently in beta and is subject to change. It's all free for now, you can feel free to try, you will get notified when the pricing plan changed.",
|
||||||
|
"attachmentDestinationRegion": "Destination Region",
|
||||||
|
"attachmentDestinationRegionAPAC": "Asia Pacific",
|
||||||
|
"attachmentDestinationRegionNGB": "Ning Bo, China, Zhejiang",
|
||||||
|
"attachmentDestinationRegionHKG": "Hong Kong",
|
||||||
"notification": "Notification",
|
"notification": "Notification",
|
||||||
"notificationUnreadCount": {
|
"notificationUnreadCount": {
|
||||||
"zero": "All notifications read",
|
"zero": "All notifications read",
|
||||||
@@ -400,6 +426,9 @@
|
|||||||
"celebrateBirthday": "Happy birthday, {}!",
|
"celebrateBirthday": "Happy birthday, {}!",
|
||||||
"celebrateMerryXmas": "Merry christmas, {}!",
|
"celebrateMerryXmas": "Merry christmas, {}!",
|
||||||
"celebrateNewYear": "Happy new year, {}!",
|
"celebrateNewYear": "Happy new year, {}!",
|
||||||
|
"celebrateLunarNewYear": "Happy lunar new year, {}!",
|
||||||
|
"celebrateMidAutumn": "Happy mid-autumn festival, {}!",
|
||||||
|
"celebrateDragonBoat": "Happy dragon boat festival, {}!",
|
||||||
"celebrateValentineDay": "Today is valentine's day, {}!",
|
"celebrateValentineDay": "Today is valentine's day, {}!",
|
||||||
"celebrateLaborDay": "Today is labor day, {}.",
|
"celebrateLaborDay": "Today is labor day, {}.",
|
||||||
"celebrateMotherDay": "Today is mother's day, {}.",
|
"celebrateMotherDay": "Today is mother's day, {}.",
|
||||||
@@ -409,6 +438,9 @@
|
|||||||
"celebrateThanksgiving": "Today is thanksgiving day, {}!",
|
"celebrateThanksgiving": "Today is thanksgiving day, {}!",
|
||||||
"pendingBirthday": "Birthday in {}",
|
"pendingBirthday": "Birthday in {}",
|
||||||
"pendingMerryXmas": "Christmas in {}",
|
"pendingMerryXmas": "Christmas in {}",
|
||||||
|
"pendingLunarNewYear": "Lunar new year in {}",
|
||||||
|
"pendingMidAutumn": "Mid-autumn festival in {}",
|
||||||
|
"pendingDragonBoat": "Dragon boat festival in {}",
|
||||||
"pendingNewYear": "New year in {}",
|
"pendingNewYear": "New year in {}",
|
||||||
"pendingValentineDay": "Valentine's day in {}",
|
"pendingValentineDay": "Valentine's day in {}",
|
||||||
"pendingLaborDay": "Labor day in {}",
|
"pendingLaborDay": "Labor day in {}",
|
||||||
@@ -450,6 +482,7 @@
|
|||||||
"accountJoinedAt": "Joined at {}",
|
"accountJoinedAt": "Joined at {}",
|
||||||
"accountBirthday": "Born on {}",
|
"accountBirthday": "Born on {}",
|
||||||
"accountBadge": "Badge",
|
"accountBadge": "Badge",
|
||||||
|
"accountCheckInNoRecords": "No check-in records",
|
||||||
"badgeCompanyStaff": "Solsynth Staff",
|
"badgeCompanyStaff": "Solsynth Staff",
|
||||||
"badgeSiteMigration": "Solar Network Native",
|
"badgeSiteMigration": "Solar Network Native",
|
||||||
"accountStatus": "Status",
|
"accountStatus": "Status",
|
||||||
@@ -458,6 +491,7 @@
|
|||||||
"accountStatusLastSeen": "Last seen at {}",
|
"accountStatusLastSeen": "Last seen at {}",
|
||||||
"postArticle": "Article on the Solar Network",
|
"postArticle": "Article on the Solar Network",
|
||||||
"postStory": "Story on the Solar Network",
|
"postStory": "Story on the Solar Network",
|
||||||
|
"postLocalDraftRestored": "Restored from device",
|
||||||
"articleWrittenAt": "Written at {}",
|
"articleWrittenAt": "Written at {}",
|
||||||
"articleEditedAt": "Edited at {}",
|
"articleEditedAt": "Edited at {}",
|
||||||
"attachmentSaved": "Saved to album",
|
"attachmentSaved": "Saved to album",
|
||||||
@@ -525,5 +559,11 @@
|
|||||||
"postCategoryKnowledge": "Knowledge",
|
"postCategoryKnowledge": "Knowledge",
|
||||||
"postCategoryLiterature": "Literature",
|
"postCategoryLiterature": "Literature",
|
||||||
"postCategoryFunny": "Funny",
|
"postCategoryFunny": "Funny",
|
||||||
"postCategoryUncategorized": "Uncategorized"
|
"postCategoryUncategorized": "Uncategorized",
|
||||||
|
"newsAllSources": "All News",
|
||||||
|
"newsReadingProviderSwap": "Swap",
|
||||||
|
"newsReadingFromReader": "You're reading from HyperNet.Reader",
|
||||||
|
"newsReadingFromOriginal": "You're reading the original article",
|
||||||
|
"newsDisclaimer": "This article is fetched from the Internet, we do not guarantee its authenticity, please judge for yourself. All content in this article belongs to the original author.",
|
||||||
|
"newsToday": "Today's News"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"screenAccountProfileEdit": "编辑资料",
|
"screenAccountProfileEdit": "编辑资料",
|
||||||
"screenAbuseReport": "滥用检举",
|
"screenAbuseReport": "滥用检举",
|
||||||
"screenSettings": "设置",
|
"screenSettings": "设置",
|
||||||
|
"screenNews": "新闻",
|
||||||
"screenAlbum": "相册",
|
"screenAlbum": "相册",
|
||||||
"screenChat": "聊天",
|
"screenChat": "聊天",
|
||||||
"screenChatManage": "编辑聊天频道",
|
"screenChatManage": "编辑聊天频道",
|
||||||
@@ -185,10 +186,19 @@
|
|||||||
"settingsThemeMaterial3Description": "将应用主题设置为 Material 3 设计范式的主题。",
|
"settingsThemeMaterial3Description": "将应用主题设置为 Material 3 设计范式的主题。",
|
||||||
"settingsAppBarTransparent": "透明顶栏",
|
"settingsAppBarTransparent": "透明顶栏",
|
||||||
"settingsAppBarTransparentDescription": "为顶栏启用透明效果。",
|
"settingsAppBarTransparentDescription": "为顶栏启用透明效果。",
|
||||||
|
"settingsDrawerPreferCollapse": "侧边栏偏好折叠",
|
||||||
|
"settingsDrawerPreferCollapseDescription": "将侧边栏优先折叠,即使屏幕宽度足够大去放下整个侧边栏。",
|
||||||
"settingsColorScheme": "主题色",
|
"settingsColorScheme": "主题色",
|
||||||
"settingsColorSchemeDescription": "设置应用主题色。",
|
"settingsColorSchemeDescription": "设置应用主题色。",
|
||||||
"settingsColorSeed": "预设色彩主题",
|
"settingsColorSeed": "预设色彩主题",
|
||||||
"settingsColorSeedDescription": "选择一个预设色彩主题。",
|
"settingsColorSeedDescription": "选择一个预设色彩主题。",
|
||||||
|
"settingsFeatures": "功能",
|
||||||
|
"settingsNotifyWithHaptic": "新通知时振动",
|
||||||
|
"settingsNotifyWithHapticDescription": "在应用在前台时收到新通知出现时出发轻量的振动。",
|
||||||
|
"settingsExpandPostLink": "展开帖子链接",
|
||||||
|
"settingsExpandPostLinkDescription": "在帖子列表中展开显示帖子中的链接。",
|
||||||
|
"settingsExpandChatLink": "展开聊天链接",
|
||||||
|
"settingsExpandChatLinkDescription": "在聊天信息中展开显示内容中的链接。",
|
||||||
"settingsNetwork": "网络",
|
"settingsNetwork": "网络",
|
||||||
"settingsNetworkServer": "HyperNet 服务器",
|
"settingsNetworkServer": "HyperNet 服务器",
|
||||||
"settingsNetworkServerDescription": "设置 HyperNet 服务器地址,选择我们提供的,或者自己搭建。",
|
"settingsNetworkServerDescription": "设置 HyperNet 服务器地址,选择我们提供的,或者自己搭建。",
|
||||||
@@ -211,8 +221,9 @@
|
|||||||
"sensitiveContentCollapsed": "敏感内容已折叠。",
|
"sensitiveContentCollapsed": "敏感内容已折叠。",
|
||||||
"sensitiveContentDescription": "此内容已被标记,可能不适合所有人查看。",
|
"sensitiveContentDescription": "此内容已被标记,可能不适合所有人查看。",
|
||||||
"sensitiveContentReveal": "显示内容",
|
"sensitiveContentReveal": "显示内容",
|
||||||
"serverConnecting": "正在连接服务器…",
|
"serverConnecting": "正在连接…",
|
||||||
"serverDisconnected": "已与服务器断开连接",
|
"serverDisconnected": "已断开连接",
|
||||||
|
"serverConnected": "已连接",
|
||||||
"fieldChatAlias": "频道别名",
|
"fieldChatAlias": "频道别名",
|
||||||
"fieldChatAliasHint": "全站范围内唯一的频道别名,用于在 URL 中表示该频道,留空则自动生成。应遵循 URL-Safe 的原则。",
|
"fieldChatAliasHint": "全站范围内唯一的频道别名,用于在 URL 中表示该频道,留空则自动生成。应遵循 URL-Safe 的原则。",
|
||||||
"fieldChatName": "名称",
|
"fieldChatName": "名称",
|
||||||
@@ -279,18 +290,25 @@
|
|||||||
"one": "{} 个附件",
|
"one": "{} 个附件",
|
||||||
"other": "{} 个附件"
|
"other": "{} 个附件"
|
||||||
},
|
},
|
||||||
|
"messageTyping": {
|
||||||
|
"one": "{} 正在输入",
|
||||||
|
"other": "{} 正在输入"
|
||||||
|
},
|
||||||
"fieldAttachmentRandomId": "访问 ID",
|
"fieldAttachmentRandomId": "访问 ID",
|
||||||
|
"fieldAttachmentAlt": "概述文字",
|
||||||
"addAttachmentFromAlbum": "从相册中添加附件",
|
"addAttachmentFromAlbum": "从相册中添加附件",
|
||||||
"addAttachmentFromClipboard": "粘贴附件",
|
"addAttachmentFromClipboard": "粘贴附件",
|
||||||
"addAttachmentFromCameraPhoto": "拍摄照片",
|
"addAttachmentFromCameraPhoto": "拍摄照片",
|
||||||
"addAttachmentFromCameraVideo": "拍摄视频",
|
"addAttachmentFromCameraVideo": "拍摄视频",
|
||||||
"addAttachmentFromRandomId": "通过访问 ID 链接",
|
"addAttachmentFromRandomId": "通过访问 ID 链接",
|
||||||
|
"attachmentDetailInfo": "附件详细信息",
|
||||||
"attachmentPastedImage": "粘贴的图片",
|
"attachmentPastedImage": "粘贴的图片",
|
||||||
"attachmentInsertLink": "插入连接",
|
"attachmentInsertLink": "插入连接",
|
||||||
"attachmentSetAsPostThumbnail": "设置为帖子缩略图",
|
"attachmentSetAsPostThumbnail": "设置为帖子缩略图",
|
||||||
"attachmentUnsetAsPostThumbnail": "取消设置为帖子缩略图",
|
"attachmentUnsetAsPostThumbnail": "取消设置为帖子缩略图",
|
||||||
"attachmentCompressVideo": "重新编码视频",
|
"attachmentCompressVideo": "重新编码视频",
|
||||||
"attachmentSetThumbnail": "设置缩略图",
|
"attachmentSetThumbnail": "设置缩略图",
|
||||||
|
"attachmentSetAlt": "设置概述文字",
|
||||||
"attachmentCopyRandomId": "复制访问 ID",
|
"attachmentCopyRandomId": "复制访问 ID",
|
||||||
"attachmentUpload": "上传",
|
"attachmentUpload": "上传",
|
||||||
"attachmentInputDialog": "上传附件",
|
"attachmentInputDialog": "上传附件",
|
||||||
@@ -308,6 +326,14 @@
|
|||||||
"attachmentUploaded": "已上传",
|
"attachmentUploaded": "已上传",
|
||||||
"attachmentPending": "未上传",
|
"attachmentPending": "未上传",
|
||||||
"attachmentCopyCompressed": "有压缩副本",
|
"attachmentCopyCompressed": "有压缩副本",
|
||||||
|
"attachmentGotBoosted": "有加速传递",
|
||||||
|
"attachmentBoost": "加速包",
|
||||||
|
"attachmentCreateBoost": "加速传递",
|
||||||
|
"attachmentBoostHint": "加速传递允许您将附件上传到更近的受众或更快的内容网络。该功能目前处于 Beta 阶段。该功能限时免费,当有价格计划更改时,您将会被通知。",
|
||||||
|
"attachmentDestinationRegion": "目标节点",
|
||||||
|
"attachmentDestinationRegionAPAC": "亚太地区",
|
||||||
|
"attachmentDestinationRegionNGB": "中国 · 浙江 · 宁波",
|
||||||
|
"attachmentDestinationRegionHKG": "香港",
|
||||||
"notification": "通知",
|
"notification": "通知",
|
||||||
"notificationUnreadCount": {
|
"notificationUnreadCount": {
|
||||||
"zero": "无未读通知",
|
"zero": "无未读通知",
|
||||||
@@ -396,6 +422,9 @@
|
|||||||
"dailyCheckNegativeHint6": "出门",
|
"dailyCheckNegativeHint6": "出门",
|
||||||
"dailyCheckNegativeHint6Description": "忘带伞遇上大雨",
|
"dailyCheckNegativeHint6Description": "忘带伞遇上大雨",
|
||||||
"celebrateBirthday": "生日快乐,{}!",
|
"celebrateBirthday": "生日快乐,{}!",
|
||||||
|
"celebrateLunarNewYear": "春节快乐,{}!",
|
||||||
|
"celebrateMidAutumn": "中秋节快乐,{}!",
|
||||||
|
"celebrateDragonBoat": "端午节快乐,{}!",
|
||||||
"celebrateMerryXmas": "圣诞快乐,{}!",
|
"celebrateMerryXmas": "圣诞快乐,{}!",
|
||||||
"celebrateNewYear": "新年快乐,{}!",
|
"celebrateNewYear": "新年快乐,{}!",
|
||||||
"celebrateValentineDay": "今天是情人节,{}!",
|
"celebrateValentineDay": "今天是情人节,{}!",
|
||||||
@@ -405,6 +434,9 @@
|
|||||||
"celebrateFatherDay": "今天是父亲节,{}。",
|
"celebrateFatherDay": "今天是父亲节,{}。",
|
||||||
"celebrateHalloween": "快乐在圣诞节,{}!",
|
"celebrateHalloween": "快乐在圣诞节,{}!",
|
||||||
"celebrateThanksgiving": "今天是感恩节,{}!",
|
"celebrateThanksgiving": "今天是感恩节,{}!",
|
||||||
|
"pendingLunarNewYear": "{} 过春节",
|
||||||
|
"pendingMidAutumn": "{} 过中秋节",
|
||||||
|
"pendingDragonBoat": "{} 过端午节",
|
||||||
"pendingBirthday": "{} 过生日",
|
"pendingBirthday": "{} 过生日",
|
||||||
"pendingMerryXmas": "{} 过圣诞节",
|
"pendingMerryXmas": "{} 过圣诞节",
|
||||||
"pendingNewYear": "{} 跨年",
|
"pendingNewYear": "{} 跨年",
|
||||||
@@ -448,6 +480,7 @@
|
|||||||
"accountJoinedAt": "加入于 {}",
|
"accountJoinedAt": "加入于 {}",
|
||||||
"accountBirthday": "出生于 {}",
|
"accountBirthday": "出生于 {}",
|
||||||
"accountBadge": "徽章",
|
"accountBadge": "徽章",
|
||||||
|
"accountCheckInNoRecords": "暂无运势记录",
|
||||||
"badgeCompanyStaff": "索尔辛茨士大夫 · 员工",
|
"badgeCompanyStaff": "索尔辛茨士大夫 · 员工",
|
||||||
"badgeSiteMigration": "Solar Network 原住民",
|
"badgeSiteMigration": "Solar Network 原住民",
|
||||||
"accountStatus": "状态",
|
"accountStatus": "状态",
|
||||||
@@ -456,6 +489,7 @@
|
|||||||
"accountStatusLastSeen": "最后一次上线于 {}",
|
"accountStatusLastSeen": "最后一次上线于 {}",
|
||||||
"postArticle": "Solar Network 上的文章",
|
"postArticle": "Solar Network 上的文章",
|
||||||
"postStory": "Solar Network 上的故事",
|
"postStory": "Solar Network 上的故事",
|
||||||
|
"postLocalDraftRestored": "从本地恢复草稿",
|
||||||
"articleWrittenAt": "发表于 {}",
|
"articleWrittenAt": "发表于 {}",
|
||||||
"articleEditedAt": "编辑于 {}",
|
"articleEditedAt": "编辑于 {}",
|
||||||
"attachmentSaved": "已保存到相册",
|
"attachmentSaved": "已保存到相册",
|
||||||
@@ -523,5 +557,11 @@
|
|||||||
"postCategoryKnowledge": "知识",
|
"postCategoryKnowledge": "知识",
|
||||||
"postCategoryLiterature": "文学",
|
"postCategoryLiterature": "文学",
|
||||||
"postCategoryFunny": "搞笑",
|
"postCategoryFunny": "搞笑",
|
||||||
"postCategoryUncategorized": "未分类"
|
"postCategoryUncategorized": "未分类",
|
||||||
|
"newsAllSources": "所有新闻",
|
||||||
|
"newsReadingProviderSwap": "切换",
|
||||||
|
"newsReadingFromReader": "你正在从 HyperNet.Reader 阅读文章",
|
||||||
|
"newsReadingFromOriginal": "你正在阅读原始文章",
|
||||||
|
"newsDisclaimer": "本文由 HyperNet.Reader 从互联网上获取,我们不担保其内容的真实性,请自行判断。本文章的所有内容版权归原作者所有。",
|
||||||
|
"newsToday": "快讯"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"screenAccountProfileEdit": "編輯資料",
|
"screenAccountProfileEdit": "編輯資料",
|
||||||
"screenAbuseReport": "濫用檢舉",
|
"screenAbuseReport": "濫用檢舉",
|
||||||
"screenSettings": "設置",
|
"screenSettings": "設置",
|
||||||
|
"screenNews": "新聞",
|
||||||
"screenAlbum": "相冊",
|
"screenAlbum": "相冊",
|
||||||
"screenChat": "聊天",
|
"screenChat": "聊天",
|
||||||
"screenChatManage": "編輯聊天頻道",
|
"screenChatManage": "編輯聊天頻道",
|
||||||
@@ -185,10 +186,19 @@
|
|||||||
"settingsThemeMaterial3Description": "將應用主題設置為 Material 3 設計範式的主題。",
|
"settingsThemeMaterial3Description": "將應用主題設置為 Material 3 設計範式的主題。",
|
||||||
"settingsAppBarTransparent": "透明頂欄",
|
"settingsAppBarTransparent": "透明頂欄",
|
||||||
"settingsAppBarTransparentDescription": "為頂欄啓用透明效果。",
|
"settingsAppBarTransparentDescription": "為頂欄啓用透明效果。",
|
||||||
|
"settingsDrawerPreferCollapse": "側邊欄偏好摺疊",
|
||||||
|
"settingsDrawerPreferCollapseDescription": "將側邊欄優先摺疊,即使屏幕寬度足夠大去放下整個側邊欄。",
|
||||||
"settingsColorScheme": "主題色",
|
"settingsColorScheme": "主題色",
|
||||||
"settingsColorSchemeDescription": "設置應用主題色。",
|
"settingsColorSchemeDescription": "設置應用主題色。",
|
||||||
"settingsColorSeed": "預設色彩主題",
|
"settingsColorSeed": "預設色彩主題",
|
||||||
"settingsColorSeedDescription": "選擇一個預設色彩主題。",
|
"settingsColorSeedDescription": "選擇一個預設色彩主題。",
|
||||||
|
"settingsFeatures": "功能",
|
||||||
|
"settingsNotifyWithHaptic": "新通知時振動",
|
||||||
|
"settingsNotifyWithHapticDescription": "在應用在前台時收到新通知出現時出發輕量的振動。",
|
||||||
|
"settingsExpandPostLink": "展開帖子鏈接",
|
||||||
|
"settingsExpandPostLinkDescription": "在帖子列表中展開顯示帖子中的鏈接。",
|
||||||
|
"settingsExpandChatLink": "展開聊天鏈接",
|
||||||
|
"settingsExpandChatLinkDescription": "在聊天信息中展開顯示內容中的鏈接。",
|
||||||
"settingsNetwork": "網絡",
|
"settingsNetwork": "網絡",
|
||||||
"settingsNetworkServer": "HyperNet 服務器",
|
"settingsNetworkServer": "HyperNet 服務器",
|
||||||
"settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。",
|
"settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。",
|
||||||
@@ -211,8 +221,9 @@
|
|||||||
"sensitiveContentCollapsed": "敏感內容已摺疊。",
|
"sensitiveContentCollapsed": "敏感內容已摺疊。",
|
||||||
"sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。",
|
"sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。",
|
||||||
"sensitiveContentReveal": "顯示內容",
|
"sensitiveContentReveal": "顯示內容",
|
||||||
"serverConnecting": "正在連接服務器…",
|
"serverConnecting": "正在連接…",
|
||||||
"serverDisconnected": "已與服務器斷開連接",
|
"serverDisconnected": "已斷開連接",
|
||||||
|
"serverConnected": "已連接",
|
||||||
"fieldChatAlias": "頻道別名",
|
"fieldChatAlias": "頻道別名",
|
||||||
"fieldChatAliasHint": "全站範圍內唯一的頻道別名,用於在 URL 中表示該頻道,留空則自動生成。應遵循 URL-Safe 的原則。",
|
"fieldChatAliasHint": "全站範圍內唯一的頻道別名,用於在 URL 中表示該頻道,留空則自動生成。應遵循 URL-Safe 的原則。",
|
||||||
"fieldChatName": "名稱",
|
"fieldChatName": "名稱",
|
||||||
@@ -279,18 +290,25 @@
|
|||||||
"one": "{} 個附件",
|
"one": "{} 個附件",
|
||||||
"other": "{} 個附件"
|
"other": "{} 個附件"
|
||||||
},
|
},
|
||||||
|
"messageTyping": {
|
||||||
|
"one": "{} 正在輸入",
|
||||||
|
"other": "{} 正在輸入"
|
||||||
|
},
|
||||||
"fieldAttachmentRandomId": "訪問 ID",
|
"fieldAttachmentRandomId": "訪問 ID",
|
||||||
|
"fieldAttachmentAlt": "概述文字",
|
||||||
"addAttachmentFromAlbum": "從相冊中添加附件",
|
"addAttachmentFromAlbum": "從相冊中添加附件",
|
||||||
"addAttachmentFromClipboard": "粘貼附件",
|
"addAttachmentFromClipboard": "粘貼附件",
|
||||||
"addAttachmentFromCameraPhoto": "拍攝照片",
|
"addAttachmentFromCameraPhoto": "拍攝照片",
|
||||||
"addAttachmentFromCameraVideo": "拍攝視頻",
|
"addAttachmentFromCameraVideo": "拍攝視頻",
|
||||||
"addAttachmentFromRandomId": "通過訪問 ID 鏈接",
|
"addAttachmentFromRandomId": "通過訪問 ID 鏈接",
|
||||||
|
"attachmentDetailInfo": "附件詳細信息",
|
||||||
"attachmentPastedImage": "粘貼的圖片",
|
"attachmentPastedImage": "粘貼的圖片",
|
||||||
"attachmentInsertLink": "插入連接",
|
"attachmentInsertLink": "插入連接",
|
||||||
"attachmentSetAsPostThumbnail": "設置為帖子縮略圖",
|
"attachmentSetAsPostThumbnail": "設置為帖子縮略圖",
|
||||||
"attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖",
|
"attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖",
|
||||||
"attachmentCompressVideo": "重新編碼視頻",
|
"attachmentCompressVideo": "重新編碼視頻",
|
||||||
"attachmentSetThumbnail": "設置縮略圖",
|
"attachmentSetThumbnail": "設置縮略圖",
|
||||||
|
"attachmentSetAlt": "設置概述文字",
|
||||||
"attachmentCopyRandomId": "複製訪問 ID",
|
"attachmentCopyRandomId": "複製訪問 ID",
|
||||||
"attachmentUpload": "上傳",
|
"attachmentUpload": "上傳",
|
||||||
"attachmentInputDialog": "上傳附件",
|
"attachmentInputDialog": "上傳附件",
|
||||||
@@ -308,6 +326,14 @@
|
|||||||
"attachmentUploaded": "已上傳",
|
"attachmentUploaded": "已上傳",
|
||||||
"attachmentPending": "未上傳",
|
"attachmentPending": "未上傳",
|
||||||
"attachmentCopyCompressed": "有壓縮副本",
|
"attachmentCopyCompressed": "有壓縮副本",
|
||||||
|
"attachmentGotBoosted": "有加速傳遞",
|
||||||
|
"attachmentBoost": "加速包",
|
||||||
|
"attachmentCreateBoost": "加速傳遞",
|
||||||
|
"attachmentBoostHint": "加速傳遞允許您將附件上傳到更近的受眾或更快的內容網絡。該功能目前處於 Beta 階段。該功能限時免費,當有價格計劃更改時,您將會被通知。",
|
||||||
|
"attachmentDestinationRegion": "目標節點",
|
||||||
|
"attachmentDestinationRegionAPAC": "亞太地區",
|
||||||
|
"attachmentDestinationRegionNGB": "中國 · 浙江 · 寧波",
|
||||||
|
"attachmentDestinationRegionHKG": "香港",
|
||||||
"notification": "通知",
|
"notification": "通知",
|
||||||
"notificationUnreadCount": {
|
"notificationUnreadCount": {
|
||||||
"zero": "無未讀通知",
|
"zero": "無未讀通知",
|
||||||
@@ -396,6 +422,9 @@
|
|||||||
"dailyCheckNegativeHint6": "出門",
|
"dailyCheckNegativeHint6": "出門",
|
||||||
"dailyCheckNegativeHint6Description": "忘帶傘遇上大雨",
|
"dailyCheckNegativeHint6Description": "忘帶傘遇上大雨",
|
||||||
"celebrateBirthday": "生日快樂,{}!",
|
"celebrateBirthday": "生日快樂,{}!",
|
||||||
|
"celebrateLunarNewYear": "春節快樂,{}!",
|
||||||
|
"celebrateMidAutumn": "中秋節快樂,{}!",
|
||||||
|
"celebrateDragonBoat": "端午節快樂,{}!",
|
||||||
"celebrateMerryXmas": "聖誕快樂,{}!",
|
"celebrateMerryXmas": "聖誕快樂,{}!",
|
||||||
"celebrateNewYear": "新年快樂,{}!",
|
"celebrateNewYear": "新年快樂,{}!",
|
||||||
"celebrateValentineDay": "今天是情人節,{}!",
|
"celebrateValentineDay": "今天是情人節,{}!",
|
||||||
@@ -405,6 +434,9 @@
|
|||||||
"celebrateFatherDay": "今天是父親節,{}。",
|
"celebrateFatherDay": "今天是父親節,{}。",
|
||||||
"celebrateHalloween": "快樂在聖誕節,{}!",
|
"celebrateHalloween": "快樂在聖誕節,{}!",
|
||||||
"celebrateThanksgiving": "今天是感恩節,{}!",
|
"celebrateThanksgiving": "今天是感恩節,{}!",
|
||||||
|
"pendingLunarNewYear": "{} 過春節",
|
||||||
|
"pendingMidAutumn": "{} 過中秋節",
|
||||||
|
"pendingDragonBoat": "{} 過端午節",
|
||||||
"pendingBirthday": "{} 過生日",
|
"pendingBirthday": "{} 過生日",
|
||||||
"pendingMerryXmas": "{} 過聖誕節",
|
"pendingMerryXmas": "{} 過聖誕節",
|
||||||
"pendingNewYear": "{} 跨年",
|
"pendingNewYear": "{} 跨年",
|
||||||
@@ -448,6 +480,7 @@
|
|||||||
"accountJoinedAt": "加入於 {}",
|
"accountJoinedAt": "加入於 {}",
|
||||||
"accountBirthday": "出生於 {}",
|
"accountBirthday": "出生於 {}",
|
||||||
"accountBadge": "徽章",
|
"accountBadge": "徽章",
|
||||||
|
"accountCheckInNoRecords": "暫無運勢記錄",
|
||||||
"badgeCompanyStaff": "索爾辛茨士大夫 · 員工",
|
"badgeCompanyStaff": "索爾辛茨士大夫 · 員工",
|
||||||
"badgeSiteMigration": "Solar Network 原住民",
|
"badgeSiteMigration": "Solar Network 原住民",
|
||||||
"accountStatus": "狀態",
|
"accountStatus": "狀態",
|
||||||
@@ -456,6 +489,7 @@
|
|||||||
"accountStatusLastSeen": "最後一次上線於 {}",
|
"accountStatusLastSeen": "最後一次上線於 {}",
|
||||||
"postArticle": "Solar Network 上的文章",
|
"postArticle": "Solar Network 上的文章",
|
||||||
"postStory": "Solar Network 上的故事",
|
"postStory": "Solar Network 上的故事",
|
||||||
|
"postLocalDraftRestored": "從本地恢復草稿",
|
||||||
"articleWrittenAt": "發表於 {}",
|
"articleWrittenAt": "發表於 {}",
|
||||||
"articleEditedAt": "編輯於 {}",
|
"articleEditedAt": "編輯於 {}",
|
||||||
"attachmentSaved": "已保存到相冊",
|
"attachmentSaved": "已保存到相冊",
|
||||||
@@ -523,5 +557,11 @@
|
|||||||
"postCategoryKnowledge": "知識",
|
"postCategoryKnowledge": "知識",
|
||||||
"postCategoryLiterature": "文學",
|
"postCategoryLiterature": "文學",
|
||||||
"postCategoryFunny": "搞笑",
|
"postCategoryFunny": "搞笑",
|
||||||
"postCategoryUncategorized": "未分類"
|
"postCategoryUncategorized": "未分類",
|
||||||
|
"newsAllSources": "所有新聞",
|
||||||
|
"newsReadingProviderSwap": "切換",
|
||||||
|
"newsReadingFromReader": "你正在從 HyperNet.Reader 閲讀文章",
|
||||||
|
"newsReadingFromOriginal": "你正在閲讀原始文章",
|
||||||
|
"newsDisclaimer": "本文由 HyperNet.Reader 從互聯網上獲取,我們不擔保其內容的真實性,請自行判斷。本文章的所有內容版權歸原作者所有。",
|
||||||
|
"newsToday": "快訊"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"screenAccountProfileEdit": "編輯資料",
|
"screenAccountProfileEdit": "編輯資料",
|
||||||
"screenAbuseReport": "濫用檢舉",
|
"screenAbuseReport": "濫用檢舉",
|
||||||
"screenSettings": "設置",
|
"screenSettings": "設置",
|
||||||
|
"screenNews": "新聞",
|
||||||
"screenAlbum": "相冊",
|
"screenAlbum": "相冊",
|
||||||
"screenChat": "聊天",
|
"screenChat": "聊天",
|
||||||
"screenChatManage": "編輯聊天頻道",
|
"screenChatManage": "編輯聊天頻道",
|
||||||
@@ -185,10 +186,19 @@
|
|||||||
"settingsThemeMaterial3Description": "將應用主題設置為 Material 3 設計範式的主題。",
|
"settingsThemeMaterial3Description": "將應用主題設置為 Material 3 設計範式的主題。",
|
||||||
"settingsAppBarTransparent": "透明頂欄",
|
"settingsAppBarTransparent": "透明頂欄",
|
||||||
"settingsAppBarTransparentDescription": "為頂欄啟用透明效果。",
|
"settingsAppBarTransparentDescription": "為頂欄啟用透明效果。",
|
||||||
|
"settingsDrawerPreferCollapse": "側邊欄偏好摺疊",
|
||||||
|
"settingsDrawerPreferCollapseDescription": "將側邊欄優先摺疊,即使屏幕寬度足夠大去放下整個側邊欄。",
|
||||||
"settingsColorScheme": "主題色",
|
"settingsColorScheme": "主題色",
|
||||||
"settingsColorSchemeDescription": "設置應用主題色。",
|
"settingsColorSchemeDescription": "設置應用主題色。",
|
||||||
"settingsColorSeed": "預設色彩主題",
|
"settingsColorSeed": "預設色彩主題",
|
||||||
"settingsColorSeedDescription": "選擇一個預設色彩主題。",
|
"settingsColorSeedDescription": "選擇一個預設色彩主題。",
|
||||||
|
"settingsFeatures": "功能",
|
||||||
|
"settingsNotifyWithHaptic": "新通知時振動",
|
||||||
|
"settingsNotifyWithHapticDescription": "在應用在前臺時收到新通知出現時出發輕量的振動。",
|
||||||
|
"settingsExpandPostLink": "展開帖子鏈接",
|
||||||
|
"settingsExpandPostLinkDescription": "在帖子列表中展開顯示帖子中的鏈接。",
|
||||||
|
"settingsExpandChatLink": "展開聊天鏈接",
|
||||||
|
"settingsExpandChatLinkDescription": "在聊天信息中展開顯示內容中的鏈接。",
|
||||||
"settingsNetwork": "網絡",
|
"settingsNetwork": "網絡",
|
||||||
"settingsNetworkServer": "HyperNet 服務器",
|
"settingsNetworkServer": "HyperNet 服務器",
|
||||||
"settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。",
|
"settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。",
|
||||||
@@ -211,8 +221,9 @@
|
|||||||
"sensitiveContentCollapsed": "敏感內容已摺疊。",
|
"sensitiveContentCollapsed": "敏感內容已摺疊。",
|
||||||
"sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。",
|
"sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。",
|
||||||
"sensitiveContentReveal": "顯示內容",
|
"sensitiveContentReveal": "顯示內容",
|
||||||
"serverConnecting": "正在連接服務器…",
|
"serverConnecting": "正在連接…",
|
||||||
"serverDisconnected": "已與服務器斷開連接",
|
"serverDisconnected": "已斷開連接",
|
||||||
|
"serverConnected": "已連接",
|
||||||
"fieldChatAlias": "頻道別名",
|
"fieldChatAlias": "頻道別名",
|
||||||
"fieldChatAliasHint": "全站範圍內唯一的頻道別名,用於在 URL 中表示該頻道,留空則自動生成。應遵循 URL-Safe 的原則。",
|
"fieldChatAliasHint": "全站範圍內唯一的頻道別名,用於在 URL 中表示該頻道,留空則自動生成。應遵循 URL-Safe 的原則。",
|
||||||
"fieldChatName": "名稱",
|
"fieldChatName": "名稱",
|
||||||
@@ -279,18 +290,25 @@
|
|||||||
"one": "{} 個附件",
|
"one": "{} 個附件",
|
||||||
"other": "{} 個附件"
|
"other": "{} 個附件"
|
||||||
},
|
},
|
||||||
|
"messageTyping": {
|
||||||
|
"one": "{} 正在輸入",
|
||||||
|
"other": "{} 正在輸入"
|
||||||
|
},
|
||||||
"fieldAttachmentRandomId": "訪問 ID",
|
"fieldAttachmentRandomId": "訪問 ID",
|
||||||
|
"fieldAttachmentAlt": "概述文字",
|
||||||
"addAttachmentFromAlbum": "從相冊中添加附件",
|
"addAttachmentFromAlbum": "從相冊中添加附件",
|
||||||
"addAttachmentFromClipboard": "粘貼附件",
|
"addAttachmentFromClipboard": "粘貼附件",
|
||||||
"addAttachmentFromCameraPhoto": "拍攝照片",
|
"addAttachmentFromCameraPhoto": "拍攝照片",
|
||||||
"addAttachmentFromCameraVideo": "拍攝視頻",
|
"addAttachmentFromCameraVideo": "拍攝視頻",
|
||||||
"addAttachmentFromRandomId": "通過訪問 ID 鏈接",
|
"addAttachmentFromRandomId": "通過訪問 ID 鏈接",
|
||||||
|
"attachmentDetailInfo": "附件詳細信息",
|
||||||
"attachmentPastedImage": "粘貼的圖片",
|
"attachmentPastedImage": "粘貼的圖片",
|
||||||
"attachmentInsertLink": "插入連接",
|
"attachmentInsertLink": "插入連接",
|
||||||
"attachmentSetAsPostThumbnail": "設置為帖子縮略圖",
|
"attachmentSetAsPostThumbnail": "設置為帖子縮略圖",
|
||||||
"attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖",
|
"attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖",
|
||||||
"attachmentCompressVideo": "重新編碼視頻",
|
"attachmentCompressVideo": "重新編碼視頻",
|
||||||
"attachmentSetThumbnail": "設置縮略圖",
|
"attachmentSetThumbnail": "設置縮略圖",
|
||||||
|
"attachmentSetAlt": "設置概述文字",
|
||||||
"attachmentCopyRandomId": "複製訪問 ID",
|
"attachmentCopyRandomId": "複製訪問 ID",
|
||||||
"attachmentUpload": "上傳",
|
"attachmentUpload": "上傳",
|
||||||
"attachmentInputDialog": "上傳附件",
|
"attachmentInputDialog": "上傳附件",
|
||||||
@@ -308,6 +326,14 @@
|
|||||||
"attachmentUploaded": "已上傳",
|
"attachmentUploaded": "已上傳",
|
||||||
"attachmentPending": "未上傳",
|
"attachmentPending": "未上傳",
|
||||||
"attachmentCopyCompressed": "有壓縮副本",
|
"attachmentCopyCompressed": "有壓縮副本",
|
||||||
|
"attachmentGotBoosted": "有加速傳遞",
|
||||||
|
"attachmentBoost": "加速包",
|
||||||
|
"attachmentCreateBoost": "加速傳遞",
|
||||||
|
"attachmentBoostHint": "加速傳遞允許您將附件上傳到更近的受眾或更快的內容網絡。該功能目前處於 Beta 階段。該功能限時免費,當有價格計劃更改時,您將會被通知。",
|
||||||
|
"attachmentDestinationRegion": "目標節點",
|
||||||
|
"attachmentDestinationRegionAPAC": "亞太地區",
|
||||||
|
"attachmentDestinationRegionNGB": "中國 · 浙江 · 寧波",
|
||||||
|
"attachmentDestinationRegionHKG": "香港",
|
||||||
"notification": "通知",
|
"notification": "通知",
|
||||||
"notificationUnreadCount": {
|
"notificationUnreadCount": {
|
||||||
"zero": "無未讀通知",
|
"zero": "無未讀通知",
|
||||||
@@ -396,6 +422,9 @@
|
|||||||
"dailyCheckNegativeHint6": "出門",
|
"dailyCheckNegativeHint6": "出門",
|
||||||
"dailyCheckNegativeHint6Description": "忘帶傘遇上大雨",
|
"dailyCheckNegativeHint6Description": "忘帶傘遇上大雨",
|
||||||
"celebrateBirthday": "生日快樂,{}!",
|
"celebrateBirthday": "生日快樂,{}!",
|
||||||
|
"celebrateLunarNewYear": "春節快樂,{}!",
|
||||||
|
"celebrateMidAutumn": "中秋節快樂,{}!",
|
||||||
|
"celebrateDragonBoat": "端午節快樂,{}!",
|
||||||
"celebrateMerryXmas": "聖誕快樂,{}!",
|
"celebrateMerryXmas": "聖誕快樂,{}!",
|
||||||
"celebrateNewYear": "新年快樂,{}!",
|
"celebrateNewYear": "新年快樂,{}!",
|
||||||
"celebrateValentineDay": "今天是情人節,{}!",
|
"celebrateValentineDay": "今天是情人節,{}!",
|
||||||
@@ -405,6 +434,9 @@
|
|||||||
"celebrateFatherDay": "今天是父親節,{}。",
|
"celebrateFatherDay": "今天是父親節,{}。",
|
||||||
"celebrateHalloween": "快樂在聖誕節,{}!",
|
"celebrateHalloween": "快樂在聖誕節,{}!",
|
||||||
"celebrateThanksgiving": "今天是感恩節,{}!",
|
"celebrateThanksgiving": "今天是感恩節,{}!",
|
||||||
|
"pendingLunarNewYear": "{} 過春節",
|
||||||
|
"pendingMidAutumn": "{} 過中秋節",
|
||||||
|
"pendingDragonBoat": "{} 過端午節",
|
||||||
"pendingBirthday": "{} 過生日",
|
"pendingBirthday": "{} 過生日",
|
||||||
"pendingMerryXmas": "{} 過聖誕節",
|
"pendingMerryXmas": "{} 過聖誕節",
|
||||||
"pendingNewYear": "{} 跨年",
|
"pendingNewYear": "{} 跨年",
|
||||||
@@ -448,6 +480,7 @@
|
|||||||
"accountJoinedAt": "加入於 {}",
|
"accountJoinedAt": "加入於 {}",
|
||||||
"accountBirthday": "出生於 {}",
|
"accountBirthday": "出生於 {}",
|
||||||
"accountBadge": "徽章",
|
"accountBadge": "徽章",
|
||||||
|
"accountCheckInNoRecords": "暫無運勢記錄",
|
||||||
"badgeCompanyStaff": "索爾辛茨士大夫 · 員工",
|
"badgeCompanyStaff": "索爾辛茨士大夫 · 員工",
|
||||||
"badgeSiteMigration": "Solar Network 原住民",
|
"badgeSiteMigration": "Solar Network 原住民",
|
||||||
"accountStatus": "狀態",
|
"accountStatus": "狀態",
|
||||||
@@ -456,6 +489,7 @@
|
|||||||
"accountStatusLastSeen": "最後一次上線於 {}",
|
"accountStatusLastSeen": "最後一次上線於 {}",
|
||||||
"postArticle": "Solar Network 上的文章",
|
"postArticle": "Solar Network 上的文章",
|
||||||
"postStory": "Solar Network 上的故事",
|
"postStory": "Solar Network 上的故事",
|
||||||
|
"postLocalDraftRestored": "從本地恢復草稿",
|
||||||
"articleWrittenAt": "發表於 {}",
|
"articleWrittenAt": "發表於 {}",
|
||||||
"articleEditedAt": "編輯於 {}",
|
"articleEditedAt": "編輯於 {}",
|
||||||
"attachmentSaved": "已保存到相冊",
|
"attachmentSaved": "已保存到相冊",
|
||||||
@@ -523,5 +557,11 @@
|
|||||||
"postCategoryKnowledge": "知識",
|
"postCategoryKnowledge": "知識",
|
||||||
"postCategoryLiterature": "文學",
|
"postCategoryLiterature": "文學",
|
||||||
"postCategoryFunny": "搞笑",
|
"postCategoryFunny": "搞笑",
|
||||||
"postCategoryUncategorized": "未分類"
|
"postCategoryUncategorized": "未分類",
|
||||||
|
"newsAllSources": "所有新聞",
|
||||||
|
"newsReadingProviderSwap": "切換",
|
||||||
|
"newsReadingFromReader": "你正在從 HyperNet.Reader 閱讀文章",
|
||||||
|
"newsReadingFromOriginal": "你正在閱讀原始文章",
|
||||||
|
"newsDisclaimer": "本文由 HyperNet.Reader 從互聯網上獲取,我們不擔保其內容的真實性,請自行判斷。本文章的所有內容版權歸原作者所有。",
|
||||||
|
"newsToday": "快訊"
|
||||||
}
|
}
|
||||||
|
|||||||
102
ios/Podfile.lock
102
ios/Podfile.lock
@@ -43,58 +43,58 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- file_saver (0.0.1):
|
- file_saver (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- Firebase/Analytics (11.4.0):
|
- Firebase/Analytics (11.6.0):
|
||||||
- Firebase/Core
|
- Firebase/Core
|
||||||
- Firebase/Core (11.4.0):
|
- Firebase/Core (11.6.0):
|
||||||
- Firebase/CoreOnly
|
- Firebase/CoreOnly
|
||||||
- FirebaseAnalytics (~> 11.4.0)
|
- FirebaseAnalytics (~> 11.6.0)
|
||||||
- Firebase/CoreOnly (11.4.0):
|
- Firebase/CoreOnly (11.6.0):
|
||||||
- FirebaseCore (= 11.4.0)
|
- FirebaseCore (~> 11.6.0)
|
||||||
- Firebase/Messaging (11.4.0):
|
- Firebase/Messaging (11.6.0):
|
||||||
- Firebase/CoreOnly
|
- Firebase/CoreOnly
|
||||||
- FirebaseMessaging (~> 11.4.0)
|
- FirebaseMessaging (~> 11.6.0)
|
||||||
- firebase_analytics (11.3.6):
|
- firebase_analytics (11.4.0):
|
||||||
- Firebase/Analytics (= 11.4.0)
|
- Firebase/Analytics (= 11.6.0)
|
||||||
- firebase_core
|
- firebase_core
|
||||||
- Flutter
|
- Flutter
|
||||||
- firebase_core (3.9.0):
|
- firebase_core (3.10.0):
|
||||||
- Firebase/CoreOnly (= 11.4.0)
|
- Firebase/CoreOnly (= 11.6.0)
|
||||||
- Flutter
|
- Flutter
|
||||||
- firebase_messaging (15.1.6):
|
- firebase_messaging (15.2.0):
|
||||||
- Firebase/Messaging (= 11.4.0)
|
- Firebase/Messaging (= 11.6.0)
|
||||||
- firebase_core
|
- firebase_core
|
||||||
- Flutter
|
- Flutter
|
||||||
- FirebaseAnalytics (11.4.0):
|
- FirebaseAnalytics (11.6.0):
|
||||||
- FirebaseAnalytics/AdIdSupport (= 11.4.0)
|
- FirebaseAnalytics/AdIdSupport (= 11.6.0)
|
||||||
- FirebaseCore (~> 11.0)
|
- FirebaseCore (~> 11.6.0)
|
||||||
- FirebaseInstallations (~> 11.0)
|
- FirebaseInstallations (~> 11.0)
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||||
- GoogleUtilities/Network (~> 8.0)
|
- GoogleUtilities/Network (~> 8.0)
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- FirebaseAnalytics/AdIdSupport (11.4.0):
|
- FirebaseAnalytics/AdIdSupport (11.6.0):
|
||||||
- FirebaseCore (~> 11.0)
|
- FirebaseCore (~> 11.6.0)
|
||||||
- FirebaseInstallations (~> 11.0)
|
- FirebaseInstallations (~> 11.0)
|
||||||
- GoogleAppMeasurement (= 11.4.0)
|
- GoogleAppMeasurement (= 11.6.0)
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||||
- GoogleUtilities/Network (~> 8.0)
|
- GoogleUtilities/Network (~> 8.0)
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- FirebaseCore (11.4.0):
|
- FirebaseCore (11.6.0):
|
||||||
- FirebaseCoreInternal (~> 11.0)
|
- FirebaseCoreInternal (~> 11.6.0)
|
||||||
- GoogleUtilities/Environment (~> 8.0)
|
- GoogleUtilities/Environment (~> 8.0)
|
||||||
- GoogleUtilities/Logger (~> 8.0)
|
- GoogleUtilities/Logger (~> 8.0)
|
||||||
- FirebaseCoreInternal (11.6.0):
|
- FirebaseCoreInternal (11.6.0):
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||||
- FirebaseInstallations (11.4.0):
|
- FirebaseInstallations (11.6.0):
|
||||||
- FirebaseCore (~> 11.0)
|
- FirebaseCore (~> 11.6.0)
|
||||||
- GoogleUtilities/Environment (~> 8.0)
|
- GoogleUtilities/Environment (~> 8.0)
|
||||||
- GoogleUtilities/UserDefaults (~> 8.0)
|
- GoogleUtilities/UserDefaults (~> 8.0)
|
||||||
- PromisesObjC (~> 2.4)
|
- PromisesObjC (~> 2.4)
|
||||||
- FirebaseMessaging (11.4.0):
|
- FirebaseMessaging (11.6.0):
|
||||||
- FirebaseCore (~> 11.0)
|
- FirebaseCore (~> 11.6.0)
|
||||||
- FirebaseInstallations (~> 11.0)
|
- FirebaseInstallations (~> 11.0)
|
||||||
- GoogleDataTransport (~> 10.0)
|
- GoogleDataTransport (~> 10.0)
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||||
@@ -105,32 +105,39 @@ PODS:
|
|||||||
- Flutter (1.0.0)
|
- Flutter (1.0.0)
|
||||||
- flutter_app_update (0.0.1):
|
- flutter_app_update (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- flutter_inappwebview_ios (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
- flutter_inappwebview_ios/Core (= 0.0.1)
|
||||||
|
- OrderedSet (~> 6.0.3)
|
||||||
|
- flutter_inappwebview_ios/Core (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
- OrderedSet (~> 6.0.3)
|
||||||
- flutter_native_splash (2.4.3):
|
- flutter_native_splash (2.4.3):
|
||||||
- Flutter
|
- Flutter
|
||||||
- flutter_udid (0.0.1):
|
- flutter_udid (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- SAMKeychain
|
- SAMKeychain
|
||||||
- flutter_webrtc (0.12.2):
|
- flutter_webrtc (0.12.6):
|
||||||
- Flutter
|
- Flutter
|
||||||
- WebRTC-SDK (= 125.6422.06)
|
- WebRTC-SDK (= 125.6422.06)
|
||||||
- gal (1.0.0):
|
- gal (1.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- GoogleAppMeasurement (11.4.0):
|
- GoogleAppMeasurement (11.6.0):
|
||||||
- GoogleAppMeasurement/AdIdSupport (= 11.4.0)
|
- GoogleAppMeasurement/AdIdSupport (= 11.6.0)
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||||
- GoogleUtilities/Network (~> 8.0)
|
- GoogleUtilities/Network (~> 8.0)
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- GoogleAppMeasurement/AdIdSupport (11.4.0):
|
- GoogleAppMeasurement/AdIdSupport (11.6.0):
|
||||||
- GoogleAppMeasurement/WithoutAdIdSupport (= 11.4.0)
|
- GoogleAppMeasurement/WithoutAdIdSupport (= 11.6.0)
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||||
- GoogleUtilities/Network (~> 8.0)
|
- GoogleUtilities/Network (~> 8.0)
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- GoogleAppMeasurement/WithoutAdIdSupport (11.4.0):
|
- GoogleAppMeasurement/WithoutAdIdSupport (11.6.0):
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||||
- GoogleUtilities/Network (~> 8.0)
|
- GoogleUtilities/Network (~> 8.0)
|
||||||
@@ -173,7 +180,7 @@ PODS:
|
|||||||
- in_app_review (2.0.0):
|
- in_app_review (2.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- Kingfisher (8.1.3)
|
- Kingfisher (8.1.3)
|
||||||
- livekit_client (2.3.3):
|
- livekit_client (2.3.5):
|
||||||
- Flutter
|
- Flutter
|
||||||
- flutter_webrtc
|
- flutter_webrtc
|
||||||
- WebRTC-SDK (= 125.6422.06)
|
- WebRTC-SDK (= 125.6422.06)
|
||||||
@@ -188,6 +195,7 @@ PODS:
|
|||||||
- nanopb/encode (= 3.30910.0)
|
- nanopb/encode (= 3.30910.0)
|
||||||
- nanopb/decode (3.30910.0)
|
- nanopb/decode (3.30910.0)
|
||||||
- nanopb/encode (3.30910.0)
|
- nanopb/encode (3.30910.0)
|
||||||
|
- OrderedSet (6.0.3)
|
||||||
- package_info_plus (0.4.5):
|
- package_info_plus (0.4.5):
|
||||||
- Flutter
|
- Flutter
|
||||||
- pasteboard (0.0.1):
|
- pasteboard (0.0.1):
|
||||||
@@ -239,6 +247,7 @@ DEPENDENCIES:
|
|||||||
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
|
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
|
||||||
- Flutter (from `Flutter`)
|
- Flutter (from `Flutter`)
|
||||||
- flutter_app_update (from `.symlinks/plugins/flutter_app_update/ios`)
|
- flutter_app_update (from `.symlinks/plugins/flutter_app_update/ios`)
|
||||||
|
- flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`)
|
||||||
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
||||||
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
|
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
|
||||||
- flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
|
- flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
|
||||||
@@ -282,6 +291,7 @@ SPEC REPOS:
|
|||||||
- GoogleUtilities
|
- GoogleUtilities
|
||||||
- Kingfisher
|
- Kingfisher
|
||||||
- nanopb
|
- nanopb
|
||||||
|
- OrderedSet
|
||||||
- PromisesObjC
|
- PromisesObjC
|
||||||
- SAMKeychain
|
- SAMKeychain
|
||||||
- SDWebImage
|
- SDWebImage
|
||||||
@@ -309,6 +319,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: Flutter
|
:path: Flutter
|
||||||
flutter_app_update:
|
flutter_app_update:
|
||||||
:path: ".symlinks/plugins/flutter_app_update/ios"
|
:path: ".symlinks/plugins/flutter_app_update/ios"
|
||||||
|
flutter_inappwebview_ios:
|
||||||
|
:path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
|
||||||
flutter_native_splash:
|
flutter_native_splash:
|
||||||
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
||||||
flutter_udid:
|
flutter_udid:
|
||||||
@@ -369,33 +381,35 @@ SPEC CHECKSUMS:
|
|||||||
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||||
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
|
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
|
||||||
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
|
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
|
||||||
Firebase: cf1b19f21410b029b6786a54e9764a0cacad3c99
|
Firebase: 374a441a91ead896215703a674d58cdb3e9d772b
|
||||||
firebase_analytics: 2815af29d49c1a994652abd37a5b001a88bc7b75
|
firebase_analytics: 07bd7cfbac54bfcdccf2bb2530f9a65486f7ef3f
|
||||||
firebase_core: b62a5080210edad3f2934314a8b2c6f5124e8e10
|
firebase_core: feb37e79f775c2bd08dd35e02d83678291317e10
|
||||||
firebase_messaging: 98619a0572d82cfb3668e78859ba9f1110e268c9
|
firebase_messaging: e2f0ba891b1509668c07f5099761518a5af8fe3c
|
||||||
FirebaseAnalytics: 3feef9ae8733c567866342a1000691baaa7cad49
|
FirebaseAnalytics: 7114c698cac995602e3b1b96663473e50d54d6e7
|
||||||
FirebaseCore: e0510f1523bc0eb21653cac00792e1e2bd6f1771
|
FirebaseCore: 48b0dd707581cf9c1a1220da68223fb0a562afaa
|
||||||
FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2
|
FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2
|
||||||
FirebaseInstallations: 6ef4a1c7eb2a61ee1f74727d7f6ce2e72acf1414
|
FirebaseInstallations: efc0946fc756e4d22d8113f7c761948120322e8c
|
||||||
FirebaseMessaging: f8a160d99c2c2e5babbbcc90c4a3e15db036aee2
|
FirebaseMessaging: e1aca1fcc23e8b9eddb0e33f375ff90944623021
|
||||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||||
flutter_app_update: 65f61da626cb111d1b24674abc4b01728d7723bc
|
flutter_app_update: 65f61da626cb111d1b24674abc4b01728d7723bc
|
||||||
flutter_native_splash: e8a1e01082d97a8099d973f919f57904c925008a
|
flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4
|
||||||
|
flutter_native_splash: f71420956eb811e6d310720fee915f1d42852e7a
|
||||||
flutter_udid: b2417673f287ee62817a1de3d1643f47b9f508ab
|
flutter_udid: b2417673f287ee62817a1de3d1643f47b9f508ab
|
||||||
flutter_webrtc: 1a53bd24f97bcfeff512f13699e721897f261563
|
flutter_webrtc: 90260f83024b1b96d239a575ea4e3708e79344d1
|
||||||
gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5
|
gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5
|
||||||
GoogleAppMeasurement: 987769c4ca6b968f2479fbcc9fe3ce34af454b8e
|
GoogleAppMeasurement: 6a9e6317b6a6d810ad03d4a66564ca6c4c5818a3
|
||||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||||
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
|
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
|
||||||
home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57
|
home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57
|
||||||
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
||||||
in_app_review: a31b5257259646ea78e0e35fc914979b0031d011
|
in_app_review: a31b5257259646ea78e0e35fc914979b0031d011
|
||||||
Kingfisher: f2af9028b16baf9dc6c07c570072bc41cbf009ef
|
Kingfisher: f2af9028b16baf9dc6c07c570072bc41cbf009ef
|
||||||
livekit_client: 02cf2cc4357a655af12ccee70ff5596ae4e6feef
|
livekit_client: dcc5fd47ba69c98fc6baeb12e862c9d43807d976
|
||||||
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
|
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
|
||||||
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
|
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
|
||||||
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
|
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
|
||||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||||
|
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
||||||
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
|
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
|
||||||
pasteboard: 982969ebaa7c78af3e6cc7761e8f5e77565d9ce0
|
pasteboard: 982969ebaa7c78af3e6cc7761e8f5e77565d9ce0
|
||||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
@@ -11,6 +12,7 @@ import 'package:surface/providers/sn_network.dart';
|
|||||||
import 'package:surface/providers/user_directory.dart';
|
import 'package:surface/providers/user_directory.dart';
|
||||||
import 'package:surface/providers/websocket.dart';
|
import 'package:surface/providers/websocket.dart';
|
||||||
import 'package:surface/types/chat.dart';
|
import 'package:surface/types/chat.dart';
|
||||||
|
import 'package:surface/types/websocket.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
class ChatMessageController extends ChangeNotifier {
|
class ChatMessageController extends ChangeNotifier {
|
||||||
@@ -36,8 +38,7 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
|
|
||||||
int? messageTotal;
|
int? messageTotal;
|
||||||
|
|
||||||
bool get isAllLoaded =>
|
bool get isAllLoaded => messageTotal != null && messages.length >= messageTotal!;
|
||||||
messageTotal != null && messages.length >= messageTotal!;
|
|
||||||
|
|
||||||
String? _boxKey;
|
String? _boxKey;
|
||||||
SnChannel? channel;
|
SnChannel? channel;
|
||||||
@@ -50,8 +51,10 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
/// Stored as a list of nonce to provide the loading state
|
/// Stored as a list of nonce to provide the loading state
|
||||||
final List<String> unconfirmedMessages = List.empty(growable: true);
|
final List<String> unconfirmedMessages = List.empty(growable: true);
|
||||||
|
|
||||||
Box<SnChatMessage>? get _box =>
|
Box<SnChatMessage>? get _box => (_boxKey == null || isPending) ? null : Hive.box<SnChatMessage>(_boxKey!);
|
||||||
(_boxKey == null || isPending) ? null : Hive.box<SnChatMessage>(_boxKey!);
|
|
||||||
|
final List<SnChannelMember> typingMembers = List.empty(growable: true);
|
||||||
|
final Map<int, Timer> typingInactiveTimer = {};
|
||||||
|
|
||||||
Future<void> initialize(SnChannel chan) async {
|
Future<void> initialize(SnChannel chan) async {
|
||||||
channel = chan;
|
channel = chan;
|
||||||
@@ -71,6 +74,7 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
_wsSubscription = _ws.stream.stream.listen((event) {
|
_wsSubscription = _ws.stream.stream.listen((event) {
|
||||||
switch (event.method) {
|
switch (event.method) {
|
||||||
case 'events.new':
|
case 'events.new':
|
||||||
|
if (event.payload?['channel_id'] != channel?.id) break;
|
||||||
final payload = SnChatMessage.fromJson(event.payload!);
|
final payload = SnChatMessage.fromJson(event.payload!);
|
||||||
_addMessage(payload);
|
_addMessage(payload);
|
||||||
break;
|
break;
|
||||||
@@ -78,22 +82,16 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
if (event.payload?['channel_id'] != channel?.id) break;
|
if (event.payload?['channel_id'] != channel?.id) break;
|
||||||
final member = SnChannelMember.fromJson(event.payload!['member']);
|
final member = SnChannelMember.fromJson(event.payload!['member']);
|
||||||
if (member.id == profile?.id) break;
|
if (member.id == profile?.id) break;
|
||||||
// TODO impl typing users
|
if (!typingMembers.any((x) => x.id == member.id)) {
|
||||||
// if (!_typingUsers.any((x) => x.id == member.id)) {
|
typingMembers.add(member);
|
||||||
// setState(() {
|
notifyListeners();
|
||||||
// _typingUsers.add(member);
|
}
|
||||||
// });
|
typingInactiveTimer[member.id]?.cancel();
|
||||||
// }
|
typingInactiveTimer[member.id] = Timer(const Duration(seconds: 3), () {
|
||||||
// _typingInactiveTimer[member.id]?.cancel();
|
typingMembers.removeWhere((x) => x.id == member.id);
|
||||||
// _typingInactiveTimer[member.id] = Timer(
|
typingInactiveTimer.remove(member.id);
|
||||||
// const Duration(seconds: 3),
|
notifyListeners();
|
||||||
// () {
|
});
|
||||||
// setState(() {
|
|
||||||
// _typingUsers.removeWhere((x) => x.id == member.id);
|
|
||||||
// _typingInactiveTimer.remove(member.id);
|
|
||||||
// });
|
|
||||||
// },
|
|
||||||
// );
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -101,6 +99,35 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Timer? _typingNotifyTimer;
|
||||||
|
bool _typingStatus = false;
|
||||||
|
|
||||||
|
Future<void> _sendTypingStatusPackage() async {
|
||||||
|
_ws.conn?.sink.add(jsonEncode(
|
||||||
|
WebSocketPackage(
|
||||||
|
method: 'status.typing',
|
||||||
|
endpoint: 'im',
|
||||||
|
payload: {
|
||||||
|
'channel_id': channel!.id,
|
||||||
|
},
|
||||||
|
).toJson(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
void pingTypingStatus() {
|
||||||
|
if (!_typingStatus) {
|
||||||
|
_sendTypingStatusPackage();
|
||||||
|
_typingStatus = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_typingNotifyTimer == null || !_typingNotifyTimer!.isActive) {
|
||||||
|
_typingNotifyTimer?.cancel();
|
||||||
|
_typingNotifyTimer = Timer(const Duration(milliseconds: 1850), () {
|
||||||
|
_typingStatus = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _saveMessageToLocal(Iterable<SnChatMessage> messages) async {
|
Future<void> _saveMessageToLocal(Iterable<SnChatMessage> messages) async {
|
||||||
if (_box == null) return;
|
if (_box == null) return;
|
||||||
await _box!.putAll({
|
await _box!.putAll({
|
||||||
@@ -167,8 +194,7 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case 'messages.edit':
|
case 'messages.edit':
|
||||||
if (message.relatedEventId != null) {
|
if (message.relatedEventId != null) {
|
||||||
final idx =
|
final idx = messages.indexWhere((x) => x.id == message.relatedEventId);
|
||||||
messages.indexWhere((x) => x.id == message.relatedEventId);
|
|
||||||
if (idx != -1) {
|
if (idx != -1) {
|
||||||
final newBody = message.body;
|
final newBody = message.body;
|
||||||
newBody.remove('related_event');
|
newBody.remove('related_event');
|
||||||
@@ -207,8 +233,7 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
'algorithm': 'plain',
|
'algorithm': 'plain',
|
||||||
if (quoteId != null) 'quote_event': quoteId,
|
if (quoteId != null) 'quote_event': quoteId,
|
||||||
if (relatedId != null) 'related_event': relatedId,
|
if (relatedId != null) 'related_event': relatedId,
|
||||||
if (attachments != null && attachments.isNotEmpty)
|
if (attachments != null && attachments.isNotEmpty) 'attachments': attachments,
|
||||||
'attachments': attachments,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mock the message locally
|
// Mock the message locally
|
||||||
@@ -305,8 +330,7 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
|
|
||||||
if (out == null) {
|
if (out == null) {
|
||||||
try {
|
try {
|
||||||
final resp = await _sn.client
|
final resp = await _sn.client.get('/cgi/im/channels/${channel!.keyPath}/events/$id');
|
||||||
.get('/cgi/im/channels/${channel!.keyPath}/events/$id');
|
|
||||||
out = SnChatMessage.fromJson(resp.data);
|
out = SnChatMessage.fromJson(resp.data);
|
||||||
_saveMessageToLocal([out]);
|
_saveMessageToLocal([out]);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
@@ -341,9 +365,7 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
bool forceRemote = false,
|
bool forceRemote = false,
|
||||||
}) async {
|
}) async {
|
||||||
late List<SnChatMessage> out;
|
late List<SnChatMessage> out;
|
||||||
if (_box != null &&
|
if (_box != null && (_box!.length >= take + offset || forceLocal) && !forceRemote) {
|
||||||
(_box!.length >= take + offset || forceLocal) &&
|
|
||||||
!forceRemote) {
|
|
||||||
out = _box!.keys
|
out = _box!.keys
|
||||||
.toList()
|
.toList()
|
||||||
.cast<int>()
|
.cast<int>()
|
||||||
@@ -386,8 +408,7 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
quoteEvent: quoteEvent,
|
quoteEvent: quoteEvent,
|
||||||
attachments: attachments
|
attachments: attachments
|
||||||
.where(
|
.where(
|
||||||
(ele) =>
|
(ele) => out[i].body['attachments']?.contains(ele?.rid) ?? false,
|
||||||
out[i].body['attachments']?.contains(ele?.rid) ?? false,
|
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
),
|
),
|
||||||
@@ -395,10 +416,7 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Preload sender accounts
|
// Preload sender accounts
|
||||||
final accountId = out
|
final accountId = out.where((ele) => ele.sender.accountId >= 0).map((ele) => ele.sender.accountId).toSet();
|
||||||
.where((ele) => ele.sender.accountId >= 0)
|
|
||||||
.map((ele) => ele.sender.accountId)
|
|
||||||
.toSet();
|
|
||||||
await _ud.listAccount(accountId);
|
await _ud.listAccount(accountId);
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:developer';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
@@ -8,6 +11,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:mime/mime.dart';
|
import 'package:mime/mime.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:surface/providers/post.dart';
|
import 'package:surface/providers/post.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';
|
||||||
@@ -100,7 +104,7 @@ class PostWriteMedia {
|
|||||||
if (attachment != null) {
|
if (attachment != null) {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final ImageProvider provider = UniversalImage.provider(sn.getAttachmentUrl(attachment!.rid));
|
final ImageProvider provider = UniversalImage.provider(sn.getAttachmentUrl(attachment!.rid));
|
||||||
if (width != null && height != null) {
|
if (width != null && height != null && !kIsWeb) {
|
||||||
return ResizeImage(
|
return ResizeImage(
|
||||||
provider,
|
provider,
|
||||||
width: width,
|
width: width,
|
||||||
@@ -150,9 +154,22 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
final TextEditingController descriptionController = TextEditingController();
|
final TextEditingController descriptionController = TextEditingController();
|
||||||
final TextEditingController aliasController = TextEditingController();
|
final TextEditingController aliasController = TextEditingController();
|
||||||
|
|
||||||
PostWriteController() {
|
bool _temporarySaveActive = false;
|
||||||
titleController.addListener(() => notifyListeners());
|
|
||||||
descriptionController.addListener(() => notifyListeners());
|
PostWriteController({bool doLoadFromTemporary = true}) {
|
||||||
|
_temporarySaveActive = doLoadFromTemporary;
|
||||||
|
titleController.addListener(() {
|
||||||
|
_temporaryPlanSave();
|
||||||
|
notifyListeners();
|
||||||
|
});
|
||||||
|
descriptionController.addListener(() {
|
||||||
|
_temporaryPlanSave();
|
||||||
|
notifyListeners();
|
||||||
|
});
|
||||||
|
contentController.addListener(() {
|
||||||
|
_temporaryPlanSave();
|
||||||
|
});
|
||||||
|
if (doLoadFromTemporary) _temporaryLoad();
|
||||||
}
|
}
|
||||||
|
|
||||||
String mode = kTitleMap.keys.first;
|
String mode = kTitleMap.keys.first;
|
||||||
@@ -199,11 +216,11 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
aliasController.text = post.alias ?? '';
|
aliasController.text = post.alias ?? '';
|
||||||
publishedAt = post.publishedAt;
|
publishedAt = post.publishedAt;
|
||||||
publishedUntil = post.publishedUntil;
|
publishedUntil = post.publishedUntil;
|
||||||
visibleUsers = List.from(post.visibleUsersList ?? []);
|
visibleUsers = List.from(post.visibleUsersList ?? [], growable: true);
|
||||||
invisibleUsers = List.from(post.invisibleUsersList ?? []);
|
invisibleUsers = List.from(post.invisibleUsersList ?? [], growable: true);
|
||||||
visibility = post.visibility;
|
visibility = post.visibility;
|
||||||
tags = List.from(post.tags.map((ele) => ele.alias));
|
tags = List.from(post.tags.map((ele) => ele.alias), growable: true);
|
||||||
categories = List.from(post.categories.map((ele) => ele.alias));
|
categories = List.from(post.categories.map((ele) => ele.alias), growable: true);
|
||||||
attachments.addAll(post.preload?.attachments?.map((ele) => PostWriteMedia(ele)) ?? []);
|
attachments.addAll(post.preload?.attachments?.map((ele) => PostWriteMedia(ele)) ?? []);
|
||||||
|
|
||||||
if (post.preload?.thumbnail != null && (post.preload?.thumbnail?.rid.isNotEmpty ?? false)) {
|
if (post.preload?.thumbnail != null && (post.preload?.thumbnail?.rid.isNotEmpty ?? false)) {
|
||||||
@@ -231,7 +248,8 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<SnAttachment> _uploadAttachment(BuildContext context, PostWriteMedia media, {bool isCompressed = false}) async {
|
Future<SnAttachment> _uploadAttachment(BuildContext context, PostWriteMedia media,
|
||||||
|
{bool isCompressed = false}) async {
|
||||||
final attach = context.read<SnAttachmentProvider>();
|
final attach = context.read<SnAttachmentProvider>();
|
||||||
|
|
||||||
final place = await attach.chunkedUploadInitialize(
|
final place = await attach.chunkedUploadInitialize(
|
||||||
@@ -242,10 +260,11 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
mimetype: media.raw != null && media.type == SnMediaType.image ? 'image/png' : null,
|
mimetype: media.raw != null && media.type == SnMediaType.image ? 'image/png' : null,
|
||||||
);
|
);
|
||||||
|
|
||||||
final item = await attach.chunkedUploadParts(
|
var item = await attach.chunkedUploadParts(
|
||||||
media.toFile()!,
|
media.toFile()!,
|
||||||
place.$1,
|
place.$1,
|
||||||
place.$2,
|
place.$2,
|
||||||
|
analyzeNow: media.type == SnMediaType.image,
|
||||||
onProgress: (value) {
|
onProgress: (value) {
|
||||||
progress = value;
|
progress = value;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -253,9 +272,13 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (media.type == SnMediaType.video && !isCompressed && context.mounted) {
|
if (media.type == SnMediaType.video && !isCompressed && context.mounted) {
|
||||||
final compressedAttachment = await _tryCompressVideoCopy(context, media);
|
try {
|
||||||
if (compressedAttachment != null) {
|
final compressedAttachment = await _tryCompressVideoCopy(context, media);
|
||||||
await attach.updateOne(item, compressedId: compressedAttachment.id);
|
if (compressedAttachment != null) {
|
||||||
|
item = await attach.updateOne(item, compressedId: compressedAttachment.id);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (context.mounted) context.showErrorDialog(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,6 +315,83 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
return compressedAttachment;
|
return compressedAttachment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static const kTemporaryStorageKey = 'int_draft_post';
|
||||||
|
|
||||||
|
Timer? _temporarySaveTimer;
|
||||||
|
|
||||||
|
void _temporaryPlanSave() {
|
||||||
|
if (!_temporarySaveActive) return;
|
||||||
|
_temporarySaveTimer?.cancel();
|
||||||
|
_temporarySaveTimer = Timer(const Duration(seconds: 1), () {
|
||||||
|
_temporarySave();
|
||||||
|
log("[PostWriter] Temporary save saved.");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _temporarySave() {
|
||||||
|
SharedPreferences.getInstance().then((prefs) {
|
||||||
|
if (titleController.text.isEmpty &&
|
||||||
|
descriptionController.text.isEmpty &&
|
||||||
|
contentController.text.isEmpty &&
|
||||||
|
thumbnail == null &&
|
||||||
|
attachments.isEmpty) {
|
||||||
|
prefs.remove(kTemporaryStorageKey);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
prefs.setString(
|
||||||
|
kTemporaryStorageKey,
|
||||||
|
jsonEncode({
|
||||||
|
'publisher': publisher,
|
||||||
|
'content': contentController.text,
|
||||||
|
if (aliasController.text.isNotEmpty) 'alias': aliasController.text,
|
||||||
|
if (titleController.text.isNotEmpty) 'title': titleController.text,
|
||||||
|
if (descriptionController.text.isNotEmpty) 'description': descriptionController.text,
|
||||||
|
if (thumbnail != null && thumbnail!.attachment != null) 'thumbnail': thumbnail!.attachment!.toJson(),
|
||||||
|
'attachments':
|
||||||
|
attachments.where((e) => e.attachment != null).map((e) => e.attachment!.toJson()).toList(growable: true),
|
||||||
|
'tags': tags.map((ele) => {'alias': ele}).toList(growable: true),
|
||||||
|
'categories': categories.map((ele) => {'alias': ele}).toList(growable: true),
|
||||||
|
'visibility': visibility,
|
||||||
|
'visible_users_list': visibleUsers,
|
||||||
|
'invisible_users_list': invisibleUsers,
|
||||||
|
if (publishedAt != null) 'published_at': publishedAt!.toUtc().toIso8601String(),
|
||||||
|
if (publishedUntil != null) 'published_until': publishedAt!.toUtc().toIso8601String(),
|
||||||
|
if (replyingPost != null) 'reply_to': replyingPost!.toJson(),
|
||||||
|
if (repostingPost != null) 'repost_to': repostingPost!.toJson(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool temporaryRestored = false;
|
||||||
|
|
||||||
|
void _temporaryLoad() {
|
||||||
|
SharedPreferences.getInstance().then((prefs) {
|
||||||
|
final raw = prefs.getString(kTemporaryStorageKey);
|
||||||
|
if (raw == null) return;
|
||||||
|
final data = jsonDecode(raw);
|
||||||
|
contentController.text = data['content'];
|
||||||
|
aliasController.text = data['alias'] ?? '';
|
||||||
|
titleController.text = data['title'] ?? '';
|
||||||
|
descriptionController.text = data['description'] ?? '';
|
||||||
|
if (data['thumbnail'] != null) thumbnail = PostWriteMedia(SnAttachment.fromJson(data['thumbnail']));
|
||||||
|
attachments
|
||||||
|
.addAll(data['attachments'].map((ele) => PostWriteMedia(SnAttachment.fromJson(ele))).cast<PostWriteMedia>());
|
||||||
|
tags = List.from(data['tags'].map((ele) => ele['alias']));
|
||||||
|
categories = List.from(data['categories'].map((ele) => ele['alias']));
|
||||||
|
visibility = data['visibility'];
|
||||||
|
visibleUsers = List.from(data['visible_users_list'] ?? []);
|
||||||
|
invisibleUsers = List.from(data['invisible_users_list'] ?? []);
|
||||||
|
if (data['published_at'] != null) publishedAt = DateTime.tryParse(data['published_at'])?.toLocal();
|
||||||
|
if (data['published_until'] != null) publishedUntil = DateTime.tryParse(data['published_until'])?.toLocal();
|
||||||
|
replyingPost = data['reply_to'] != null ? SnPost.fromJson(data['reply_to']) : null;
|
||||||
|
repostingPost = data['repost_to'] != null ? SnPost.fromJson(data['repost_to']) : null;
|
||||||
|
temporaryRestored = true;
|
||||||
|
notifyListeners();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> uploadSingleAttachment(BuildContext context, int idx) async {
|
Future<void> uploadSingleAttachment(BuildContext context, int idx) async {
|
||||||
if (isBusy) return;
|
if (isBusy) return;
|
||||||
|
|
||||||
@@ -336,7 +436,7 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
mimetype: media.raw != null && media.type == SnMediaType.image ? 'image/png' : null,
|
mimetype: media.raw != null && media.type == SnMediaType.image ? 'image/png' : null,
|
||||||
);
|
);
|
||||||
|
|
||||||
final item = await attach.chunkedUploadParts(
|
var item = await attach.chunkedUploadParts(
|
||||||
media.toFile()!,
|
media.toFile()!,
|
||||||
place.$1,
|
place.$1,
|
||||||
place.$2,
|
place.$2,
|
||||||
@@ -347,11 +447,15 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (media.type == SnMediaType.video && context.mounted) {
|
try {
|
||||||
final compressedAttachment = await _tryCompressVideoCopy(context, media);
|
if (context.mounted) {
|
||||||
if (compressedAttachment != null) {
|
final compressedAttachment = await _tryCompressVideoCopy(context, media);
|
||||||
await attach.updateOne(item, compressedId: compressedAttachment.id);
|
if (compressedAttachment != null) {
|
||||||
|
item = await attach.updateOne(item, compressedId: compressedAttachment.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (context.mounted) context.showErrorDialog(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
progress = (i + 1) / attachments.length * kAttachmentProgressWeight;
|
progress = (i + 1) / attachments.length * kAttachmentProgressWeight;
|
||||||
@@ -407,6 +511,7 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
method: editingPost != null ? 'PUT' : 'POST',
|
method: editingPost != null ? 'PUT' : 'POST',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
reset();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
context.showErrorDialog(err);
|
context.showErrorDialog(err);
|
||||||
@@ -455,73 +560,88 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
|
|
||||||
void setPublisher(SnPublisher? item) {
|
void setPublisher(SnPublisher? item) {
|
||||||
publisher = item;
|
publisher = item;
|
||||||
|
_temporaryPlanSave();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void setPublishedAt(DateTime? value) {
|
void setPublishedAt(DateTime? value) {
|
||||||
publishedAt = value;
|
publishedAt = value;
|
||||||
|
_temporaryPlanSave();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void setPublishedUntil(DateTime? value) {
|
void setPublishedUntil(DateTime? value) {
|
||||||
publishedUntil = value;
|
publishedUntil = value;
|
||||||
|
_temporaryPlanSave();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void setTags(List<String> value) {
|
void setTags(List<String> value) {
|
||||||
tags = value;
|
tags = value;
|
||||||
|
_temporaryPlanSave();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void setCategories(List<String> value) {
|
void setCategories(List<String> value) {
|
||||||
categories = value;
|
categories = value;
|
||||||
|
_temporaryPlanSave();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void setVisibility(int value) {
|
void setVisibility(int value) {
|
||||||
visibility = value;
|
visibility = value;
|
||||||
|
_temporaryPlanSave();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void setVisibleUsers(List<int> value) {
|
void setVisibleUsers(List<int> value) {
|
||||||
visibleUsers = value;
|
visibleUsers = value;
|
||||||
|
_temporaryPlanSave();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void setInvisibleUsers(List<int> value) {
|
void setInvisibleUsers(List<int> value) {
|
||||||
invisibleUsers = value;
|
invisibleUsers = value;
|
||||||
|
_temporaryPlanSave();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void setProgress(double? value) {
|
void setProgress(double? value) {
|
||||||
progress = value;
|
progress = value;
|
||||||
|
_temporaryPlanSave();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void setIsBusy(bool value) {
|
void setIsBusy(bool value) {
|
||||||
isBusy = value;
|
isBusy = value;
|
||||||
|
_temporaryPlanSave();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void setMode(String value) {
|
void setMode(String value) {
|
||||||
mode = value;
|
mode = value;
|
||||||
|
_temporaryPlanSave();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void reset() {
|
void reset() {
|
||||||
publishedAt = null;
|
publishedAt = null;
|
||||||
publishedUntil = null;
|
publishedUntil = null;
|
||||||
|
thumbnail = null;
|
||||||
|
visibility = 0;
|
||||||
titleController.clear();
|
titleController.clear();
|
||||||
descriptionController.clear();
|
descriptionController.clear();
|
||||||
contentController.clear();
|
contentController.clear();
|
||||||
aliasController.clear();
|
aliasController.clear();
|
||||||
tags.clear();
|
tags = List.empty(growable: true);
|
||||||
categories.clear();
|
categories = List.empty(growable: true);
|
||||||
attachments.clear();
|
attachments = List.empty(growable: true);
|
||||||
editingPost = null;
|
editingPost = null;
|
||||||
replyingPost = null;
|
replyingPost = null;
|
||||||
repostingPost = null;
|
repostingPost = null;
|
||||||
mode = kTitleMap.keys.first;
|
mode = kTitleMap.keys.first;
|
||||||
|
temporaryRestored = false;
|
||||||
|
SharedPreferences.getInstance().then((prefs) => prefs.remove(kTemporaryStorageKey));
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import 'package:easy_localization_loader/easy_localization_loader.dart';
|
|||||||
import 'package:firebase_core/firebase_core.dart';
|
import 'package:firebase_core/firebase_core.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:gap/gap.dart';
|
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
@@ -18,7 +17,6 @@ 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:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
|
||||||
import 'package:surface/firebase_options.dart';
|
import 'package:surface/firebase_options.dart';
|
||||||
import 'package:surface/providers/channel.dart';
|
import 'package:surface/providers/channel.dart';
|
||||||
import 'package:surface/providers/chat_call.dart';
|
import 'package:surface/providers/chat_call.dart';
|
||||||
@@ -30,6 +28,7 @@ import 'package:surface/providers/post.dart';
|
|||||||
import 'package:surface/providers/relationship.dart';
|
import 'package:surface/providers/relationship.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/sn_sticker.dart';
|
||||||
import 'package:surface/providers/special_day.dart';
|
import 'package:surface/providers/special_day.dart';
|
||||||
import 'package:surface/providers/theme.dart';
|
import 'package:surface/providers/theme.dart';
|
||||||
import 'package:surface/providers/user_directory.dart';
|
import 'package:surface/providers/user_directory.dart';
|
||||||
@@ -41,7 +40,6 @@ import 'package:surface/types/chat.dart';
|
|||||||
import 'package:surface/types/realm.dart';
|
import 'package:surface/types/realm.dart';
|
||||||
import 'package:flutter_web_plugins/url_strategy.dart' show usePathUrlStrategy;
|
import 'package:flutter_web_plugins/url_strategy.dart' show usePathUrlStrategy;
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/version_label.dart';
|
|
||||||
import 'package:version/version.dart';
|
import 'package:version/version.dart';
|
||||||
import 'package:workmanager/workmanager.dart';
|
import 'package:workmanager/workmanager.dart';
|
||||||
import 'package:in_app_review/in_app_review.dart';
|
import 'package:in_app_review/in_app_review.dart';
|
||||||
@@ -144,6 +142,7 @@ class SolianApp extends StatelessWidget {
|
|||||||
Provider(create: (ctx) => SnPostContentProvider(ctx)),
|
Provider(create: (ctx) => SnPostContentProvider(ctx)),
|
||||||
Provider(create: (ctx) => SnRelationshipProvider(ctx)),
|
Provider(create: (ctx) => SnRelationshipProvider(ctx)),
|
||||||
Provider(create: (ctx) => SnLinkPreviewProvider(ctx)),
|
Provider(create: (ctx) => SnLinkPreviewProvider(ctx)),
|
||||||
|
Provider(create: (ctx) => SnStickerProvider(ctx)),
|
||||||
ChangeNotifierProvider(create: (ctx) => UserProvider(ctx)),
|
ChangeNotifierProvider(create: (ctx) => UserProvider(ctx)),
|
||||||
ChangeNotifierProvider(create: (ctx) => WebSocketProvider(ctx)),
|
ChangeNotifierProvider(create: (ctx) => WebSocketProvider(ctx)),
|
||||||
ChangeNotifierProvider(create: (ctx) => NotificationProvider(ctx)),
|
ChangeNotifierProvider(create: (ctx) => NotificationProvider(ctx)),
|
||||||
@@ -208,8 +207,6 @@ class _AppSplashScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _AppSplashScreenState extends State<_AppSplashScreen> {
|
class _AppSplashScreenState extends State<_AppSplashScreen> {
|
||||||
bool _isReady = false;
|
|
||||||
|
|
||||||
void _tryRequestRating() async {
|
void _tryRequestRating() async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
if (prefs.containsKey('first_boot_time')) {
|
if (prefs.containsKey('first_boot_time')) {
|
||||||
@@ -261,6 +258,10 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
|
|||||||
|
|
||||||
Future<void> _initialize() async {
|
Future<void> _initialize() async {
|
||||||
try {
|
try {
|
||||||
|
final cfg = context.read<ConfigProvider>();
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
cfg.calcDrawerSize(context);
|
||||||
|
});
|
||||||
final home = context.read<HomeWidgetProvider>();
|
final home = context.read<HomeWidgetProvider>();
|
||||||
await home.initialize();
|
await home.initialize();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@@ -278,12 +279,11 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
|
|||||||
await ws.tryConnect();
|
await ws.tryConnect();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final notify = context.read<NotificationProvider>();
|
final notify = context.read<NotificationProvider>();
|
||||||
|
notify.listen();
|
||||||
await notify.registerPushNotifications();
|
await notify.registerPushNotifications();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
await context.showErrorDialog(err);
|
await context.showErrorDialog(err);
|
||||||
} finally {
|
|
||||||
setState(() => _isReady = true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,32 +303,17 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (!_isReady) {
|
final cfg = context.read<ConfigProvider>();
|
||||||
return Scaffold(
|
return NotificationListener<SizeChangedLayoutNotification>(
|
||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
onNotification: (notification) {
|
||||||
body: Container(
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
constraints: const BoxConstraints(maxWidth: 180),
|
cfg.calcDrawerSize(context);
|
||||||
child: Column(
|
});
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
return false;
|
||||||
mainAxisSize: MainAxisSize.min,
|
},
|
||||||
children: [
|
child: SizeChangedLayoutNotifier(
|
||||||
if (MediaQuery.of(context).platformBrightness == Brightness.dark)
|
child: widget.child,
|
||||||
Image.asset("assets/icon/icon-dark.png", width: 64, height: 64)
|
),
|
||||||
else
|
);
|
||||||
Image.asset("assets/icon/icon.png", width: 64, height: 64),
|
|
||||||
const Gap(6),
|
|
||||||
LinearProgressIndicator(
|
|
||||||
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
|
|
||||||
),
|
|
||||||
const Gap(20),
|
|
||||||
Text('appInitializing'.tr(), textAlign: TextAlign.center),
|
|
||||||
AppVersionLabel(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
).center(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return widget.child;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:responsive_framework/responsive_framework.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:surface/providers/widget.dart';
|
import 'package:surface/providers/widget.dart';
|
||||||
|
|
||||||
@@ -12,6 +13,10 @@ const kNetworkServerStoreKey = 'app_server_url';
|
|||||||
const kAppbarTransparentStoreKey = 'app_bar_transparent';
|
const kAppbarTransparentStoreKey = 'app_bar_transparent';
|
||||||
const kAppBackgroundStoreKey = 'app_has_background';
|
const kAppBackgroundStoreKey = 'app_has_background';
|
||||||
const kAppColorSchemeStoreKey = 'app_color_scheme';
|
const kAppColorSchemeStoreKey = 'app_color_scheme';
|
||||||
|
const kAppDrawerPreferCollapse = 'app_drawer_prefer_collapse';
|
||||||
|
const kAppNotifyWithHaptic = 'app_notify_with_haptic';
|
||||||
|
const kAppExpandPostLink = 'app_expand_post_link';
|
||||||
|
const kAppExpandChatLink = 'app_expand_chat_link';
|
||||||
|
|
||||||
const Map<String, FilterQuality> kImageQualityLevel = {
|
const Map<String, FilterQuality> kImageQualityLevel = {
|
||||||
'settingsImageQualityLowest': FilterQuality.none,
|
'settingsImageQualityLowest': FilterQuality.none,
|
||||||
@@ -33,6 +38,32 @@ class ConfigProvider extends ChangeNotifier {
|
|||||||
prefs = await SharedPreferences.getInstance();
|
prefs = await SharedPreferences.getInstance();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool drawerIsCollapsed = false;
|
||||||
|
bool drawerIsExpanded = false;
|
||||||
|
|
||||||
|
void calcDrawerSize(BuildContext context, {bool withMediaQuery = false}) {
|
||||||
|
bool newDrawerIsCollapsed = false;
|
||||||
|
bool newDrawerIsExpanded = false;
|
||||||
|
if (withMediaQuery) {
|
||||||
|
newDrawerIsCollapsed = MediaQuery.of(context).size.width < 450;
|
||||||
|
newDrawerIsExpanded = MediaQuery.of(context).size.width >= 451;
|
||||||
|
} else {
|
||||||
|
final rpb = ResponsiveBreakpoints.of(context);
|
||||||
|
newDrawerIsCollapsed = rpb.smallerOrEqualTo(MOBILE);
|
||||||
|
newDrawerIsExpanded = rpb.largerThan(TABLET)
|
||||||
|
? (prefs.getBool(kAppDrawerPreferCollapse) ?? false)
|
||||||
|
? false
|
||||||
|
: true
|
||||||
|
: false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newDrawerIsExpanded != drawerIsExpanded || newDrawerIsCollapsed != drawerIsCollapsed) {
|
||||||
|
drawerIsExpanded = newDrawerIsExpanded;
|
||||||
|
drawerIsCollapsed = newDrawerIsCollapsed;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
FilterQuality get imageQuality {
|
FilterQuality get imageQuality {
|
||||||
return kImageQualityLevel.values.elementAtOrNull(prefs.getInt('app_image_quality') ?? 3) ?? FilterQuality.high;
|
return kImageQualityLevel.values.elementAtOrNull(prefs.getInt('app_image_quality') ?? 3) ?? FilterQuality.high;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,6 +58,11 @@ class NavigationProvider extends ChangeNotifier {
|
|||||||
screen: 'realm',
|
screen: 'realm',
|
||||||
label: 'screenRealm',
|
label: 'screenRealm',
|
||||||
),
|
),
|
||||||
|
AppNavDestination(
|
||||||
|
icon: Icon(Symbols.newspaper, weight: 400, opticalSize: 20),
|
||||||
|
screen: 'news',
|
||||||
|
label: 'screenNews',
|
||||||
|
),
|
||||||
AppNavDestination(
|
AppNavDestination(
|
||||||
icon: Icon(Symbols.photo_library, weight: 400, opticalSize: 20),
|
icon: Icon(Symbols.photo_library, weight: 400, opticalSize: 20),
|
||||||
screen: 'album',
|
screen: 'album',
|
||||||
@@ -83,8 +88,7 @@ class NavigationProvider extends ChangeNotifier {
|
|||||||
|
|
||||||
List<AppNavDestination> destinations = [];
|
List<AppNavDestination> destinations = [];
|
||||||
|
|
||||||
int get pinnedDestinationCount =>
|
int get pinnedDestinationCount => destinations.where((ele) => ele.isPinned).length;
|
||||||
destinations.where((ele) => ele.isPinned).length;
|
|
||||||
|
|
||||||
NavigationProvider() {
|
NavigationProvider() {
|
||||||
buildDestinations(kDefaultPinnedDestination);
|
buildDestinations(kDefaultPinnedDestination);
|
||||||
@@ -113,17 +117,13 @@ class NavigationProvider extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool isIndexInRange(int min, int max) {
|
bool isIndexInRange(int min, int max) {
|
||||||
return _currentIndex != null &&
|
return _currentIndex != null && _currentIndex! >= min && _currentIndex! < max;
|
||||||
_currentIndex! >= min &&
|
|
||||||
_currentIndex! < max;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void autoDetectIndex(GoRouter? state) {
|
void autoDetectIndex(GoRouter? state) {
|
||||||
if (state == null) return;
|
if (state == null) return;
|
||||||
final idx = destinations.indexWhere(
|
final idx = destinations.indexWhere(
|
||||||
(ele) =>
|
(ele) => ele.screen == state.routerDelegate.currentConfiguration.last.route.name,
|
||||||
ele.screen ==
|
|
||||||
state.routerDelegate.currentConfiguration.last.route.name,
|
|
||||||
);
|
);
|
||||||
_currentIndex = idx == -1 ? null : idx;
|
_currentIndex = idx == -1 ? null : idx;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|||||||
@@ -4,18 +4,26 @@ import 'dart:io';
|
|||||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_udid/flutter_udid.dart';
|
import 'package:flutter_udid/flutter_udid.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:surface/providers/config.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
import 'package:surface/providers/userinfo.dart';
|
import 'package:surface/providers/userinfo.dart';
|
||||||
|
import 'package:surface/providers/websocket.dart';
|
||||||
|
import 'package:surface/types/notification.dart';
|
||||||
|
|
||||||
class NotificationProvider extends ChangeNotifier {
|
class NotificationProvider extends ChangeNotifier {
|
||||||
late final SnNetworkProvider _sn;
|
late final SnNetworkProvider _sn;
|
||||||
late final UserProvider _ua;
|
late final UserProvider _ua;
|
||||||
|
late final WebSocketProvider _ws;
|
||||||
|
late final ConfigProvider _cfg;
|
||||||
|
|
||||||
NotificationProvider(BuildContext context) {
|
NotificationProvider(BuildContext context) {
|
||||||
_sn = context.read<SnNetworkProvider>();
|
_sn = context.read<SnNetworkProvider>();
|
||||||
_ua = context.read<UserProvider>();
|
_ua = context.read<UserProvider>();
|
||||||
|
_ws = context.read<WebSocketProvider>();
|
||||||
|
_cfg = context.read<ConfigProvider>();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> registerPushNotifications() async {
|
Future<void> registerPushNotifications() async {
|
||||||
@@ -62,4 +70,23 @@ class NotificationProvider extends ChangeNotifier {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<SnNotification> notifications = List.empty(growable: true);
|
||||||
|
|
||||||
|
void listen() {
|
||||||
|
_ws.stream.stream.listen((event) {
|
||||||
|
if (event.method == 'notifications.new') {
|
||||||
|
final notification = SnNotification.fromJson(event.payload!);
|
||||||
|
notifications.add(notification);
|
||||||
|
notifyListeners();
|
||||||
|
final doHaptic = _cfg.prefs.getBool(kAppNotifyWithHaptic) ?? true;
|
||||||
|
if (doHaptic) HapticFeedback.lightImpact();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear() {
|
||||||
|
notifications.clear();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ class SnAttachmentProvider {
|
|||||||
Map<String, dynamic>? metadata, {
|
Map<String, dynamic>? metadata, {
|
||||||
String? mimetype,
|
String? mimetype,
|
||||||
Function(double progress)? onProgress,
|
Function(double progress)? onProgress,
|
||||||
|
bool analyzeNow = false,
|
||||||
}) async {
|
}) async {
|
||||||
final filePayload = MultipartFile.fromBytes(data, filename: filename);
|
final filePayload = MultipartFile.fromBytes(data, filename: filename);
|
||||||
final fileAlt = filename.contains('.') ? filename.substring(0, filename.lastIndexOf('.')) : filename;
|
final fileAlt = filename.contains('.') ? filename.substring(0, filename.lastIndexOf('.')) : filename;
|
||||||
@@ -108,6 +109,7 @@ class SnAttachmentProvider {
|
|||||||
final resp = await _sn.client.post(
|
final resp = await _sn.client.post(
|
||||||
'/cgi/uc/attachments',
|
'/cgi/uc/attachments',
|
||||||
data: formData,
|
data: formData,
|
||||||
|
queryParameters: {'analyzeNow': analyzeNow},
|
||||||
onSendProgress: (count, total) {
|
onSendProgress: (count, total) {
|
||||||
if (onProgress != null) {
|
if (onProgress != null) {
|
||||||
onProgress(count / total);
|
onProgress(count / total);
|
||||||
@@ -152,10 +154,12 @@ class SnAttachmentProvider {
|
|||||||
String rid,
|
String rid,
|
||||||
String cid, {
|
String cid, {
|
||||||
Function(double progress)? onProgress,
|
Function(double progress)? onProgress,
|
||||||
|
bool analyzeNow = false,
|
||||||
}) async {
|
}) async {
|
||||||
final resp = await _sn.client.post(
|
final resp = await _sn.client.post(
|
||||||
'/cgi/uc/fragments/$rid/$cid',
|
'/cgi/uc/fragments/$rid/$cid',
|
||||||
data: data,
|
data: data,
|
||||||
|
queryParameters: {'analyzeNow': analyzeNow},
|
||||||
options: Options(headers: {'Content-Type': 'application/octet-stream'}),
|
options: Options(headers: {'Content-Type': 'application/octet-stream'}),
|
||||||
onSendProgress: (count, total) {
|
onSendProgress: (count, total) {
|
||||||
if (onProgress != null) {
|
if (onProgress != null) {
|
||||||
@@ -176,6 +180,7 @@ class SnAttachmentProvider {
|
|||||||
SnAttachmentFragment place,
|
SnAttachmentFragment place,
|
||||||
int chunkSize, {
|
int chunkSize, {
|
||||||
Function(double progress)? onProgress,
|
Function(double progress)? onProgress,
|
||||||
|
bool analyzeNow = false,
|
||||||
}) async {
|
}) async {
|
||||||
final Map<String, dynamic> chunks = place.fileChunks;
|
final Map<String, dynamic> chunks = place.fileChunks;
|
||||||
var completedTasks = 0;
|
var completedTasks = 0;
|
||||||
@@ -198,6 +203,7 @@ class SnAttachmentProvider {
|
|||||||
data,
|
data,
|
||||||
place.rid,
|
place.rid,
|
||||||
entry.key,
|
entry.key,
|
||||||
|
analyzeNow: analyzeNow,
|
||||||
onProgress: (progress) {
|
onProgress: (progress) {
|
||||||
final overallProgress = (completedTasks + progress) / chunks.length;
|
final overallProgress = (completedTasks + progress) / chunks.length;
|
||||||
onProgress?.call(overallProgress);
|
onProgress?.call(overallProgress);
|
||||||
|
|||||||
38
lib/providers/sn_sticker.dart
Normal file
38
lib/providers/sn_sticker.dart
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import 'dart:developer';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:surface/providers/sn_network.dart';
|
||||||
|
import 'package:surface/types/attachment.dart';
|
||||||
|
|
||||||
|
class SnStickerProvider {
|
||||||
|
late final SnNetworkProvider _sn;
|
||||||
|
final Map<String, SnSticker?> _cache = {};
|
||||||
|
|
||||||
|
SnStickerProvider(BuildContext context) {
|
||||||
|
_sn = context.read<SnNetworkProvider>();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasNotSticker(String alias) {
|
||||||
|
return _cache.containsKey(alias) && _cache[alias] == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<SnSticker?> lookupSticker(String alias) async {
|
||||||
|
if (_cache.containsKey(alias)) {
|
||||||
|
return _cache[alias];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final resp = await _sn.client.get('/cgi/uc/stickers/lookup/$alias');
|
||||||
|
final sticker = SnSticker.fromJson(resp.data);
|
||||||
|
_cache[alias] = sticker;
|
||||||
|
|
||||||
|
return sticker;
|
||||||
|
} catch (err) {
|
||||||
|
_cache[alias] = null;
|
||||||
|
log('[Sticker] Failed to lookup sticker $alias: $err');
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,9 +3,12 @@ import 'package:provider/provider.dart';
|
|||||||
import 'package:surface/providers/userinfo.dart';
|
import 'package:surface/providers/userinfo.dart';
|
||||||
|
|
||||||
// Stored as key: month, day
|
// Stored as key: month, day
|
||||||
const Map<String, (int, int)> kSpecialDays = {
|
final Map<String, (int, int)> kSpecialDays = {
|
||||||
// Birthday is dynamically generated according to the user's profile
|
// Birthday is dynamically generated according to the user's profile
|
||||||
'NewYear': (1, 1),
|
'NewYear': (1, 1),
|
||||||
|
'LunarNewYear': (lunarToGregorian(null, 1, 1).month, lunarToGregorian(null, 1, 1).day),
|
||||||
|
'MidAutumn': (lunarToGregorian(null, 8, 15).month, lunarToGregorian(null, 8, 15).day),
|
||||||
|
'DragonBoat': (lunarToGregorian(null, 5, 5).month, lunarToGregorian(null, 5, 5).day),
|
||||||
'ValentineDay': (2, 14),
|
'ValentineDay': (2, 14),
|
||||||
'LaborDay': (5, 1),
|
'LaborDay': (5, 1),
|
||||||
'MotherDay': (5, 11),
|
'MotherDay': (5, 11),
|
||||||
@@ -19,6 +22,9 @@ const Map<String, (int, int)> kSpecialDays = {
|
|||||||
const Map<String, String> kSpecialDaysSymbol = {
|
const Map<String, String> kSpecialDaysSymbol = {
|
||||||
'Birthday': '🎂',
|
'Birthday': '🎂',
|
||||||
'NewYear': '🎉',
|
'NewYear': '🎉',
|
||||||
|
'LunarNewYear': '🎉',
|
||||||
|
'MidAutumn': '🥮',
|
||||||
|
'DragonBoat': '🐲',
|
||||||
'MerryXmas': '🎄',
|
'MerryXmas': '🎄',
|
||||||
'ValentineDay': '💑',
|
'ValentineDay': '💑',
|
||||||
'LaborDay': '🏋️',
|
'LaborDay': '🏋️',
|
||||||
@@ -134,3 +140,45 @@ class SpecialDayProvider {
|
|||||||
return (elapsedDuration / totalDuration).clamp(0.0, 1.0);
|
return (elapsedDuration / totalDuration).clamp(0.0, 1.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final Map<int, LunarYear> lunarYearData = {
|
||||||
|
2025: LunarYear(
|
||||||
|
startDate: DateTime(2025, 1, 29),
|
||||||
|
months: [29, 30, 30, 29, 30, 29, 29, 30, 30, 29, 30, 29],
|
||||||
|
leapMonth: 0,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
class LunarYear {
|
||||||
|
final DateTime startDate;
|
||||||
|
final List<int> months;
|
||||||
|
final int leapMonth;
|
||||||
|
|
||||||
|
LunarYear({required this.startDate, required this.months, required this.leapMonth});
|
||||||
|
}
|
||||||
|
|
||||||
|
DateTime lunarToGregorian(int? year, int month, int day, {bool isLeapMonth = false}) {
|
||||||
|
year = year ?? DateTime.now().year;
|
||||||
|
final lunarYear = lunarYearData[year];
|
||||||
|
if (lunarYear == null) {
|
||||||
|
throw Exception('Lunar data for year $year not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
int leapMonth = lunarYear.leapMonth;
|
||||||
|
if (isLeapMonth && (leapMonth == 0 || leapMonth != month)) {
|
||||||
|
throw Exception('Invalid leap month for year $year');
|
||||||
|
}
|
||||||
|
|
||||||
|
int daysFromStart = 0;
|
||||||
|
for (int i = 0; i < month - 1; i++) {
|
||||||
|
daysFromStart += lunarYear.months[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLeapMonth) {
|
||||||
|
daysFromStart += lunarYear.months[month - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
daysFromStart += day - 1;
|
||||||
|
|
||||||
|
return lunarYear.startDate.add(Duration(days: daysFromStart));
|
||||||
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class WebSocketProvider extends ChangeNotifier {
|
|||||||
|
|
||||||
Future<void> connect({noRetry = false}) async {
|
Future<void> connect({noRetry = false}) async {
|
||||||
if (!_ua.isAuthorized) return;
|
if (!_ua.isAuthorized) return;
|
||||||
if (isConnected) {
|
if (isConnected || conn != null) {
|
||||||
disconnect();
|
disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,7 +97,7 @@ class WebSocketProvider extends ChangeNotifier {
|
|||||||
onError: (err) {
|
onError: (err) {
|
||||||
isConnected = false;
|
isConnected = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
Future.delayed(const Duration(seconds: 11), () => connect());
|
Future.delayed(const Duration(seconds: 1), () => connect());
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
416
lib/router.dart
416
lib/router.dart
@@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:surface/screens/abuse_report.dart';
|
import 'package:surface/screens/abuse_report.dart';
|
||||||
import 'package:surface/screens/account.dart';
|
import 'package:surface/screens/account.dart';
|
||||||
import 'package:surface/screens/account/pfp.dart';
|
import 'package:surface/screens/account/profile_page.dart';
|
||||||
import 'package:surface/screens/account/profile_edit.dart';
|
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';
|
||||||
@@ -19,6 +19,8 @@ import 'package:surface/screens/chat/room.dart';
|
|||||||
import 'package:surface/screens/explore.dart';
|
import 'package:surface/screens/explore.dart';
|
||||||
import 'package:surface/screens/friend.dart';
|
import 'package:surface/screens/friend.dart';
|
||||||
import 'package:surface/screens/home.dart';
|
import 'package:surface/screens/home.dart';
|
||||||
|
import 'package:surface/screens/news/news_detail.dart';
|
||||||
|
import 'package:surface/screens/news/news_list.dart';
|
||||||
import 'package:surface/screens/notification.dart';
|
import 'package:surface/screens/notification.dart';
|
||||||
import 'package:surface/screens/post/post_detail.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';
|
||||||
@@ -31,258 +33,200 @@ import 'package:surface/screens/settings.dart';
|
|||||||
import 'package:surface/screens/sharing.dart';
|
import 'package:surface/screens/sharing.dart';
|
||||||
import 'package:surface/types/post.dart';
|
import 'package:surface/types/post.dart';
|
||||||
import 'package:surface/widgets/about.dart';
|
import 'package:surface/widgets/about.dart';
|
||||||
import 'package:surface/widgets/navigation/app_background.dart';
|
|
||||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
|
|
||||||
|
Widget _fadeThroughTransition(
|
||||||
|
BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
|
||||||
|
return FadeThroughTransition(
|
||||||
|
animation: animation,
|
||||||
|
secondaryAnimation: secondaryAnimation,
|
||||||
|
fillColor: Colors.transparent,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final _appRoutes = [
|
final _appRoutes = [
|
||||||
ShellRoute(
|
GoRoute(
|
||||||
builder: (context, state, child) => AppPageScaffold(
|
path: '/',
|
||||||
body: child,
|
name: 'home',
|
||||||
showAppBar: false,
|
builder: (context, state) => const HomeScreen(),
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/posts',
|
||||||
|
name: 'explore',
|
||||||
|
builder: (context, state) => const ExploreScreen(),
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/',
|
path: '/write/:mode',
|
||||||
name: 'home',
|
name: 'postEditor',
|
||||||
pageBuilder: (context, state) => NoTransitionPage(
|
builder: (context, state) => PostEditorScreen(
|
||||||
child: const HomeScreen(),
|
mode: state.pathParameters['mode']!,
|
||||||
|
postEditId: int.tryParse(
|
||||||
|
state.uri.queryParameters['editing'] ?? '',
|
||||||
|
),
|
||||||
|
postReplyId: int.tryParse(
|
||||||
|
state.uri.queryParameters['replying'] ?? '',
|
||||||
|
),
|
||||||
|
postRepostId: int.tryParse(
|
||||||
|
state.uri.queryParameters['reposting'] ?? '',
|
||||||
|
),
|
||||||
|
extraProps: state.extra as PostEditorExtraProps?,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/posts',
|
path: '/search',
|
||||||
name: 'explore',
|
name: 'postSearch',
|
||||||
pageBuilder: (context, state) => NoTransitionPage(
|
builder: (context, state) => PostSearchScreen(
|
||||||
child: const ExploreScreen(),
|
initialTags: state.uri.queryParameters['tags']?.split(','),
|
||||||
),
|
initialCategories: state.uri.queryParameters['categories']?.split(','),
|
||||||
routes: [
|
|
||||||
GoRoute(
|
|
||||||
path: '/write/:mode',
|
|
||||||
name: 'postEditor',
|
|
||||||
builder: (context, state) => AppBackground(
|
|
||||||
child: PostEditorScreen(
|
|
||||||
mode: state.pathParameters['mode']!,
|
|
||||||
postEditId: int.tryParse(
|
|
||||||
state.uri.queryParameters['editing'] ?? '',
|
|
||||||
),
|
|
||||||
postReplyId: int.tryParse(
|
|
||||||
state.uri.queryParameters['replying'] ?? '',
|
|
||||||
),
|
|
||||||
postRepostId: int.tryParse(
|
|
||||||
state.uri.queryParameters['reposting'] ?? '',
|
|
||||||
),
|
|
||||||
extraProps: state.extra as PostEditorExtraProps?,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: '/search',
|
|
||||||
name: 'postSearch',
|
|
||||||
builder: (context, state) => AppBackground(
|
|
||||||
child: PostSearchScreen(
|
|
||||||
initialTags: state.uri.queryParameters['tags']?.split(','),
|
|
||||||
initialCategories: state.uri.queryParameters['categories']?.split(','),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: '/publishers/:name',
|
|
||||||
name: 'postPublisher',
|
|
||||||
builder: (context, state) => AppBackground(
|
|
||||||
child: PostPublisherScreen(name: state.pathParameters['name']!),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: '/:slug',
|
|
||||||
name: 'postDetail',
|
|
||||||
builder: (context, state) => AppBackground(
|
|
||||||
child: PostDetailScreen(
|
|
||||||
slug: state.pathParameters['slug']!,
|
|
||||||
preload: state.extra as SnPost?,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: '/account',
|
|
||||||
name: 'account',
|
|
||||||
pageBuilder: (context, state) => NoTransitionPage(
|
|
||||||
child: const AccountScreen(),
|
|
||||||
),
|
|
||||||
routes: [],
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: '/chat',
|
|
||||||
name: 'chat',
|
|
||||||
pageBuilder: (context, state) => NoTransitionPage(
|
|
||||||
child: const ChatScreen(),
|
|
||||||
),
|
|
||||||
routes: [
|
|
||||||
GoRoute(
|
|
||||||
path: '/:scope/:alias',
|
|
||||||
name: 'chatRoom',
|
|
||||||
builder: (context, state) => AppBackground(
|
|
||||||
child: ChatRoomScreen(
|
|
||||||
scope: state.pathParameters['scope']!,
|
|
||||||
alias: state.pathParameters['alias']!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: '/:scope/:alias/call',
|
|
||||||
name: 'chatCallRoom',
|
|
||||||
builder: (context, state) => AppBackground(
|
|
||||||
child: CallRoomScreen(
|
|
||||||
scope: state.pathParameters['scope']!,
|
|
||||||
alias: state.pathParameters['alias']!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: '/:scope/:alias/detail',
|
|
||||||
name: 'channelDetail',
|
|
||||||
builder: (context, state) => AppBackground(
|
|
||||||
child: ChannelDetailScreen(
|
|
||||||
scope: state.pathParameters['scope']!,
|
|
||||||
alias: state.pathParameters['alias']!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: '/manage',
|
|
||||||
name: 'chatManage',
|
|
||||||
pageBuilder: (context, state) => CustomTransitionPage(
|
|
||||||
child: ChatManageScreen(
|
|
||||||
editingChannelAlias: state.uri.queryParameters['editing'],
|
|
||||||
),
|
|
||||||
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
|
||||||
return FadeThroughTransition(
|
|
||||||
animation: animation,
|
|
||||||
secondaryAnimation: secondaryAnimation,
|
|
||||||
fillColor: Colors.transparent,
|
|
||||||
child: AppBackground(
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: '/:alias',
|
|
||||||
name: 'realmDetail',
|
|
||||||
builder: (context, state) => AppBackground(
|
|
||||||
child: RealmDetailScreen(alias: state.pathParameters['alias']!),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: '/realm',
|
|
||||||
name: 'realm',
|
|
||||||
pageBuilder: (context, state) => NoTransitionPage(
|
|
||||||
child: const RealmScreen(),
|
|
||||||
),
|
|
||||||
routes: [
|
|
||||||
GoRoute(
|
|
||||||
path: '/manage',
|
|
||||||
name: 'realmManage',
|
|
||||||
pageBuilder: (context, state) => CustomTransitionPage(
|
|
||||||
child: RealmManageScreen(
|
|
||||||
editingRealmAlias: state.uri.queryParameters['editing'],
|
|
||||||
),
|
|
||||||
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
|
||||||
return FadeThroughTransition(
|
|
||||||
animation: animation,
|
|
||||||
secondaryAnimation: secondaryAnimation,
|
|
||||||
fillColor: Colors.transparent,
|
|
||||||
child: AppBackground(
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: '/album',
|
|
||||||
name: 'album',
|
|
||||||
pageBuilder: (context, state) => NoTransitionPage(
|
|
||||||
child: const AlbumScreen(),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/friend',
|
path: '/publishers/:name',
|
||||||
name: 'friend',
|
name: 'postPublisher',
|
||||||
pageBuilder: (context, state) => NoTransitionPage(
|
builder: (context, state) => PostPublisherScreen(name: state.pathParameters['name']!),
|
||||||
child: const FriendScreen(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/notification',
|
path: '/:slug',
|
||||||
name: 'notification',
|
name: 'postDetail',
|
||||||
pageBuilder: (context, state) => NoTransitionPage(
|
builder: (context, state) => PostDetailScreen(
|
||||||
child: const NotificationScreen(),
|
slug: state.pathParameters['slug']!,
|
||||||
|
preload: state.extra as SnPost?,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
ShellRoute(
|
GoRoute(
|
||||||
builder: (context, state, child) => AppPageScaffold(body: child),
|
path: '/account',
|
||||||
|
name: 'account',
|
||||||
|
builder: (context, state) => const AccountScreen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/chat',
|
||||||
|
name: 'chat',
|
||||||
|
builder: (context, state) => const ChatScreen(),
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/auth/login',
|
path: '/:scope/:alias',
|
||||||
name: 'authLogin',
|
name: 'chatRoom',
|
||||||
builder: (context, state) => const AppBackground(
|
builder: (context, state) => ChatRoomScreen(
|
||||||
child: LoginScreen(),
|
scope: state.pathParameters['scope']!,
|
||||||
|
alias: state.pathParameters['alias']!,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/auth/register',
|
path: '/:scope/:alias/call',
|
||||||
name: 'authRegister',
|
name: 'chatCallRoom',
|
||||||
builder: (context, state) => const AppBackground(
|
builder: (context, state) => CallRoomScreen(
|
||||||
child: RegisterScreen(),
|
scope: state.pathParameters['scope']!,
|
||||||
|
alias: state.pathParameters['alias']!,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/reports',
|
path: '/:scope/:alias/detail',
|
||||||
name: 'abuseReport',
|
name: 'channelDetail',
|
||||||
builder: (context, state) => const AppBackground(
|
builder: (context, state) => ChannelDetailScreen(
|
||||||
child: AbuseReportScreen(),
|
scope: state.pathParameters['scope']!,
|
||||||
|
alias: state.pathParameters['alias']!,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/account/profile/edit',
|
path: '/manage',
|
||||||
name: 'accountProfileEdit',
|
name: 'chatManage',
|
||||||
builder: (context, state) => const AppBackground(
|
builder: (context, state) => ChatManageScreen(
|
||||||
child: ProfileEditScreen(),
|
editingChannelAlias: state.uri.queryParameters['editing'],
|
||||||
),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: '/account/publishers',
|
|
||||||
name: 'accountPublishers',
|
|
||||||
builder: (context, state) => const AppBackground(
|
|
||||||
child: PublisherScreen(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: '/account/publishers/new',
|
|
||||||
name: 'accountPublisherNew',
|
|
||||||
builder: (context, state) => const AppBackground(
|
|
||||||
child: AccountPublisherNewScreen(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: '/account/publishers/edit/:name',
|
|
||||||
name: 'accountPublisherEdit',
|
|
||||||
builder: (context, state) => AppBackground(
|
|
||||||
child: AccountPublisherEditScreen(
|
|
||||||
name: state.pathParameters['name']!,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/realm',
|
||||||
|
name: 'realm',
|
||||||
|
pageBuilder: (context, state) => CustomTransitionPage(
|
||||||
|
transitionsBuilder: _fadeThroughTransition,
|
||||||
|
child: const RealmScreen(),
|
||||||
|
),
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: '/:alias',
|
||||||
|
name: 'realmDetail',
|
||||||
|
builder: (context, state) => RealmDetailScreen(alias: state.pathParameters['alias']!),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/manage',
|
||||||
|
name: 'realmManage',
|
||||||
|
builder: (context, state) => RealmManageScreen(
|
||||||
|
editingRealmAlias: state.uri.queryParameters['editing'],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/news',
|
||||||
|
name: 'news',
|
||||||
|
builder: (context, state) => const NewsScreen(),
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: '/:hash',
|
||||||
|
name: 'newsDetail',
|
||||||
|
builder: (context, state) => NewsDetailScreen(
|
||||||
|
hash: state.pathParameters['hash']!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/album',
|
||||||
|
name: 'album',
|
||||||
|
builder: (context, state) => const AlbumScreen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/friend',
|
||||||
|
name: 'friend',
|
||||||
|
builder: (context, state) => const FriendScreen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/notification',
|
||||||
|
name: 'notification',
|
||||||
|
builder: (context, state) => const NotificationScreen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/auth/login',
|
||||||
|
name: 'authLogin',
|
||||||
|
builder: (context, state) => LoginScreen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/auth/register',
|
||||||
|
name: 'authRegister',
|
||||||
|
builder: (context, state) => RegisterScreen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/reports',
|
||||||
|
name: 'abuseReport',
|
||||||
|
builder: (context, state) => AbuseReportScreen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/account/profile/edit',
|
||||||
|
name: 'accountProfileEdit',
|
||||||
|
builder: (context, state) => ProfileEditScreen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/account/publishers',
|
||||||
|
name: 'accountPublishers',
|
||||||
|
builder: (context, state) => PublisherScreen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/account/publishers/new',
|
||||||
|
name: 'accountPublisherNew',
|
||||||
|
builder: (context, state) => AccountPublisherNewScreen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/account/publishers/edit/:name',
|
||||||
|
name: 'accountPublisherEdit',
|
||||||
|
builder: (context, state) => AccountPublisherEditScreen(
|
||||||
|
name: state.pathParameters['name']!,
|
||||||
|
),
|
||||||
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/account/:name',
|
path: '/account/:name',
|
||||||
name: 'accountProfilePage',
|
name: 'accountProfilePage',
|
||||||
@@ -290,29 +234,15 @@ final _appRoutes = [
|
|||||||
child: UserScreen(name: state.pathParameters['name']!),
|
child: UserScreen(name: state.pathParameters['name']!),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ShellRoute(
|
GoRoute(
|
||||||
builder: (context, state, child) => AppPageScaffold(body: child),
|
path: '/settings',
|
||||||
routes: [
|
name: 'settings',
|
||||||
GoRoute(
|
builder: (context, state) => SettingsScreen(),
|
||||||
path: '/settings',
|
|
||||||
name: 'settings',
|
|
||||||
builder: (context, state) => const AppBackground(
|
|
||||||
child: SettingsScreen(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
ShellRoute(
|
GoRoute(
|
||||||
builder: (context, state, child) => AppPageScaffold(body: child),
|
path: '/about',
|
||||||
routes: [
|
name: 'about',
|
||||||
GoRoute(
|
builder: (context, state) => AboutScreen(),
|
||||||
path: '/about',
|
|
||||||
name: 'about',
|
|
||||||
builder: (context, state) => const AppBackground(
|
|
||||||
child: AboutScreen(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:provider/provider.dart';
|
|||||||
import 'package:styled_widget/styled_widget.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/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
|
|
||||||
import '../types/account.dart';
|
import '../types/account.dart';
|
||||||
|
|
||||||
@@ -56,7 +57,11 @@ class _AbuseReportScreenState extends State<AbuseReportScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return AppScaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: const PageBackButton(),
|
||||||
|
title: Text('screenAbuseReport').tr(),
|
||||||
|
),
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
ListTile(
|
ListTile(
|
||||||
@@ -73,6 +78,7 @@ class _AbuseReportScreenState extends State<AbuseReportScreen> {
|
|||||||
else
|
else
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
|
padding: EdgeInsets.only(top: 8),
|
||||||
itemCount: _reports.length,
|
itemCount: _reports.length,
|
||||||
itemBuilder: (context, idx) {
|
itemBuilder: (context, idx) {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import 'package:surface/providers/websocket.dart';
|
|||||||
import 'package:surface/widgets/account/account_image.dart';
|
import 'package:surface/widgets/account/account_image.dart';
|
||||||
import 'package:surface/widgets/app_bar_leading.dart';
|
import 'package:surface/widgets/app_bar_leading.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
|
|
||||||
class AccountScreen extends StatelessWidget {
|
class AccountScreen extends StatelessWidget {
|
||||||
const AccountScreen({super.key});
|
const AccountScreen({super.key});
|
||||||
@@ -20,7 +21,7 @@ class AccountScreen extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final ua = context.watch<UserProvider>();
|
final ua = context.watch<UserProvider>();
|
||||||
|
|
||||||
return Scaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: AutoAppBarLeading(),
|
leading: AutoAppBarLeading(),
|
||||||
title: Text("screenAccount").tr(),
|
title: Text("screenAccount").tr(),
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import 'package:surface/providers/userinfo.dart';
|
|||||||
import 'package:surface/widgets/account/account_image.dart';
|
import 'package:surface/widgets/account/account_image.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/loading_indicator.dart';
|
import 'package:surface/widgets/loading_indicator.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
import 'package:surface/widgets/universal_image.dart';
|
import 'package:surface/widgets/universal_image.dart';
|
||||||
|
|
||||||
class ProfileEditScreen extends StatefulWidget {
|
class ProfileEditScreen extends StatefulWidget {
|
||||||
@@ -81,8 +82,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
|||||||
onDateTimeChanged: (DateTime newDate) {
|
onDateTimeChanged: (DateTime newDate) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_birthday = newDate;
|
_birthday = newDate;
|
||||||
_birthdayController.text =
|
_birthdayController.text = DateFormat(_kDateFormat).format(_birthday!);
|
||||||
DateFormat(_kDateFormat).format(_birthday!);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -96,11 +96,9 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
|||||||
if (image == null) return;
|
if (image == null) return;
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
final ImageProvider imageProvider =
|
final ImageProvider imageProvider = kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path));
|
||||||
kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path));
|
final aspectRatios =
|
||||||
final aspectRatios = place == 'banner'
|
place == 'banner' ? [CropAspectRatio(width: 16, height: 7)] : [CropAspectRatio(width: 1, height: 1)];
|
||||||
? [CropAspectRatio(width: 16, height: 7)]
|
|
||||||
: [CropAspectRatio(width: 1, height: 1)];
|
|
||||||
final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS))
|
final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS))
|
||||||
? await showCupertinoImageCropper(
|
? await showCupertinoImageCropper(
|
||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
@@ -122,10 +120,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
|||||||
|
|
||||||
setState(() => _isBusy = true);
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
final rawBytes =
|
final rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!.buffer.asUint8List();
|
||||||
(await result.uiImage.toByteData(format: ImageByteFormat.png))!
|
|
||||||
.buffer
|
|
||||||
.asUint8List();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final attachment = await attach.directUploadOne(
|
final attachment = await attach.directUploadOne(
|
||||||
@@ -212,136 +207,141 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
|||||||
|
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
|
||||||
return SingleChildScrollView(
|
return AppScaffold(
|
||||||
child: Column(
|
appBar: AppBar(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
leading: const PageBackButton(),
|
||||||
children: [
|
title: Text('screenAccountProfileEdit').tr(),
|
||||||
LoadingIndicator(isActive: _isBusy),
|
),
|
||||||
const Gap(24),
|
body: SingleChildScrollView(
|
||||||
Stack(
|
child: Column(
|
||||||
clipBehavior: Clip.none,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Material(
|
LoadingIndicator(isActive: _isBusy),
|
||||||
elevation: 0,
|
const Gap(24),
|
||||||
child: InkWell(
|
Stack(
|
||||||
child: ClipRRect(
|
clipBehavior: Clip.none,
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
children: [
|
||||||
child: AspectRatio(
|
Material(
|
||||||
aspectRatio: 16 / 9,
|
elevation: 0,
|
||||||
child: Container(
|
child: InkWell(
|
||||||
color:
|
child: ClipRRect(
|
||||||
Theme.of(context).colorScheme.surfaceContainerHigh,
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
child: _banner != null
|
child: AspectRatio(
|
||||||
? AutoResizeUniversalImage(
|
aspectRatio: 16 / 9,
|
||||||
sn.getAttachmentUrl(_banner!),
|
child: Container(
|
||||||
fit: BoxFit.cover,
|
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
)
|
child: _banner != null
|
||||||
: const SizedBox.shrink(),
|
? AutoResizeUniversalImage(
|
||||||
|
sn.getAttachmentUrl(_banner!),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
_updateImage('banner');
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
bottom: -28,
|
|
||||||
left: 16,
|
|
||||||
child: Material(
|
|
||||||
elevation: 2,
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(40)),
|
|
||||||
child: InkWell(
|
|
||||||
child: AccountImage(content: _avatar, radius: 40),
|
|
||||||
onTap: () {
|
onTap: () {
|
||||||
_updateImage('avatar');
|
_updateImage('banner');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
Positioned(
|
||||||
],
|
bottom: -28,
|
||||||
).padding(horizontal: padding),
|
left: 16,
|
||||||
const Gap(8 + 28),
|
child: Material(
|
||||||
Column(
|
elevation: 2,
|
||||||
children: [
|
borderRadius: const BorderRadius.all(Radius.circular(40)),
|
||||||
TextField(
|
child: InkWell(
|
||||||
readOnly: true,
|
child: AccountImage(content: _avatar, radius: 40),
|
||||||
controller: _usernameController,
|
onTap: () {
|
||||||
decoration: InputDecoration(
|
_updateImage('avatar');
|
||||||
border: const UnderlineInputBorder(),
|
},
|
||||||
labelText: 'fieldUsername'.tr(),
|
|
||||||
helperText: 'fieldUsernameCannotEditHint'.tr(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Gap(4),
|
|
||||||
TextField(
|
|
||||||
controller: _nicknameController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
border: const UnderlineInputBorder(),
|
|
||||||
labelText: 'fieldNickname'.tr(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Gap(4),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Flexible(
|
|
||||||
flex: 1,
|
|
||||||
child: TextField(
|
|
||||||
controller: _firstNameController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
border: const UnderlineInputBorder(),
|
|
||||||
labelText: 'fieldFirstName'.tr(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Gap(8),
|
),
|
||||||
Flexible(
|
],
|
||||||
flex: 1,
|
).padding(horizontal: padding),
|
||||||
child: TextField(
|
const Gap(8 + 28),
|
||||||
controller: _lastNameController,
|
Column(
|
||||||
decoration: InputDecoration(
|
children: [
|
||||||
border: const UnderlineInputBorder(),
|
TextField(
|
||||||
labelText: 'fieldLastName'.tr(),
|
readOnly: true,
|
||||||
|
controller: _usernameController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const UnderlineInputBorder(),
|
||||||
|
labelText: 'fieldUsername'.tr(),
|
||||||
|
helperText: 'fieldUsernameCannotEditHint'.tr(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(4),
|
||||||
|
TextField(
|
||||||
|
controller: _nicknameController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const UnderlineInputBorder(),
|
||||||
|
labelText: 'fieldNickname'.tr(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(4),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
flex: 1,
|
||||||
|
child: TextField(
|
||||||
|
controller: _firstNameController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const UnderlineInputBorder(),
|
||||||
|
labelText: 'fieldFirstName'.tr(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const Gap(8),
|
||||||
|
Flexible(
|
||||||
|
flex: 1,
|
||||||
|
child: TextField(
|
||||||
|
controller: _lastNameController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const UnderlineInputBorder(),
|
||||||
|
labelText: 'fieldLastName'.tr(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Gap(4),
|
||||||
|
TextField(
|
||||||
|
controller: _descriptionController,
|
||||||
|
keyboardType: TextInputType.multiline,
|
||||||
|
maxLines: null,
|
||||||
|
minLines: 3,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const UnderlineInputBorder(),
|
||||||
|
labelText: 'fieldDescription'.tr(),
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
const Gap(4),
|
|
||||||
TextField(
|
|
||||||
controller: _descriptionController,
|
|
||||||
keyboardType: TextInputType.multiline,
|
|
||||||
maxLines: null,
|
|
||||||
minLines: 3,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
border: const UnderlineInputBorder(),
|
|
||||||
labelText: 'fieldDescription'.tr(),
|
|
||||||
),
|
),
|
||||||
),
|
const Gap(4),
|
||||||
const Gap(4),
|
TextField(
|
||||||
TextField(
|
controller: _birthdayController,
|
||||||
controller: _birthdayController,
|
readOnly: true,
|
||||||
readOnly: true,
|
decoration: InputDecoration(
|
||||||
decoration: InputDecoration(
|
border: const UnderlineInputBorder(),
|
||||||
border: const UnderlineInputBorder(),
|
labelText: 'fieldBirthday'.tr(),
|
||||||
labelText: 'fieldBirthday'.tr(),
|
),
|
||||||
|
onTap: () => _selectBirthday(),
|
||||||
),
|
),
|
||||||
onTap: () => _selectBirthday(),
|
],
|
||||||
),
|
).padding(horizontal: padding + 8),
|
||||||
],
|
const Gap(12),
|
||||||
).padding(horizontal: padding + 8),
|
Row(
|
||||||
const Gap(12),
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
Row(
|
children: [
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
ElevatedButton.icon(
|
||||||
children: [
|
onPressed: _isBusy ? null : _updateUserInfo,
|
||||||
ElevatedButton.icon(
|
icon: const Icon(Symbols.save),
|
||||||
onPressed: _isBusy ? null : _updateUserInfo,
|
label: Text('apply').tr(),
|
||||||
icon: const Icon(Symbols.save),
|
),
|
||||||
label: Text('apply').tr(),
|
],
|
||||||
),
|
).padding(horizontal: padding),
|
||||||
],
|
],
|
||||||
).padding(horizontal: padding),
|
),
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -241,6 +241,7 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
|||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
body: CustomScrollView(
|
body: CustomScrollView(
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
slivers: [
|
slivers: [
|
||||||
@@ -517,6 +518,12 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
|||||||
future: _getCheckInRecords(),
|
future: _getCheckInRecords(),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (!snapshot.hasData) return const SizedBox.shrink();
|
if (!snapshot.hasData) return const SizedBox.shrink();
|
||||||
|
if (snapshot.data!.length <= 1) {
|
||||||
|
return Text(
|
||||||
|
'accountCheckInNoRecords',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
).tr().fontWeight(FontWeight.bold).center().padding(horizontal: 20, vertical: 8);
|
||||||
|
}
|
||||||
final records = snapshot.data!;
|
final records = snapshot.data!;
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
@@ -588,7 +595,7 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
|||||||
subtitle: Text('@${ele.name}'),
|
subtitle: Text('@${ele.name}'),
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
GoRouter.of(context).pushNamed(
|
GoRouter.of(context).goNamed(
|
||||||
'postPublisher',
|
'postPublisher',
|
||||||
pathParameters: {'name': ele.name},
|
pathParameters: {'name': ele.name},
|
||||||
);
|
);
|
||||||
@@ -18,6 +18,7 @@ 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/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/loading_indicator.dart';
|
import 'package:surface/widgets/loading_indicator.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
import 'package:surface/widgets/universal_image.dart';
|
import 'package:surface/widgets/universal_image.dart';
|
||||||
|
|
||||||
class AccountPublisherEditScreen extends StatefulWidget {
|
class AccountPublisherEditScreen extends StatefulWidget {
|
||||||
@@ -176,7 +177,7 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen>
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
|
||||||
return Scaffold(
|
return AppScaffold(
|
||||||
body: SingleChildScrollView(
|
body: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import 'package:surface/providers/userinfo.dart';
|
|||||||
import 'package:surface/types/realm.dart';
|
import 'package:surface/types/realm.dart';
|
||||||
import 'package:surface/widgets/account/account_image.dart';
|
import 'package:surface/widgets/account/account_image.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
|
|
||||||
class AccountPublisherNewScreen extends StatefulWidget {
|
class AccountPublisherNewScreen extends StatefulWidget {
|
||||||
const AccountPublisherNewScreen({super.key});
|
const AccountPublisherNewScreen({super.key});
|
||||||
@@ -24,7 +25,11 @@ class _AccountPublisherNewScreenState extends State<AccountPublisherNewScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return AppScaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: const PageBackButton(),
|
||||||
|
title: Text('screenAccountPublisherNew').tr(),
|
||||||
|
),
|
||||||
body: SingleChildScrollView(
|
body: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ 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/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/loading_indicator.dart';
|
import 'package:surface/widgets/loading_indicator.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
|
|
||||||
class PublisherScreen extends StatefulWidget {
|
class PublisherScreen extends StatefulWidget {
|
||||||
const PublisherScreen({super.key});
|
const PublisherScreen({super.key});
|
||||||
@@ -32,8 +33,7 @@ class _PublisherScreenState extends State<PublisherScreen> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
final resp = await sn.client.get('/cgi/co/publishers/me');
|
final resp = await sn.client.get('/cgi/co/publishers/me');
|
||||||
final List<SnPublisher> out = List<SnPublisher>.from(
|
final List<SnPublisher> out = List<SnPublisher>.from(resp.data?.map((e) => SnPublisher.fromJson(e)) ?? []);
|
||||||
resp.data?.map((e) => SnPublisher.fromJson(e)) ?? []);
|
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
@@ -53,7 +53,11 @@ class _PublisherScreenState extends State<PublisherScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return AppScaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: const PageBackButton(),
|
||||||
|
title: Text('screenAccountPublishers').tr(),
|
||||||
|
),
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
ListTile(
|
ListTile(
|
||||||
@@ -62,9 +66,7 @@ class _PublisherScreenState extends State<PublisherScreen> {
|
|||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
leading: const Icon(Symbols.add_circle),
|
leading: const Icon(Symbols.add_circle),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
GoRouter.of(context)
|
GoRouter.of(context).pushNamed('accountPublisherNew').then((value) {
|
||||||
.pushNamed('accountPublisherNew')
|
|
||||||
.then((value) {
|
|
||||||
if (value == true) {
|
if (value == true) {
|
||||||
_publishers.clear();
|
_publishers.clear();
|
||||||
_fetchPublishers();
|
_fetchPublishers();
|
||||||
@@ -75,48 +77,52 @@ class _PublisherScreenState extends State<PublisherScreen> {
|
|||||||
const Divider(height: 1),
|
const Divider(height: 1),
|
||||||
LoadingIndicator(isActive: _isBusy),
|
LoadingIndicator(isActive: _isBusy),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: RefreshIndicator(
|
child: MediaQuery.removePadding(
|
||||||
onRefresh: () {
|
context: context,
|
||||||
_publishers.clear();
|
removeTop: true,
|
||||||
return _fetchPublishers();
|
child: RefreshIndicator(
|
||||||
},
|
onRefresh: () {
|
||||||
child: ListView.builder(
|
_publishers.clear();
|
||||||
itemCount: _publishers.length,
|
return _fetchPublishers();
|
||||||
itemBuilder: (context, idx) {
|
|
||||||
final publisher = _publishers[idx];
|
|
||||||
return ListTile(
|
|
||||||
title: Text(publisher.nick),
|
|
||||||
subtitle: Text('@${publisher.name}'),
|
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
leading: AccountImage(content: publisher.avatar),
|
|
||||||
trailing: PopupMenuButton(
|
|
||||||
itemBuilder: (BuildContext context) => [
|
|
||||||
PopupMenuItem(
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Symbols.edit),
|
|
||||||
const Gap(16),
|
|
||||||
Text('edit').tr(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
GoRouter.of(context).pushNamed(
|
|
||||||
'accountPublisherEdit',
|
|
||||||
pathParameters: {
|
|
||||||
'name': publisher.name,
|
|
||||||
},
|
|
||||||
).then((value) {
|
|
||||||
if (value == true) {
|
|
||||||
_publishers.clear();
|
|
||||||
_fetchPublishers();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
child: ListView.builder(
|
||||||
|
itemCount: _publishers.length,
|
||||||
|
itemBuilder: (context, idx) {
|
||||||
|
final publisher = _publishers[idx];
|
||||||
|
return ListTile(
|
||||||
|
title: Text(publisher.nick),
|
||||||
|
subtitle: Text('@${publisher.name}'),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
leading: AccountImage(content: publisher.avatar),
|
||||||
|
trailing: PopupMenuButton(
|
||||||
|
itemBuilder: (BuildContext context) => [
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.edit),
|
||||||
|
const Gap(16),
|
||||||
|
Text('edit').tr(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
GoRouter.of(context).pushNamed(
|
||||||
|
'accountPublisherEdit',
|
||||||
|
pathParameters: {
|
||||||
|
'name': publisher.name,
|
||||||
|
},
|
||||||
|
).then((value) {
|
||||||
|
if (value == true) {
|
||||||
|
_publishers.clear();
|
||||||
|
_fetchPublishers();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import 'package:surface/widgets/app_bar_leading.dart';
|
|||||||
import 'package:surface/widgets/attachment/attachment_zoom.dart';
|
import 'package:surface/widgets/attachment/attachment_zoom.dart';
|
||||||
import 'package:surface/widgets/attachment/attachment_item.dart';
|
import 'package:surface/widgets/attachment/attachment_item.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
class AlbumScreen extends StatefulWidget {
|
class AlbumScreen extends StatefulWidget {
|
||||||
@@ -82,7 +83,7 @@ class _AlbumScreenState extends State<AlbumScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return AppScaffold(
|
||||||
body: CustomScrollView(
|
body: CustomScrollView(
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
slivers: [
|
slivers: [
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import 'package:surface/providers/sn_network.dart';
|
|||||||
import 'package:surface/providers/userinfo.dart';
|
import 'package:surface/providers/userinfo.dart';
|
||||||
import 'package:surface/types/auth.dart';
|
import 'package:surface/types/auth.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
import '../../providers/websocket.dart';
|
import '../../providers/websocket.dart';
|
||||||
@@ -35,67 +36,73 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Theme(
|
return AppScaffold(
|
||||||
data: Theme.of(context).copyWith(canvasColor: Colors.transparent),
|
appBar: AppBar(
|
||||||
child: SingleChildScrollView(
|
leading: const PageBackButton(),
|
||||||
child: PageTransitionSwitcher(
|
title: Text('screenAuthLogin').tr(),
|
||||||
transitionBuilder: (
|
),
|
||||||
Widget child,
|
body: Theme(
|
||||||
Animation<double> primaryAnimation,
|
data: Theme.of(context).copyWith(canvasColor: Colors.transparent),
|
||||||
Animation<double> secondaryAnimation,
|
child: SingleChildScrollView(
|
||||||
) {
|
child: PageTransitionSwitcher(
|
||||||
return SharedAxisTransition(
|
transitionBuilder: (
|
||||||
animation: primaryAnimation,
|
Widget child,
|
||||||
secondaryAnimation: secondaryAnimation,
|
Animation<double> primaryAnimation,
|
||||||
transitionType: SharedAxisTransitionType.horizontal,
|
Animation<double> secondaryAnimation,
|
||||||
child: Container(
|
) {
|
||||||
constraints: BoxConstraints(maxWidth: 380),
|
return SharedAxisTransition(
|
||||||
child: child,
|
animation: primaryAnimation,
|
||||||
),
|
secondaryAnimation: secondaryAnimation,
|
||||||
);
|
transitionType: SharedAxisTransitionType.horizontal,
|
||||||
},
|
child: Container(
|
||||||
child: switch (_period % 3) {
|
constraints: BoxConstraints(maxWidth: 380),
|
||||||
1 => _LoginPickerScreen(
|
child: child,
|
||||||
key: const ValueKey(1),
|
),
|
||||||
ticket: _currentTicket,
|
);
|
||||||
factors: _factors,
|
},
|
||||||
onTicket: (p0) => setState(() {
|
child: switch (_period % 3) {
|
||||||
_currentTicket = p0;
|
1 => _LoginPickerScreen(
|
||||||
}),
|
key: const ValueKey(1),
|
||||||
onPickFactor: (p0) => setState(() {
|
ticket: _currentTicket,
|
||||||
_factorPicked = p0;
|
factors: _factors,
|
||||||
}),
|
onTicket: (p0) => setState(() {
|
||||||
onNext: () => setState(() {
|
_currentTicket = p0;
|
||||||
_period++;
|
}),
|
||||||
}),
|
onPickFactor: (p0) => setState(() {
|
||||||
),
|
_factorPicked = p0;
|
||||||
2 => _LoginCheckScreen(
|
}),
|
||||||
key: const ValueKey(2),
|
onNext: () => setState(() {
|
||||||
ticket: _currentTicket,
|
_period++;
|
||||||
factor: _factorPicked,
|
}),
|
||||||
onTicket: (p0) => setState(() {
|
),
|
||||||
_currentTicket = p0;
|
2 => _LoginCheckScreen(
|
||||||
}),
|
key: const ValueKey(2),
|
||||||
onNext: () => setState(() {
|
ticket: _currentTicket,
|
||||||
_period = 1;
|
factor: _factorPicked,
|
||||||
}),
|
onTicket: (p0) => setState(() {
|
||||||
),
|
_currentTicket = p0;
|
||||||
_ => _LoginLookupScreen(
|
}),
|
||||||
key: const ValueKey(0),
|
onNext: () => setState(() {
|
||||||
ticket: _currentTicket,
|
_period = 1;
|
||||||
onTicket: (p0) => setState(() {
|
}),
|
||||||
_currentTicket = p0;
|
),
|
||||||
}),
|
_ => _LoginLookupScreen(
|
||||||
onFactor: (p0) => setState(() {
|
key: const ValueKey(0),
|
||||||
_factors = p0;
|
ticket: _currentTicket,
|
||||||
}),
|
onTicket: (p0) => setState(() {
|
||||||
onNext: () => setState(() {
|
_currentTicket = p0;
|
||||||
_period++;
|
}),
|
||||||
}),
|
onFactor: (p0) => setState(() {
|
||||||
),
|
_factors = p0;
|
||||||
},
|
}),
|
||||||
).padding(all: 24),
|
onNext: () => setState(() {
|
||||||
).center(),
|
_period++;
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
).padding(all: 24),
|
||||||
|
).center(),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -441,7 +448,7 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> {
|
|||||||
|
|
||||||
widget.onNext();
|
widget.onNext();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if(mounted) context.showErrorDialog(err);
|
if (mounted) context.showErrorDialog(err);
|
||||||
return;
|
return;
|
||||||
} finally {
|
} finally {
|
||||||
setState(() => _isBusy = false);
|
setState(() => _isBusy = false);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'package:provider/provider.dart';
|
|||||||
import 'package:styled_widget/styled_widget.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/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
class RegisterScreen extends StatefulWidget {
|
class RegisterScreen extends StatefulWidget {
|
||||||
@@ -54,175 +55,178 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return StyledWidget(Container(
|
return AppScaffold(
|
||||||
constraints: const BoxConstraints(maxWidth: 380),
|
appBar: AppBar(
|
||||||
child: SingleChildScrollView(
|
leading: const PageBackButton(),
|
||||||
child: Column(
|
title: Text('screenAuthRegister').tr(),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
),
|
||||||
children: [
|
body: StyledWidget(Container(
|
||||||
Align(
|
constraints: const BoxConstraints(maxWidth: 380),
|
||||||
alignment: Alignment.centerLeft,
|
child: SingleChildScrollView(
|
||||||
child: CircleAvatar(
|
child: Column(
|
||||||
radius: 26,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
child: const Icon(
|
children: [
|
||||||
Symbols.person_add,
|
Align(
|
||||||
size: 28,
|
alignment: Alignment.centerLeft,
|
||||||
),
|
child: CircleAvatar(
|
||||||
).padding(bottom: 8),
|
radius: 26,
|
||||||
),
|
child: const Icon(
|
||||||
Text(
|
Symbols.person_add,
|
||||||
'screenAuthRegister',
|
size: 28,
|
||||||
style: const TextStyle(
|
),
|
||||||
fontSize: 28,
|
).padding(bottom: 8),
|
||||||
fontWeight: FontWeight.w900,
|
|
||||||
),
|
),
|
||||||
).tr().padding(left: 4, bottom: 16),
|
Text(
|
||||||
Form(
|
'screenAuthRegister',
|
||||||
key: _formKey,
|
style: const TextStyle(
|
||||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
fontSize: 28,
|
||||||
child: Column(
|
fontWeight: FontWeight.w900,
|
||||||
children: [
|
),
|
||||||
TextFormField(
|
).tr().padding(left: 4, bottom: 16),
|
||||||
validator: (value) {
|
Form(
|
||||||
if (value == null || value.length < 4 || value.length > 32) {
|
key: _formKey,
|
||||||
return 'fieldUsernameLengthLimit'.tr(args: [4.toString(), 32.toString()]);
|
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||||
}
|
child: Column(
|
||||||
if (!RegExp(r'^[a-zA-Z0-9_]+$').hasMatch(value)) {
|
children: [
|
||||||
return 'fieldUsernameAlphanumOnly'.tr();
|
TextFormField(
|
||||||
}
|
validator: (value) {
|
||||||
return null;
|
if (value == null || value.length < 4 || value.length > 32) {
|
||||||
},
|
return 'fieldUsernameLengthLimit'.tr(args: [4.toString(), 32.toString()]);
|
||||||
autocorrect: false,
|
}
|
||||||
enableSuggestions: false,
|
if (!RegExp(r'^[a-zA-Z0-9_]+$').hasMatch(value)) {
|
||||||
controller: _usernameController,
|
return 'fieldUsernameAlphanumOnly'.tr();
|
||||||
autofillHints: const [AutofillHints.username],
|
}
|
||||||
decoration: InputDecoration(
|
return null;
|
||||||
isDense: true,
|
},
|
||||||
border: const UnderlineInputBorder(),
|
autocorrect: false,
|
||||||
labelText: 'fieldUsername'.tr(),
|
enableSuggestions: false,
|
||||||
),
|
controller: _usernameController,
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
autofillHints: const [AutofillHints.username],
|
||||||
),
|
decoration: InputDecoration(
|
||||||
const Gap(12),
|
isDense: true,
|
||||||
TextFormField(
|
border: const UnderlineInputBorder(),
|
||||||
validator: (value) {
|
labelText: 'fieldUsername'.tr(),
|
||||||
if (value == null || value.length < 4 || value.length > 32) {
|
|
||||||
return 'fieldNicknameLengthLimit'.tr(args: [4.toString(), 32.toString()]);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
autocorrect: false,
|
|
||||||
enableSuggestions: false,
|
|
||||||
controller: _nicknameController,
|
|
||||||
autofillHints: const [AutofillHints.nickname],
|
|
||||||
decoration: InputDecoration(
|
|
||||||
isDense: true,
|
|
||||||
border: const UnderlineInputBorder(),
|
|
||||||
labelText: 'fieldNickname'.tr(),
|
|
||||||
),
|
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
|
||||||
),
|
|
||||||
const Gap(12),
|
|
||||||
TextFormField(
|
|
||||||
validator: (value) {
|
|
||||||
if (value == null || value.isEmpty) {
|
|
||||||
return 'fieldCannotBeEmpty'.tr();
|
|
||||||
}
|
|
||||||
if (!EmailValidator.validate(value)) {
|
|
||||||
return 'fieldEmailAddressMustBeValid'.tr();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
autocorrect: false,
|
|
||||||
enableSuggestions: false,
|
|
||||||
controller: _emailController,
|
|
||||||
autofillHints: const [AutofillHints.email],
|
|
||||||
decoration: InputDecoration(
|
|
||||||
isDense: true,
|
|
||||||
border: const UnderlineInputBorder(),
|
|
||||||
labelText: 'fieldEmail'.tr(),
|
|
||||||
),
|
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
|
||||||
),
|
|
||||||
const Gap(12),
|
|
||||||
TextFormField(
|
|
||||||
validator: (value) {
|
|
||||||
if (value == null || value.isEmpty) {
|
|
||||||
return 'fieldCannotBeEmpty'.tr();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
obscureText: true,
|
|
||||||
autocorrect: false,
|
|
||||||
enableSuggestions: false,
|
|
||||||
autofillHints: const [AutofillHints.password],
|
|
||||||
controller: _passwordController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
isDense: true,
|
|
||||||
border: const UnderlineInputBorder(),
|
|
||||||
labelText: 'fieldPassword'.tr(),
|
|
||||||
),
|
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).padding(horizontal: 7),
|
|
||||||
),
|
|
||||||
const Gap(16),
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.centerRight,
|
|
||||||
child: StyledWidget(
|
|
||||||
Container(
|
|
||||||
constraints: const BoxConstraints(maxWidth: 290),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'termAcceptNextWithAgree'.tr(),
|
|
||||||
textAlign: TextAlign.end,
|
|
||||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
|
||||||
color: Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.onSurface
|
|
||||||
.withAlpha((255 * 0.75).round()),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Material(
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
color: Colors.transparent,
|
),
|
||||||
child: InkWell(
|
const Gap(12),
|
||||||
child: Row(
|
TextFormField(
|
||||||
mainAxisSize: MainAxisSize.min,
|
validator: (value) {
|
||||||
children: [
|
if (value == null || value.length < 4 || value.length > 32) {
|
||||||
Text('termAcceptLink'.tr()),
|
return 'fieldNicknameLengthLimit'.tr(args: [4.toString(), 32.toString()]);
|
||||||
const Gap(4),
|
}
|
||||||
const Icon(Symbols.launch, size: 14),
|
return null;
|
||||||
],
|
},
|
||||||
|
autocorrect: false,
|
||||||
|
enableSuggestions: false,
|
||||||
|
controller: _nicknameController,
|
||||||
|
autofillHints: const [AutofillHints.nickname],
|
||||||
|
decoration: InputDecoration(
|
||||||
|
isDense: true,
|
||||||
|
border: const UnderlineInputBorder(),
|
||||||
|
labelText: 'fieldNickname'.tr(),
|
||||||
|
),
|
||||||
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
),
|
||||||
|
const Gap(12),
|
||||||
|
TextFormField(
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'fieldCannotBeEmpty'.tr();
|
||||||
|
}
|
||||||
|
if (!EmailValidator.validate(value)) {
|
||||||
|
return 'fieldEmailAddressMustBeValid'.tr();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
autocorrect: false,
|
||||||
|
enableSuggestions: false,
|
||||||
|
controller: _emailController,
|
||||||
|
autofillHints: const [AutofillHints.email],
|
||||||
|
decoration: InputDecoration(
|
||||||
|
isDense: true,
|
||||||
|
border: const UnderlineInputBorder(),
|
||||||
|
labelText: 'fieldEmail'.tr(),
|
||||||
|
),
|
||||||
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
),
|
||||||
|
const Gap(12),
|
||||||
|
TextFormField(
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'fieldCannotBeEmpty'.tr();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
obscureText: true,
|
||||||
|
autocorrect: false,
|
||||||
|
enableSuggestions: false,
|
||||||
|
autofillHints: const [AutofillHints.password],
|
||||||
|
controller: _passwordController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
isDense: true,
|
||||||
|
border: const UnderlineInputBorder(),
|
||||||
|
labelText: 'fieldPassword'.tr(),
|
||||||
|
),
|
||||||
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 7),
|
||||||
|
),
|
||||||
|
const Gap(16),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: StyledWidget(
|
||||||
|
Container(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 290),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'termAcceptNextWithAgree'.tr(),
|
||||||
|
textAlign: TextAlign.end,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface.withAlpha((255 * 0.75).round()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text('termAcceptLink'.tr()),
|
||||||
|
const Gap(4),
|
||||||
|
const Icon(Symbols.launch, size: 14),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
launchUrlString('https://solsynth.dev/terms');
|
||||||
|
},
|
||||||
),
|
),
|
||||||
onTap: () {
|
|
||||||
launchUrlString('https://solsynth.dev/terms');
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).padding(horizontal: 16),
|
||||||
|
),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: () => _performAction(context),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text('next').tr(),
|
||||||
|
const Icon(Symbols.chevron_right),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
).padding(horizontal: 16),
|
|
||||||
),
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.centerRight,
|
|
||||||
child: TextButton(
|
|
||||||
onPressed: () => _performAction(context),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text('next').tr(),
|
|
||||||
const Icon(Symbols.chevron_right),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
)).padding(all: 24).center(),
|
||||||
)).padding(all: 24).center();
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import 'package:surface/widgets/account/account_select.dart';
|
|||||||
import 'package:surface/widgets/app_bar_leading.dart';
|
import 'package:surface/widgets/app_bar_leading.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/loading_indicator.dart';
|
import 'package:surface/widgets/loading_indicator.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
import 'package:surface/widgets/unauthorized_hint.dart';
|
import 'package:surface/widgets/unauthorized_hint.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
@@ -120,7 +121,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
final ua = context.read<UserProvider>();
|
final ua = context.read<UserProvider>();
|
||||||
|
|
||||||
if (!ua.isAuthorized) {
|
if (!ua.isAuthorized) {
|
||||||
return Scaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: AutoAppBarLeading(),
|
leading: AutoAppBarLeading(),
|
||||||
title: Text('screenChat').tr(),
|
title: Text('screenChat').tr(),
|
||||||
@@ -131,7 +132,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: AutoAppBarLeading(),
|
leading: AutoAppBarLeading(),
|
||||||
title: Text('screenChat').tr(),
|
title: Text('screenChat').tr(),
|
||||||
@@ -195,22 +196,58 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
children: [
|
children: [
|
||||||
LoadingIndicator(isActive: _isBusy),
|
LoadingIndicator(isActive: _isBusy),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: RefreshIndicator(
|
child: MediaQuery.removePadding(
|
||||||
onRefresh: () => Future.sync(() => _refreshChannels()),
|
context: context,
|
||||||
child: ListView.builder(
|
removeTop: true,
|
||||||
itemCount: _channels?.length ?? 0,
|
child: RefreshIndicator(
|
||||||
itemBuilder: (context, idx) {
|
onRefresh: () => Future.sync(() => _refreshChannels()),
|
||||||
final channel = _channels![idx];
|
child: ListView.builder(
|
||||||
final lastMessage = _lastMessages?[channel.id];
|
itemCount: _channels?.length ?? 0,
|
||||||
|
itemBuilder: (context, idx) {
|
||||||
|
final channel = _channels![idx];
|
||||||
|
final lastMessage = _lastMessages?[channel.id];
|
||||||
|
|
||||||
if (channel.type == 1) {
|
if (channel.type == 1) {
|
||||||
final otherMember = channel.members?.cast<SnChannelMember?>().firstWhere(
|
final otherMember = channel.members?.cast<SnChannelMember?>().firstWhere(
|
||||||
(ele) => ele?.accountId != ua.user?.id,
|
(ele) => ele?.accountId != ua.user?.id,
|
||||||
orElse: () => null,
|
orElse: () => null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
title: Text(ud.getAccountFromCache(otherMember?.accountId)?.nick ?? channel.name),
|
||||||
|
subtitle: lastMessage != null
|
||||||
|
? Text(
|
||||||
|
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
'channelDirectMessageDescription'.tr(args: [
|
||||||
|
'@${ud.getAccountFromCache(otherMember?.accountId)?.name}',
|
||||||
|
]),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
leading: AccountImage(
|
||||||
|
content: ud.getAccountFromCache(otherMember?.accountId)?.avatar,
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
GoRouter.of(context).pushNamed(
|
||||||
|
'chatRoom',
|
||||||
|
pathParameters: {
|
||||||
|
'scope': channel.realm?.alias ?? 'global',
|
||||||
|
'alias': channel.alias,
|
||||||
|
},
|
||||||
|
).then((value) {
|
||||||
|
if (mounted) _refreshChannels();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(ud.getAccountFromCache(otherMember?.accountId)?.nick ?? channel.name),
|
title: Text(channel.name),
|
||||||
subtitle: lastMessage != null
|
subtitle: lastMessage != null
|
||||||
? Text(
|
? Text(
|
||||||
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
|
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
|
||||||
@@ -218,15 +255,14 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
)
|
)
|
||||||
: Text(
|
: Text(
|
||||||
'channelDirectMessageDescription'.tr(args: [
|
channel.description,
|
||||||
'@${ud.getAccountFromCache(otherMember?.accountId)?.name}',
|
|
||||||
]),
|
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
leading: AccountImage(
|
leading: AccountImage(
|
||||||
content: ud.getAccountFromCache(otherMember?.accountId)?.avatar,
|
content: null,
|
||||||
|
fallbackWidget: const Icon(Symbols.chat, size: 20),
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
GoRouter.of(context).pushNamed(
|
GoRouter.of(context).pushNamed(
|
||||||
@@ -240,39 +276,8 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
|
),
|
||||||
return ListTile(
|
|
||||||
title: Text(channel.name),
|
|
||||||
subtitle: lastMessage != null
|
|
||||||
? Text(
|
|
||||||
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
)
|
|
||||||
: Text(
|
|
||||||
channel.description,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
leading: AccountImage(
|
|
||||||
content: null,
|
|
||||||
fallbackWidget: const Icon(Symbols.chat, size: 20),
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
GoRouter.of(context).pushNamed(
|
|
||||||
'chatRoom',
|
|
||||||
pathParameters: {
|
|
||||||
'scope': channel.realm?.alias ?? 'global',
|
|
||||||
'alias': channel.alias,
|
|
||||||
},
|
|
||||||
).then((value) {
|
|
||||||
if (value == true) _refreshChannels();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -9,10 +9,12 @@ import 'package:styled_widget/styled_widget.dart';
|
|||||||
import 'package:surface/providers/chat_call.dart';
|
import 'package:surface/providers/chat_call.dart';
|
||||||
import 'package:surface/widgets/chat/call/call_controls.dart';
|
import 'package:surface/widgets/chat/call/call_controls.dart';
|
||||||
import 'package:surface/widgets/chat/call/call_participant.dart';
|
import 'package:surface/widgets/chat/call/call_participant.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
|
|
||||||
class CallRoomScreen extends StatefulWidget {
|
class CallRoomScreen extends StatefulWidget {
|
||||||
final String scope;
|
final String scope;
|
||||||
final String alias;
|
final String alias;
|
||||||
|
|
||||||
const CallRoomScreen({super.key, required this.scope, required this.alias});
|
const CallRoomScreen({super.key, required this.scope, required this.alias});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -35,8 +37,7 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
|
|||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
color:
|
color: Theme.of(context).colorScheme.surfaceContainer.withOpacity(0.75),
|
||||||
Theme.of(context).colorScheme.surfaceContainer.withOpacity(0.75),
|
|
||||||
child: call.focusTrack != null
|
child: call.focusTrack != null
|
||||||
? InteractiveParticipantWidget(
|
? InteractiveParticipantWidget(
|
||||||
isFixedAvatar: false,
|
isFixedAvatar: false,
|
||||||
@@ -71,8 +72,7 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
|
|||||||
color: Theme.of(context).cardColor,
|
color: Theme.of(context).cardColor,
|
||||||
participant: track,
|
participant: track,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (track.participant.sid !=
|
if (track.participant.sid != call.focusTrack?.participant.sid) {
|
||||||
call.focusTrack?.participant.sid) {
|
|
||||||
call.setFocusTrack(track);
|
call.setFocusTrack(track);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -114,14 +114,10 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
|
|||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
child: InteractiveParticipantWidget(
|
child: InteractiveParticipantWidget(
|
||||||
color: Theme.of(context)
|
color: Theme.of(context).colorScheme.surfaceContainerHigh.withOpacity(0.75),
|
||||||
.colorScheme
|
|
||||||
.surfaceContainerHigh
|
|
||||||
.withOpacity(0.75),
|
|
||||||
participant: track,
|
participant: track,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (track.participant.sid !=
|
if (track.participant.sid != call.focusTrack?.participant.sid) {
|
||||||
call.focusTrack?.participant.sid) {
|
|
||||||
call.setFocusTrack(track);
|
call.setFocusTrack(track);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -152,157 +148,134 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
|
|||||||
return ListenableBuilder(
|
return ListenableBuilder(
|
||||||
listenable: call,
|
listenable: call,
|
||||||
builder: (context, _) {
|
builder: (context, _) {
|
||||||
return Scaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: RichText(
|
title: RichText(
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
text: TextSpan(children: [
|
text: TextSpan(children: [
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: 'call'.tr(),
|
text: 'call'.tr(),
|
||||||
style: Theme.of(context)
|
style: Theme.of(context).textTheme.titleLarge!.copyWith(color: Colors.white),
|
||||||
.textTheme
|
|
||||||
.titleLarge!
|
|
||||||
.copyWith(color: Colors.white),
|
|
||||||
),
|
),
|
||||||
const TextSpan(text: '\n'),
|
const TextSpan(text: '\n'),
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: call.lastDuration.toString(),
|
text: call.lastDuration.toString(),
|
||||||
style: Theme.of(context)
|
style: Theme.of(context).textTheme.bodySmall!.copyWith(color: Colors.white),
|
||||||
.textTheme
|
|
||||||
.bodySmall!
|
|
||||||
.copyWith(color: Colors.white),
|
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: SafeArea(
|
body: GestureDetector(
|
||||||
child: GestureDetector(
|
behavior: HitTestBehavior.translucent,
|
||||||
behavior: HitTestBehavior.translucent,
|
child: Column(
|
||||||
child: Column(
|
children: [
|
||||||
children: [
|
SizedBox(
|
||||||
|
width: MediaQuery.of(context).size.width,
|
||||||
|
height: 64,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Builder(builder: (context) {
|
||||||
|
final call = context.read<ChatCallProvider>();
|
||||||
|
final connectionQuality =
|
||||||
|
call.room.localParticipant?.connectionQuality ?? livekit.ConnectionQuality.unknown;
|
||||||
|
return Expanded(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
call.channel?.name ?? 'unknown'.tr(),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(6),
|
||||||
|
Text(call.lastDuration.toString())
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
{
|
||||||
|
livekit.ConnectionState.disconnected: 'callStatusDisconnected'.tr(),
|
||||||
|
livekit.ConnectionState.connected: 'callStatusConnected'.tr(),
|
||||||
|
livekit.ConnectionState.connecting: 'callStatusConnecting'.tr(),
|
||||||
|
livekit.ConnectionState.reconnecting: 'callStatusReconnecting'.tr(),
|
||||||
|
}[call.room.connectionState]!,
|
||||||
|
),
|
||||||
|
const Gap(6),
|
||||||
|
if (connectionQuality != livekit.ConnectionQuality.unknown)
|
||||||
|
Icon(
|
||||||
|
{
|
||||||
|
livekit.ConnectionQuality.excellent: Icons.signal_cellular_alt,
|
||||||
|
livekit.ConnectionQuality.good: Icons.signal_cellular_alt_2_bar,
|
||||||
|
livekit.ConnectionQuality.poor: Icons.signal_cellular_alt_1_bar,
|
||||||
|
}[connectionQuality],
|
||||||
|
color: {
|
||||||
|
livekit.ConnectionQuality.excellent: Colors.green,
|
||||||
|
livekit.ConnectionQuality.good: Colors.orange,
|
||||||
|
livekit.ConnectionQuality.poor: Colors.red,
|
||||||
|
}[connectionQuality],
|
||||||
|
size: 16,
|
||||||
|
)
|
||||||
|
else
|
||||||
|
const SizedBox(
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
color: Colors.white,
|
||||||
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
|
).padding(all: 3),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: _layoutMode == 0 ? const Icon(Icons.view_list) : const Icon(Icons.grid_view),
|
||||||
|
onPressed: () {
|
||||||
|
_switchLayout();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(left: 20, right: 16),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Material(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||||
|
child: Builder(
|
||||||
|
builder: (context) {
|
||||||
|
switch (_layoutMode) {
|
||||||
|
case 1:
|
||||||
|
return _buildGridLayout();
|
||||||
|
default:
|
||||||
|
return _buildListLayout();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (call.room.localParticipant != null)
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: MediaQuery.of(context).size.width,
|
width: MediaQuery.of(context).size.width,
|
||||||
height: 64,
|
child: ControlsWidget(
|
||||||
child: Row(
|
call.room,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
call.room.localParticipant!,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Builder(builder: (context) {
|
|
||||||
final call = context.read<ChatCallProvider>();
|
|
||||||
final connectionQuality =
|
|
||||||
call.room.localParticipant?.connectionQuality ??
|
|
||||||
livekit.ConnectionQuality.unknown;
|
|
||||||
return Expanded(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
call.channel?.name ?? 'unknown'.tr(),
|
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Gap(6),
|
|
||||||
Text(call.lastDuration.toString())
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
{
|
|
||||||
livekit.ConnectionState.disconnected:
|
|
||||||
'callStatusDisconnected'.tr(),
|
|
||||||
livekit.ConnectionState.connected:
|
|
||||||
'callStatusConnected'.tr(),
|
|
||||||
livekit.ConnectionState.connecting:
|
|
||||||
'callStatusConnecting'.tr(),
|
|
||||||
livekit.ConnectionState.reconnecting:
|
|
||||||
'callStatusReconnecting'.tr(),
|
|
||||||
}[call.room.connectionState]!,
|
|
||||||
),
|
|
||||||
const Gap(6),
|
|
||||||
if (connectionQuality !=
|
|
||||||
livekit.ConnectionQuality.unknown)
|
|
||||||
Icon(
|
|
||||||
{
|
|
||||||
livekit.ConnectionQuality.excellent:
|
|
||||||
Icons.signal_cellular_alt,
|
|
||||||
livekit.ConnectionQuality.good:
|
|
||||||
Icons.signal_cellular_alt_2_bar,
|
|
||||||
livekit.ConnectionQuality.poor:
|
|
||||||
Icons.signal_cellular_alt_1_bar,
|
|
||||||
}[connectionQuality],
|
|
||||||
color: {
|
|
||||||
livekit.ConnectionQuality.excellent:
|
|
||||||
Colors.green,
|
|
||||||
livekit.ConnectionQuality.good:
|
|
||||||
Colors.orange,
|
|
||||||
livekit.ConnectionQuality.poor:
|
|
||||||
Colors.red,
|
|
||||||
}[connectionQuality],
|
|
||||||
size: 16,
|
|
||||||
)
|
|
||||||
else
|
|
||||||
const SizedBox(
|
|
||||||
width: 12,
|
|
||||||
height: 12,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
color: Colors.white,
|
|
||||||
strokeWidth: 2,
|
|
||||||
),
|
|
||||||
).padding(all: 3),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
icon: _layoutMode == 0
|
|
||||||
? const Icon(Icons.view_list)
|
|
||||||
: const Icon(Icons.grid_view),
|
|
||||||
onPressed: () {
|
|
||||||
_switchLayout();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).padding(left: 20, right: 16),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Material(
|
|
||||||
color:
|
|
||||||
Theme.of(context).colorScheme.surfaceContainerLow,
|
|
||||||
child: Builder(
|
|
||||||
builder: (context) {
|
|
||||||
switch (_layoutMode) {
|
|
||||||
case 1:
|
|
||||||
return _buildGridLayout();
|
|
||||||
default:
|
|
||||||
return _buildListLayout();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (call.room.localParticipant != null)
|
],
|
||||||
SizedBox(
|
|
||||||
width: MediaQuery.of(context).size.width,
|
|
||||||
child: ControlsWidget(
|
|
||||||
call.room,
|
|
||||||
call.room.localParticipant!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () {},
|
|
||||||
),
|
),
|
||||||
|
onTap: () {},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import 'package:surface/types/chat.dart';
|
|||||||
import 'package:surface/widgets/account/account_image.dart';
|
import 'package:surface/widgets/account/account_image.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/loading_indicator.dart';
|
import 'package:surface/widgets/loading_indicator.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||||
|
|
||||||
class ChannelDetailScreen extends StatefulWidget {
|
class ChannelDetailScreen extends StatefulWidget {
|
||||||
@@ -189,7 +190,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
|||||||
|
|
||||||
final isOwned = ua.isAuthorized && _channel?.accountId == ua.user?.id;
|
final isOwned = ua.isAuthorized && _channel?.accountId == ua.user?.id;
|
||||||
|
|
||||||
return Scaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: _channel != null ? Text(_channel!.name) : Text('loading').tr(),
|
title: _channel != null ? Text(_channel!.name) : Text('loading').tr(),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import 'package:surface/types/realm.dart';
|
|||||||
import 'package:surface/widgets/account/account_image.dart';
|
import 'package:surface/widgets/account/account_image.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/loading_indicator.dart';
|
import 'package:surface/widgets/loading_indicator.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
class ChatManageScreen extends StatefulWidget {
|
class ChatManageScreen extends StatefulWidget {
|
||||||
@@ -87,7 +88,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
|||||||
try {
|
try {
|
||||||
final resp = await sn.client.request(
|
final resp = await sn.client.request(
|
||||||
widget.editingChannelAlias != null
|
widget.editingChannelAlias != null
|
||||||
? '/cgi/im/channels/$scope/${widget.editingChannelAlias}'
|
? '/cgi/im/channels/$scope/${_editingChannel!.id}'
|
||||||
: '/cgi/im/channels/$scope',
|
: '/cgi/im/channels/$scope',
|
||||||
data: payload,
|
data: payload,
|
||||||
options: Options(
|
options: Options(
|
||||||
@@ -121,7 +122,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: widget.editingChannelAlias != null
|
title: widget.editingChannelAlias != null
|
||||||
? Text('screenChatManage').tr()
|
? Text('screenChatManage').tr()
|
||||||
|
|||||||
@@ -17,8 +17,10 @@ import 'package:surface/types/chat.dart';
|
|||||||
import 'package:surface/widgets/chat/call/call_prejoin.dart';
|
import 'package:surface/widgets/chat/call/call_prejoin.dart';
|
||||||
import 'package:surface/widgets/chat/chat_message.dart';
|
import 'package:surface/widgets/chat/chat_message.dart';
|
||||||
import 'package:surface/widgets/chat/chat_message_input.dart';
|
import 'package:surface/widgets/chat/chat_message_input.dart';
|
||||||
|
import 'package:surface/widgets/chat/chat_typing_indicator.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/loading_indicator.dart';
|
import 'package:surface/widgets/loading_indicator.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||||
|
|
||||||
import '../../providers/user_directory.dart';
|
import '../../providers/user_directory.dart';
|
||||||
@@ -210,7 +212,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
final call = context.watch<ChatCallProvider>();
|
final call = context.watch<ChatCallProvider>();
|
||||||
final ud = context.read<UserDirectoryProvider>();
|
final ud = context.read<UserDirectoryProvider>();
|
||||||
|
|
||||||
return Scaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(
|
title: Text(
|
||||||
_channel?.type == 1
|
_channel?.type == 1
|
||||||
@@ -280,11 +282,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: InfiniteList(
|
child: InfiniteList(
|
||||||
reverse: true,
|
reverse: true,
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(top: 12),
|
||||||
left: 12,
|
|
||||||
right: 12,
|
|
||||||
top: 12,
|
|
||||||
),
|
|
||||||
hasReachedMax: _messageController.isAllLoaded,
|
hasReachedMax: _messageController.isAllLoaded,
|
||||||
itemCount: _messageController.messages.length,
|
itemCount: _messageController.messages.length,
|
||||||
isLoading: _messageController.isLoading,
|
isLoading: _messageController.isLoading,
|
||||||
@@ -310,23 +308,20 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
|
|
||||||
return Align(
|
return Align(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: Container(
|
child: ChatMessage(
|
||||||
constraints: BoxConstraints(maxWidth: 480),
|
data: message,
|
||||||
child: ChatMessage(
|
isMerged: canMerge,
|
||||||
data: message,
|
hasMerged: canMergePrevious,
|
||||||
isMerged: canMerge,
|
isPending: _messageController.unconfirmedMessages.contains(message.uuid),
|
||||||
hasMerged: canMergePrevious,
|
onReply: (value) {
|
||||||
isPending: _messageController.unconfirmedMessages.contains(message.uuid),
|
_inputGlobalKey.currentState?.setReply(value);
|
||||||
onReply: (value) {
|
},
|
||||||
_inputGlobalKey.currentState?.setReply(value);
|
onEdit: (value) {
|
||||||
},
|
_inputGlobalKey.currentState?.setEdit(value);
|
||||||
onEdit: (value) {
|
},
|
||||||
_inputGlobalKey.currentState?.setEdit(value);
|
onDelete: (value) {
|
||||||
},
|
_inputGlobalKey.currentState?.deleteMessage(value);
|
||||||
onDelete: (value) {
|
},
|
||||||
_inputGlobalKey.currentState?.deleteMessage(value);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -335,11 +330,17 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
if (!_messageController.isPending)
|
if (!_messageController.isPending)
|
||||||
Material(
|
Material(
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
child: ChatMessageInput(
|
child: Column(
|
||||||
key: _inputGlobalKey,
|
children: [
|
||||||
otherMember: _otherMember,
|
ChatTypingIndicator(controller: _messageController),
|
||||||
controller: _messageController,
|
ChatMessageInput(
|
||||||
).padding(bottom: MediaQuery.of(context).padding.bottom),
|
key: _inputGlobalKey,
|
||||||
|
otherMember: _otherMember,
|
||||||
|
controller: _messageController,
|
||||||
|
),
|
||||||
|
Gap(MediaQuery.of(context).padding.bottom),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:animations/animations.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_expandable_fab/flutter_expandable_fab.dart';
|
import 'package:flutter_expandable_fab/flutter_expandable_fab.dart';
|
||||||
@@ -8,9 +9,11 @@ import 'package:provider/provider.dart';
|
|||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:surface/providers/post.dart';
|
import 'package:surface/providers/post.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
|
import 'package:surface/screens/post/post_detail.dart';
|
||||||
import 'package:surface/types/post.dart';
|
import 'package:surface/types/post.dart';
|
||||||
import 'package:surface/widgets/app_bar_leading.dart';
|
import 'package:surface/widgets/app_bar_leading.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
import 'package:surface/widgets/post/post_item.dart';
|
import 'package:surface/widgets/post/post_item.dart';
|
||||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||||
|
|
||||||
@@ -93,7 +96,7 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return AppScaffold(
|
||||||
floatingActionButtonLocation: ExpandableFab.location,
|
floatingActionButtonLocation: ExpandableFab.location,
|
||||||
floatingActionButton: ExpandableFab(
|
floatingActionButton: ExpandableFab(
|
||||||
key: _fabKey,
|
key: _fabKey,
|
||||||
@@ -210,6 +213,7 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SliverGap(12),
|
||||||
SliverInfiniteList(
|
SliverInfiniteList(
|
||||||
itemCount: _posts.length,
|
itemCount: _posts.length,
|
||||||
isLoading: _isBusy,
|
isLoading: _isBusy,
|
||||||
@@ -217,27 +221,37 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
|||||||
hasReachedMax: _postCount != null && _posts.length >= _postCount!,
|
hasReachedMax: _postCount != null && _posts.length >= _postCount!,
|
||||||
onFetchData: _fetchPosts,
|
onFetchData: _fetchPosts,
|
||||||
itemBuilder: (context, idx) {
|
itemBuilder: (context, idx) {
|
||||||
return GestureDetector(
|
return Center(
|
||||||
child: PostItem(
|
child: OpenContainer(
|
||||||
data: _posts[idx],
|
closedBuilder: (_, __) => Container(
|
||||||
maxWidth: 640,
|
constraints: const BoxConstraints(maxWidth: 640),
|
||||||
onChanged: (data) {
|
child: PostItem(
|
||||||
setState(() => _posts[idx] = data);
|
data: _posts[idx],
|
||||||
},
|
maxWidth: 640,
|
||||||
onDeleted: () {
|
onChanged: (data) {
|
||||||
_refreshPosts();
|
setState(() => _posts[idx] = data);
|
||||||
},
|
},
|
||||||
|
onDeleted: () {
|
||||||
|
_refreshPosts();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
openBuilder: (_, close) => PostDetailScreen(
|
||||||
|
slug: _posts[idx].id.toString(),
|
||||||
|
preload: _posts[idx],
|
||||||
|
onBack: close,
|
||||||
|
),
|
||||||
|
openColor: Colors.transparent,
|
||||||
|
openElevation: 0,
|
||||||
|
closedColor: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(0.75),
|
||||||
|
transitionType: ContainerTransitionType.fade,
|
||||||
|
closedShape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(16)),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onTap: () {
|
|
||||||
GoRouter.of(context).pushNamed(
|
|
||||||
'postDetail',
|
|
||||||
pathParameters: {'slug': _posts[idx].id.toString()},
|
|
||||||
extra: _posts[idx],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
separatorBuilder: (context, index) => const Divider(height: 1),
|
separatorBuilder: (_, __) => const Gap(8),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import 'package:surface/widgets/account/account_image.dart';
|
|||||||
import 'package:surface/widgets/app_bar_leading.dart';
|
import 'package:surface/widgets/app_bar_leading.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/loading_indicator.dart';
|
import 'package:surface/widgets/loading_indicator.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
|
|
||||||
import '../providers/userinfo.dart';
|
import '../providers/userinfo.dart';
|
||||||
import '../widgets/unauthorized_hint.dart';
|
import '../widgets/unauthorized_hint.dart';
|
||||||
@@ -180,7 +181,7 @@ class _FriendScreenState extends State<FriendScreen> {
|
|||||||
final ua = context.read<UserProvider>();
|
final ua = context.read<UserProvider>();
|
||||||
|
|
||||||
if (!ua.isAuthorized) {
|
if (!ua.isAuthorized) {
|
||||||
return Scaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: AutoAppBarLeading(),
|
leading: AutoAppBarLeading(),
|
||||||
title: Text('screenFriend').tr(),
|
title: Text('screenFriend').tr(),
|
||||||
@@ -191,7 +192,7 @@ class _FriendScreenState extends State<FriendScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: AutoAppBarLeading(),
|
leading: AutoAppBarLeading(),
|
||||||
title: Text('screenFriend').tr(),
|
title: Text('screenFriend').tr(),
|
||||||
@@ -233,52 +234,56 @@ class _FriendScreenState extends State<FriendScreen> {
|
|||||||
if (_requests.isNotEmpty || _blocks.isNotEmpty)
|
if (_requests.isNotEmpty || _blocks.isNotEmpty)
|
||||||
const Divider(height: 1),
|
const Divider(height: 1),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: RefreshIndicator(
|
child: MediaQuery.removePadding(
|
||||||
onRefresh: () => Future.wait([
|
context: context,
|
||||||
_fetchRelations(),
|
removeTop: true,
|
||||||
_fetchRequests(),
|
child: RefreshIndicator(
|
||||||
]),
|
onRefresh: () => Future.wait([
|
||||||
child: ListView.builder(
|
_fetchRelations(),
|
||||||
itemCount: _relations.length,
|
_fetchRequests(),
|
||||||
itemBuilder: (context, index) {
|
]),
|
||||||
final relation = _relations[index];
|
child: ListView.builder(
|
||||||
final other = relation.related;
|
itemCount: _relations.length,
|
||||||
return ListTile(
|
itemBuilder: (context, index) {
|
||||||
contentPadding: const EdgeInsets.only(right: 24, left: 16),
|
final relation = _relations[index];
|
||||||
leading: AccountImage(content: other?.avatar),
|
final other = relation.related;
|
||||||
title: Text(other?.nick ?? 'unknown'),
|
return ListTile(
|
||||||
subtitle: Text(other?.nick ?? 'unknown'),
|
contentPadding: const EdgeInsets.only(right: 24, left: 16),
|
||||||
trailing: SizedBox(
|
leading: AccountImage(content: other?.avatar),
|
||||||
height: 48,
|
title: Text(other?.nick ?? 'unknown'),
|
||||||
width: 120,
|
subtitle: Text(other?.nick ?? 'unknown'),
|
||||||
child: Column(
|
trailing: SizedBox(
|
||||||
mainAxisSize: MainAxisSize.min,
|
height: 48,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
width: 120,
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
child: Column(
|
||||||
children: [
|
mainAxisSize: MainAxisSize.min,
|
||||||
Row(
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
InkWell(
|
Row(
|
||||||
onTap: _isUpdating
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
? null
|
children: [
|
||||||
: () => _changeRelation(relation, 2),
|
InkWell(
|
||||||
child: Text('friendBlock').tr(),
|
onTap: _isUpdating
|
||||||
),
|
? null
|
||||||
const Gap(8),
|
: () => _changeRelation(relation, 2),
|
||||||
InkWell(
|
child: Text('friendBlock').tr(),
|
||||||
onTap: _isUpdating
|
),
|
||||||
? null
|
const Gap(8),
|
||||||
: () => _deleteRelation(relation),
|
InkWell(
|
||||||
child: Text('friendDeleteAction').tr(),
|
onTap: _isUpdating
|
||||||
),
|
? null
|
||||||
],
|
: () => _deleteRelation(relation),
|
||||||
),
|
child: Text('friendDeleteAction').tr(),
|
||||||
],
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
},
|
||||||
},
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
|||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
import 'package:html/parser.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:relative_time/relative_time.dart';
|
import 'package:relative_time/relative_time.dart';
|
||||||
@@ -22,9 +23,11 @@ import 'package:surface/providers/special_day.dart';
|
|||||||
import 'package:surface/providers/userinfo.dart';
|
import 'package:surface/providers/userinfo.dart';
|
||||||
import 'package:surface/providers/widget.dart';
|
import 'package:surface/providers/widget.dart';
|
||||||
import 'package:surface/types/check_in.dart';
|
import 'package:surface/types/check_in.dart';
|
||||||
|
import 'package:surface/types/news.dart';
|
||||||
import 'package:surface/types/post.dart';
|
import 'package:surface/types/post.dart';
|
||||||
import 'package:surface/widgets/app_bar_leading.dart';
|
import 'package:surface/widgets/app_bar_leading.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
import 'package:surface/widgets/post/post_item.dart';
|
import 'package:surface/widgets/post/post_item.dart';
|
||||||
|
|
||||||
class HomeScreenDashEntry {
|
class HomeScreenDashEntry {
|
||||||
@@ -51,9 +54,9 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
static const List<HomeScreenDashEntry> kCards = [
|
static const List<HomeScreenDashEntry> kCards = [
|
||||||
HomeScreenDashEntry(
|
HomeScreenDashEntry(
|
||||||
name: 'dashEntryRecommendation',
|
name: 'dashEntryRecommendation',
|
||||||
cols: 2,
|
|
||||||
rows: 2,
|
|
||||||
child: _HomeDashRecommendationPostWidget(),
|
child: _HomeDashRecommendationPostWidget(),
|
||||||
|
rows: 2,
|
||||||
|
cols: 2,
|
||||||
),
|
),
|
||||||
HomeScreenDashEntry(
|
HomeScreenDashEntry(
|
||||||
name: 'dashEntryCheckIn',
|
name: 'dashEntryCheckIn',
|
||||||
@@ -63,11 +66,16 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
name: 'dashEntryNotification',
|
name: 'dashEntryNotification',
|
||||||
child: _HomeDashNotificationWidget(),
|
child: _HomeDashNotificationWidget(),
|
||||||
),
|
),
|
||||||
|
HomeScreenDashEntry(
|
||||||
|
name: 'dashEntryTodayNews',
|
||||||
|
child: _HomeDashTodayNews(),
|
||||||
|
cols: 2,
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: AutoAppBarLeading(),
|
leading: AutoAppBarLeading(),
|
||||||
title: Text("screenHome").tr(),
|
title: Text("screenHome").tr(),
|
||||||
@@ -153,9 +161,14 @@ class _HomeDashUpdateWidget extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _HomeDashSpecialDayWidget extends StatelessWidget {
|
class _HomeDashSpecialDayWidget extends StatefulWidget {
|
||||||
const _HomeDashSpecialDayWidget();
|
const _HomeDashSpecialDayWidget();
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_HomeDashSpecialDayWidget> createState() => _HomeDashSpecialDayWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HomeDashSpecialDayWidgetState extends State<_HomeDashSpecialDayWidget> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final ua = context.watch<UserProvider>();
|
final ua = context.watch<UserProvider>();
|
||||||
@@ -165,21 +178,20 @@ class _HomeDashSpecialDayWidget extends StatelessWidget {
|
|||||||
|
|
||||||
if (days.isNotEmpty) {
|
if (days.isNotEmpty) {
|
||||||
return Column(
|
return Column(
|
||||||
spacing: 8,
|
|
||||||
children: days.map((ele) {
|
children: days.map((ele) {
|
||||||
return Card(
|
return Card(
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: Text(kSpecialDaysSymbol[ele] ?? '🎉').fontSize(24),
|
leading: Text(kSpecialDaysSymbol[ele] ?? '🎉').fontSize(24),
|
||||||
title: Text('celebrate$ele').tr(args: [ua.user?.nick ?? 'user']),
|
title: Text('celebrate$ele').tr(args: [ua.user?.nick ?? 'user']),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
DateFormat('y/M/d').format(DateTime.now().copyWith(
|
DateFormat('y/M/d').format(DateTime.now().copyWith(
|
||||||
month: kSpecialDays[ele]!.$1,
|
month: kSpecialDays[ele]?.$1,
|
||||||
day: kSpecialDays[ele]!.$2,
|
day: kSpecialDays[ele]?.$2,
|
||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
).padding(bottom: 8);
|
).padding(bottom: 8);
|
||||||
}).toList());
|
}).toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
final nextOne = dayz.getNextSpecialDay();
|
final nextOne = dayz.getNextSpecialDay();
|
||||||
@@ -193,7 +205,7 @@ class _HomeDashSpecialDayWidget extends StatelessWidget {
|
|||||||
return Card(
|
return Card(
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: Text(kSpecialDaysSymbol[name] ?? '🎉').fontSize(24),
|
leading: Text(kSpecialDaysSymbol[name] ?? '🎉').fontSize(24),
|
||||||
title: Text('pending$name').tr(args: [RelativeTime(context).format(date)]),
|
title: Text('pending$name').tr(args: [RelativeTime(context).format(date).replaceFirst('in', '').trim()]),
|
||||||
subtitle: Row(
|
subtitle: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
@@ -204,6 +216,9 @@ class _HomeDashSpecialDayWidget extends StatelessWidget {
|
|||||||
separatorType: SeparatorType.symbol,
|
separatorType: SeparatorType.symbol,
|
||||||
decoration: BoxDecoration(),
|
decoration: BoxDecoration(),
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
|
onDone: () {
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
),
|
),
|
||||||
const Gap(12),
|
const Gap(12),
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -222,6 +237,107 @@ class _HomeDashSpecialDayWidget extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _HomeDashTodayNews extends StatefulWidget {
|
||||||
|
const _HomeDashTodayNews();
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_HomeDashTodayNews> createState() => _HomeDashTodayNewsState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HomeDashTodayNewsState extends State<_HomeDashTodayNews> {
|
||||||
|
SnNewsArticle? _article;
|
||||||
|
|
||||||
|
Future<void> _fetchArticle() async {
|
||||||
|
try {
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
final resp = await sn.client.get('/cgi/re/news/today');
|
||||||
|
_article = SnNewsArticle.fromJson(resp.data['data']);
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
rethrow;
|
||||||
|
} finally {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
initState() {
|
||||||
|
super.initState();
|
||||||
|
_fetchArticle();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Card(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.newspaper),
|
||||||
|
const Gap(8),
|
||||||
|
Text(
|
||||||
|
'newsToday',
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
).tr()
|
||||||
|
],
|
||||||
|
).padding(horizontal: 18, top: 12, bottom: 8),
|
||||||
|
if (_article != null)
|
||||||
|
Expanded(
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||||
|
child: Column(
|
||||||
|
spacing: 4,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
_article!.title,
|
||||||
|
style: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 18),
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
parse(_article!.description).children.map((e) => e.text.trim()).join(),
|
||||||
|
maxLines: 3,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
Builder(
|
||||||
|
builder: (context) {
|
||||||
|
final date = _article!.publishedAt ?? _article!.createdAt;
|
||||||
|
return Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
spacing: 2,
|
||||||
|
children: [
|
||||||
|
Text(DateFormat().format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
|
||||||
|
Text(' · ').textStyle(Theme.of(context).textTheme.bodySmall!).bold(),
|
||||||
|
Text(RelativeTime(context).format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
|
||||||
|
],
|
||||||
|
).opacity(0.75);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 16),
|
||||||
|
onTap: () {
|
||||||
|
GoRouter.of(context).pushNamed(
|
||||||
|
'newsDetail',
|
||||||
|
pathParameters: {'hash': _article!.hash},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Expanded(
|
||||||
|
child: Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _HomeDashCheckInWidget extends StatefulWidget {
|
class _HomeDashCheckInWidget extends StatefulWidget {
|
||||||
const _HomeDashCheckInWidget();
|
const _HomeDashCheckInWidget();
|
||||||
|
|
||||||
@@ -380,6 +496,8 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
|
|||||||
Text(
|
Text(
|
||||||
'dailyCheckInNone',
|
'dailyCheckInNone',
|
||||||
style: Theme.of(context).textTheme.bodyLarge,
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
).tr(),
|
).tr(),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|||||||
238
lib/screens/news/news_detail.dart
Normal file
238
lib/screens/news/news_detail.dart
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:html/dom.dart' as dom;
|
||||||
|
import 'package:html/parser.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:relative_time/relative_time.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:surface/providers/sn_network.dart';
|
||||||
|
import 'package:surface/types/news.dart';
|
||||||
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
|
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||||
|
import 'package:surface/widgets/universal_image.dart';
|
||||||
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
|
class NewsDetailScreen extends StatefulWidget {
|
||||||
|
final String hash;
|
||||||
|
|
||||||
|
const NewsDetailScreen({super.key, required this.hash});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<NewsDetailScreen> createState() => _NewsDetailScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NewsDetailScreenState extends State<NewsDetailScreen> {
|
||||||
|
SnNewsArticle? _article;
|
||||||
|
dom.Document? _articleFragment;
|
||||||
|
|
||||||
|
Future<void> _fetchArticle() async {
|
||||||
|
try {
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
final resp = await sn.client.get('/cgi/re/news/${widget.hash}');
|
||||||
|
_article = SnNewsArticle.fromJson(resp.data);
|
||||||
|
_articleFragment = parse(_article!.content);
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err).then((_) {
|
||||||
|
if (!mounted) return;
|
||||||
|
Navigator.pop(context);
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> _parseHtmlToWidgets(Iterable<dom.Element>? elements) {
|
||||||
|
if (elements == null) return [];
|
||||||
|
|
||||||
|
final List<Widget> widgets = [];
|
||||||
|
|
||||||
|
for (final node in elements) {
|
||||||
|
switch (node.localName) {
|
||||||
|
case 'h1':
|
||||||
|
case 'h2':
|
||||||
|
case 'h3':
|
||||||
|
case 'h4':
|
||||||
|
case 'h5':
|
||||||
|
case 'h6':
|
||||||
|
widgets.add(Text(node.text.trim(), style: Theme.of(context).textTheme.titleMedium));
|
||||||
|
break;
|
||||||
|
case 'p':
|
||||||
|
if (node.text.trim().isEmpty) continue;
|
||||||
|
widgets.add(
|
||||||
|
Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
text: node.text.trim(),
|
||||||
|
children: [
|
||||||
|
for (final child in node.children)
|
||||||
|
switch (child.localName) {
|
||||||
|
'a' => TextSpan(
|
||||||
|
text: child.text.trim(),
|
||||||
|
style: const TextStyle(decoration: TextDecoration.underline),
|
||||||
|
recognizer: TapGestureRecognizer()
|
||||||
|
..onTap = () {
|
||||||
|
launchUrlString(child.attributes['href']!);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_ => TextSpan(text: child.text.trim()),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
),
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'a':
|
||||||
|
// drop single link
|
||||||
|
break;
|
||||||
|
case 'div':
|
||||||
|
// ignore div text, normally it is not meaningful
|
||||||
|
widgets.addAll(_parseHtmlToWidgets(node.children));
|
||||||
|
break;
|
||||||
|
case 'hr':
|
||||||
|
widgets.add(const Divider());
|
||||||
|
break;
|
||||||
|
case 'img':
|
||||||
|
var src = node.attributes['src'];
|
||||||
|
if (src == null) break;
|
||||||
|
final width = double.tryParse(node.attributes['width'] ?? 'null');
|
||||||
|
final height = double.tryParse(node.attributes['height'] ?? 'null');
|
||||||
|
final ratio = width != null && height != null ? width / height : 1.0;
|
||||||
|
if (src.startsWith('//')) {
|
||||||
|
src = 'https:$src';
|
||||||
|
} else if (!src.startsWith('http')) {
|
||||||
|
final baseUri = Uri.parse(_article!.url);
|
||||||
|
final baseUrl = '${baseUri.scheme}://${baseUri.host}';
|
||||||
|
src = '$baseUrl/$src';
|
||||||
|
}
|
||||||
|
widgets.add(
|
||||||
|
AspectRatio(
|
||||||
|
aspectRatio: ratio,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
height: height ?? double.infinity,
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||||
|
child: Container(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
child: AutoResizeUniversalImage(
|
||||||
|
src,
|
||||||
|
fit: width != null && height != null ? BoxFit.cover : BoxFit.contain,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
widgets.addAll(_parseHtmlToWidgets(node.children));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return widgets;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_fetchArticle();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isReadingFromReader = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AppScaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: const PageBackButton(),
|
||||||
|
title: Text(_article?.title ?? 'loading'.tr()),
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
MaterialBanner(
|
||||||
|
dividerColor: Colors.transparent,
|
||||||
|
leading: const Icon(Icons.info),
|
||||||
|
content: Text(_isReadingFromReader ? 'newsReadingFromReader'.tr() : 'newsReadingFromOriginal'.tr()),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
child: Text('newsReadingProviderSwap').tr(),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() => _isReadingFromReader = !_isReadingFromReader);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (_articleFragment != null && _isReadingFromReader)
|
||||||
|
Expanded(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
Text(_article!.title, style: Theme.of(context).textTheme.titleLarge),
|
||||||
|
Builder(builder: (context) {
|
||||||
|
final htmlDescription = parse(_article!.description);
|
||||||
|
return Text(
|
||||||
|
htmlDescription.children.map((ele) => ele.text.trim()).join(),
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
Builder(builder: (context) {
|
||||||
|
final date = _article!.publishedAt ?? _article!.createdAt;
|
||||||
|
return Row(
|
||||||
|
spacing: 2,
|
||||||
|
children: [
|
||||||
|
Text(DateFormat().format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
|
||||||
|
Text(' · ').textStyle(Theme.of(context).textTheme.bodySmall!).bold(),
|
||||||
|
Text(RelativeTime(context).format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
|
||||||
|
],
|
||||||
|
).opacity(0.75);
|
||||||
|
}),
|
||||||
|
Text('newsDisclaimer').tr().textStyle(Theme.of(context).textTheme.bodySmall!).opacity(0.75),
|
||||||
|
const Divider(),
|
||||||
|
..._parseHtmlToWidgets(_articleFragment!.children),
|
||||||
|
const Divider(),
|
||||||
|
InkWell(
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Reference from original website',
|
||||||
|
style: TextStyle(decoration: TextDecoration.underline),
|
||||||
|
),
|
||||||
|
const Gap(4),
|
||||||
|
Icon(Icons.launch, size: 16),
|
||||||
|
],
|
||||||
|
).opacity(0.85),
|
||||||
|
onTap: () {
|
||||||
|
launchUrlString(_article!.url);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Gap(MediaQuery.of(context).padding.bottom),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 12, vertical: 16),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else if (_article != null)
|
||||||
|
Expanded(
|
||||||
|
child: InAppWebView(
|
||||||
|
key: GlobalKey(),
|
||||||
|
initialUrlRequest: URLRequest(url: WebUri(_article!.url)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
227
lib/screens/news/news_list.dart
Normal file
227
lib/screens/news/news_list.dart
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
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:html/parser.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:relative_time/relative_time.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:surface/providers/sn_network.dart';
|
||||||
|
import 'package:surface/types/news.dart';
|
||||||
|
import 'package:surface/widgets/app_bar_leading.dart';
|
||||||
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
|
import 'package:surface/widgets/universal_image.dart';
|
||||||
|
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||||
|
|
||||||
|
class NewsScreen extends StatefulWidget {
|
||||||
|
const NewsScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<NewsScreen> createState() => _NewsScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NewsScreenState extends State<NewsScreen> {
|
||||||
|
List<SnNewsSource>? _sources;
|
||||||
|
|
||||||
|
@override
|
||||||
|
initState() {
|
||||||
|
super.initState();
|
||||||
|
_fetchSources();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _fetchSources() async {
|
||||||
|
try {
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
final resp = await sn.client.get('/cgi/re/well-known/sources');
|
||||||
|
_sources = List<SnNewsSource>.from(
|
||||||
|
resp.data?.map((e) => SnNewsSource.fromJson(e)) ?? [],
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
} finally {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (_sources == null) {
|
||||||
|
return AppScaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: AutoAppBarLeading(),
|
||||||
|
title: Text('screenNews').tr(),
|
||||||
|
),
|
||||||
|
body: Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return DefaultTabController(
|
||||||
|
length: _sources!.length + 1,
|
||||||
|
child: AppScaffold(
|
||||||
|
body: NestedScrollView(
|
||||||
|
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
||||||
|
return <Widget>[
|
||||||
|
SliverOverlapAbsorber(
|
||||||
|
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
||||||
|
sliver: SliverAppBar(
|
||||||
|
leading: AutoAppBarLeading(),
|
||||||
|
title: Text('screenNews').tr(),
|
||||||
|
bottom: TabBar(
|
||||||
|
isScrollable: true,
|
||||||
|
tabs: [
|
||||||
|
Tab(child: Text('newsAllSources'.tr())),
|
||||||
|
for (final source in _sources!) Tab(child: Text(source.label)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
body: TabBarView(
|
||||||
|
children: [
|
||||||
|
_NewsArticleListWidget(allSources: _sources!),
|
||||||
|
for (final source in _sources!)
|
||||||
|
_NewsArticleListWidget(
|
||||||
|
source: source.id,
|
||||||
|
allSources: _sources!,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NewsArticleListWidget extends StatefulWidget {
|
||||||
|
final String? source;
|
||||||
|
final List<SnNewsSource> allSources;
|
||||||
|
|
||||||
|
const _NewsArticleListWidget({this.source, required this.allSources});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_NewsArticleListWidget> createState() => _NewsArticleListWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NewsArticleListWidgetState extends State<_NewsArticleListWidget> {
|
||||||
|
bool _isBusy = false;
|
||||||
|
|
||||||
|
int? _totalCount;
|
||||||
|
final List<SnNewsArticle> _articles = List.empty(growable: true);
|
||||||
|
|
||||||
|
Future<void> _fetchArticles() async {
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
final resp = await sn.client.get('/cgi/re/news', queryParameters: {
|
||||||
|
'take': 10,
|
||||||
|
'offset': _articles.length,
|
||||||
|
if (widget.source != null) 'source': widget.source,
|
||||||
|
});
|
||||||
|
_totalCount = resp.data['count'];
|
||||||
|
_articles.addAll(List<SnNewsArticle>.from(
|
||||||
|
resp.data['data']?.map((e) => SnNewsArticle.fromJson(e)) ?? [],
|
||||||
|
));
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
} finally {
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_fetchArticles();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MediaQuery.removePadding(
|
||||||
|
context: context,
|
||||||
|
removeTop: true,
|
||||||
|
child: RefreshIndicator(
|
||||||
|
onRefresh: _fetchArticles,
|
||||||
|
child: InfiniteList(
|
||||||
|
isLoading: _isBusy,
|
||||||
|
itemCount: _articles.length,
|
||||||
|
hasReachedMax: _totalCount != null && _articles.length >= _totalCount!,
|
||||||
|
onFetchData: () {
|
||||||
|
_fetchArticles();
|
||||||
|
},
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final article = _articles[index];
|
||||||
|
|
||||||
|
final baseUri = Uri.parse(article.url);
|
||||||
|
final baseUrl = '${baseUri.scheme}://${baseUri.host}';
|
||||||
|
|
||||||
|
final htmlDescription = parse(article.description);
|
||||||
|
final date = article.publishedAt ?? article.createdAt;
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
child: InkWell(
|
||||||
|
radius: 8,
|
||||||
|
onTap: () {
|
||||||
|
GoRouter.of(context).pushNamed(
|
||||||
|
'newsDetail',
|
||||||
|
pathParameters: {'hash': article.hash},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (article.thumbnail.isNotEmpty && !article.thumbnail.endsWith('.svg'))
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
topRight: Radius.circular(8),
|
||||||
|
topLeft: Radius.circular(8),
|
||||||
|
),
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: 16 / 9,
|
||||||
|
child: Container(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
child: AutoResizeUniversalImage(
|
||||||
|
article.thumbnail.startsWith('http') ? article.thumbnail : '$baseUrl/${article.thumbnail}',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(16),
|
||||||
|
Text(article.title).textStyle(Theme.of(context).textTheme.titleLarge!).padding(horizontal: 16),
|
||||||
|
const Gap(8),
|
||||||
|
Text(htmlDescription.children.map((ele) => ele.text.trim()).join())
|
||||||
|
.textStyle(Theme.of(context).textTheme.bodyMedium!)
|
||||||
|
.padding(horizontal: 16),
|
||||||
|
const Gap(8),
|
||||||
|
Row(
|
||||||
|
spacing: 2,
|
||||||
|
children: [
|
||||||
|
Text(widget.allSources.where((x) => x.id == article.source).first.label)
|
||||||
|
.textStyle(Theme.of(context).textTheme.bodySmall!),
|
||||||
|
],
|
||||||
|
).opacity(0.75).padding(horizontal: 16),
|
||||||
|
Row(
|
||||||
|
spacing: 2,
|
||||||
|
children: [
|
||||||
|
Text(DateFormat().format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
|
||||||
|
Text(' · ').textStyle(Theme.of(context).textTheme.bodySmall!).bold(),
|
||||||
|
Text(RelativeTime(context).format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
|
||||||
|
],
|
||||||
|
).opacity(0.75).padding(horizontal: 16),
|
||||||
|
const Gap(16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import 'package:surface/widgets/app_bar_leading.dart';
|
|||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/loading_indicator.dart';
|
import 'package:surface/widgets/loading_indicator.dart';
|
||||||
import 'package:surface/widgets/markdown_content.dart';
|
import 'package:surface/widgets/markdown_content.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
import 'package:surface/widgets/post/post_item.dart';
|
import 'package:surface/widgets/post/post_item.dart';
|
||||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||||
|
|
||||||
@@ -82,24 +83,15 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() => _isSubmitting = true);
|
setState(() => _isSubmitting = true);
|
||||||
|
|
||||||
List<int> markList = List.empty(growable: true);
|
|
||||||
for (final element in _notifications) {
|
|
||||||
if (element.id <= 0) continue;
|
|
||||||
if (element.readAt != null) continue;
|
|
||||||
markList.add(element.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
await sn.client.put('/cgi/id/notifications/read', data: {
|
final resp = await sn.client.put('/cgi/id/notifications/read/all');
|
||||||
'messages': markList,
|
|
||||||
});
|
|
||||||
_notifications.clear();
|
_notifications.clear();
|
||||||
_fetchNotifications();
|
_fetchNotifications();
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showSnackbar(
|
context.showSnackbar(
|
||||||
'notificationMarkAllReadPrompt'.plural(markList.length),
|
'notificationMarkAllReadPrompt'.plural(resp.data['count']),
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@@ -146,7 +138,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||||||
final ua = context.read<UserProvider>();
|
final ua = context.read<UserProvider>();
|
||||||
|
|
||||||
if (!ua.isAuthorized) {
|
if (!ua.isAuthorized) {
|
||||||
return Scaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: AutoAppBarLeading(),
|
leading: AutoAppBarLeading(),
|
||||||
title: Text('screenNotification').tr(),
|
title: Text('screenNotification').tr(),
|
||||||
@@ -157,7 +149,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: AutoAppBarLeading(),
|
leading: AutoAppBarLeading(),
|
||||||
title: Text('screenNotification').tr(),
|
title: Text('screenNotification').tr(),
|
||||||
@@ -215,10 +207,11 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||||||
style: Theme.of(context).textTheme.titleSmall,
|
style: Theme.of(context).textTheme.titleSmall,
|
||||||
),
|
),
|
||||||
if (nty.subtitle != null) const Gap(4),
|
if (nty.subtitle != null) const Gap(4),
|
||||||
MarkdownTextContent(
|
SelectionArea(
|
||||||
content: nty.body,
|
child: MarkdownTextContent(
|
||||||
isAutoWarp: true,
|
content: nty.body,
|
||||||
isSelectable: true,
|
isAutoWarp: true,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if ([
|
if ([
|
||||||
'interactive.feedback',
|
'interactive.feedback',
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import 'package:surface/providers/userinfo.dart';
|
|||||||
import 'package:surface/types/post.dart';
|
import 'package:surface/types/post.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/loading_indicator.dart';
|
import 'package:surface/widgets/loading_indicator.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_background.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
import 'package:surface/widgets/post/post_comment_list.dart';
|
import 'package:surface/widgets/post/post_comment_list.dart';
|
||||||
import 'package:surface/widgets/post/post_item.dart';
|
import 'package:surface/widgets/post/post_item.dart';
|
||||||
import 'package:surface/widgets/post/post_mini_editor.dart';
|
import 'package:surface/widgets/post/post_mini_editor.dart';
|
||||||
@@ -20,12 +22,9 @@ import 'package:surface/widgets/post/post_mini_editor.dart';
|
|||||||
class PostDetailScreen extends StatefulWidget {
|
class PostDetailScreen extends StatefulWidget {
|
||||||
final String slug;
|
final String slug;
|
||||||
final SnPost? preload;
|
final SnPost? preload;
|
||||||
|
final Function? onBack;
|
||||||
|
|
||||||
const PostDetailScreen({
|
const PostDetailScreen({super.key, required this.slug, this.preload, this.onBack});
|
||||||
super.key,
|
|
||||||
required this.slug,
|
|
||||||
this.preload,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<PostDetailScreen> createState() => _PostDetailScreenState();
|
State<PostDetailScreen> createState() => _PostDetailScreenState();
|
||||||
@@ -67,121 +66,129 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
|||||||
final ua = context.watch<UserProvider>();
|
final ua = context.watch<UserProvider>();
|
||||||
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||||
|
|
||||||
return Scaffold(
|
return AppBackground(
|
||||||
appBar: AppBar(
|
isRoot: widget.onBack != null,
|
||||||
leading: BackButton(
|
child: AppScaffold(
|
||||||
onPressed: () {
|
appBar: AppBar(
|
||||||
if (GoRouter.of(context).canPop()) {
|
leading: BackButton(
|
||||||
GoRouter.of(context).pop(context);
|
onPressed: () {
|
||||||
return;
|
if (widget.onBack != null) {
|
||||||
}
|
widget.onBack!.call();
|
||||||
GoRouter.of(context).replaceNamed('explore');
|
}
|
||||||
},
|
if (GoRouter.of(context).canPop()) {
|
||||||
),
|
GoRouter.of(context).pop(context);
|
||||||
title: _data?.body['title'] != null
|
return;
|
||||||
? RichText(
|
}
|
||||||
textAlign: TextAlign.center,
|
GoRouter.of(context).replaceNamed('explore');
|
||||||
text: TextSpan(children: [
|
},
|
||||||
TextSpan(
|
|
||||||
text: _data?.body['title'] ?? 'postNoun'.tr(),
|
|
||||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
|
||||||
color: Theme.of(context).appBarTheme.foregroundColor!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const TextSpan(text: '\n'),
|
|
||||||
TextSpan(
|
|
||||||
text: 'postDetail'.tr(),
|
|
||||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
|
||||||
color: Theme.of(context).appBarTheme.foregroundColor!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
: Text('postDetail').tr(),
|
|
||||||
),
|
|
||||||
body: CustomScrollView(
|
|
||||||
slivers: [
|
|
||||||
SliverToBoxAdapter(
|
|
||||||
child: LoadingIndicator(isActive: _isBusy),
|
|
||||||
),
|
),
|
||||||
if (_data != null)
|
title: _data?.body['title'] != null
|
||||||
SliverToBoxAdapter(
|
? RichText(
|
||||||
child: PostItem(
|
textAlign: TextAlign.center,
|
||||||
data: _data!,
|
text: TextSpan(children: [
|
||||||
maxWidth: 640,
|
TextSpan(
|
||||||
showComments: false,
|
text: _data?.body['title'] ?? 'postNoun'.tr(),
|
||||||
showFullPost: true,
|
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
||||||
onChanged: (data) {
|
color: Theme.of(context).appBarTheme.foregroundColor!,
|
||||||
setState(() => _data = data);
|
|
||||||
},
|
|
||||||
onDeleted: () {
|
|
||||||
Navigator.pop(context);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SliverToBoxAdapter(child: Divider(height: 1)),
|
|
||||||
if (_data != null)
|
|
||||||
SliverToBoxAdapter(
|
|
||||||
child: Container(
|
|
||||||
constraints: const BoxConstraints(maxWidth: 640),
|
|
||||||
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).center(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (_data != null && ua.isAuthorized)
|
|
||||||
SliverToBoxAdapter(
|
|
||||||
child: Container(
|
|
||||||
height: 240,
|
|
||||||
constraints: const BoxConstraints(maxWidth: 640),
|
|
||||||
margin:
|
|
||||||
ResponsiveBreakpoints.of(context).largerThan(MOBILE) ? const EdgeInsets.all(8) : EdgeInsets.zero,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
|
|
||||||
? const BorderRadius.all(Radius.circular(8))
|
|
||||||
: BorderRadius.zero,
|
|
||||||
border: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
|
|
||||||
? Border.all(
|
|
||||||
color: Theme.of(context).dividerColor,
|
|
||||||
width: 1 / devicePixelRatio,
|
|
||||||
)
|
|
||||||
: Border.symmetric(
|
|
||||||
horizontal: BorderSide(
|
|
||||||
color: Theme.of(context).dividerColor,
|
|
||||||
width: 1 / devicePixelRatio,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const TextSpan(text: '\n'),
|
||||||
child: PostMiniEditor(
|
TextSpan(
|
||||||
postReplyId: _data!.id,
|
text: 'postDetail'.tr(),
|
||||||
onPost: () {
|
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||||
setState(() {
|
color: Theme.of(context).appBarTheme.foregroundColor!,
|
||||||
_data = _data!.copyWith(
|
),
|
||||||
metric: _data!.metric.copyWith(
|
),
|
||||||
replyCount: _data!.metric.replyCount + 1,
|
]),
|
||||||
),
|
maxLines: 2,
|
||||||
);
|
overflow: TextOverflow.ellipsis,
|
||||||
});
|
)
|
||||||
_childListKey.currentState!.refresh();
|
: Text('postDetail').tr(),
|
||||||
|
),
|
||||||
|
body: CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: LoadingIndicator(isActive: _isBusy),
|
||||||
|
),
|
||||||
|
if (_data != null)
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: PostItem(
|
||||||
|
data: _data!,
|
||||||
|
maxWidth: 640,
|
||||||
|
showComments: false,
|
||||||
|
showFullPost: true,
|
||||||
|
onChanged: (data) {
|
||||||
|
setState(() => _data = data);
|
||||||
|
},
|
||||||
|
onDeleted: () {
|
||||||
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
).center(),
|
),
|
||||||
),
|
const SliverToBoxAdapter(child: Divider(height: 1)),
|
||||||
if (_data != null)
|
if (_data != null)
|
||||||
PostCommentSliverList(
|
SliverToBoxAdapter(
|
||||||
key: _childListKey,
|
child: Container(
|
||||||
parentPostId: _data!.id,
|
constraints: const BoxConstraints(maxWidth: 640),
|
||||||
maxWidth: 640,
|
child: Row(
|
||||||
),
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)),
|
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).center(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_data != null && ua.isAuthorized)
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Container(
|
||||||
|
height: 240,
|
||||||
|
constraints: const BoxConstraints(maxWidth: 640),
|
||||||
|
margin:
|
||||||
|
ResponsiveBreakpoints.of(context).largerThan(MOBILE) ? const EdgeInsets.all(8) : EdgeInsets.zero,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
|
||||||
|
? const BorderRadius.all(Radius.circular(8))
|
||||||
|
: BorderRadius.zero,
|
||||||
|
border: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
|
||||||
|
? Border.all(
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
width: 1 / devicePixelRatio,
|
||||||
|
)
|
||||||
|
: Border.symmetric(
|
||||||
|
horizontal: BorderSide(
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
width: 1 / devicePixelRatio,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: PostMiniEditor(
|
||||||
|
postReplyId: _data!.id,
|
||||||
|
onPost: () {
|
||||||
|
setState(() {
|
||||||
|
_data = _data!.copyWith(
|
||||||
|
metric: _data!.metric.copyWith(
|
||||||
|
replyCount: _data!.metric.replyCount + 1,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
_childListKey.currentState!.refresh();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).center(),
|
||||||
|
),
|
||||||
|
if (_data != null)
|
||||||
|
PostCommentSliverList(
|
||||||
|
key: _childListKey,
|
||||||
|
parentPostId: _data!.id,
|
||||||
|
maxWidth: 640,
|
||||||
|
),
|
||||||
|
SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import 'package:surface/providers/sn_network.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';
|
||||||
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
import 'package:surface/widgets/post/post_item.dart';
|
import 'package:surface/widgets/post/post_item.dart';
|
||||||
import 'package:surface/widgets/post/post_media_pending_list.dart';
|
import 'package:surface/widgets/post/post_media_pending_list.dart';
|
||||||
import 'package:surface/widgets/post/post_meta_editor.dart';
|
import 'package:surface/widgets/post/post_meta_editor.dart';
|
||||||
@@ -54,7 +55,9 @@ class PostEditorScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _PostEditorScreenState extends State<PostEditorScreen> {
|
class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||||
final PostWriteController _writeController = PostWriteController();
|
late final PostWriteController _writeController = PostWriteController(
|
||||||
|
doLoadFromTemporary: widget.postEditId == null,
|
||||||
|
);
|
||||||
|
|
||||||
bool _isFetching = false;
|
bool _isFetching = false;
|
||||||
|
|
||||||
@@ -126,7 +129,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
|||||||
return ListenableBuilder(
|
return ListenableBuilder(
|
||||||
listenable: _writeController,
|
listenable: _writeController,
|
||||||
builder: (context, _) {
|
builder: (context, _) {
|
||||||
return Scaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: BackButton(
|
leading: BackButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@@ -301,19 +304,22 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
// Content Input Area
|
// Content Input Area
|
||||||
TextField(
|
Container(
|
||||||
controller: _writeController.contentController,
|
constraints: const BoxConstraints(maxWidth: 640),
|
||||||
maxLines: null,
|
child: TextField(
|
||||||
decoration: InputDecoration(
|
controller: _writeController.contentController,
|
||||||
hintText: 'fieldPostContent'.tr(),
|
maxLines: null,
|
||||||
hintStyle: TextStyle(fontSize: 14),
|
decoration: InputDecoration(
|
||||||
isCollapsed: true,
|
hintText: 'fieldPostContent'.tr(),
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
hintStyle: TextStyle(fontSize: 14),
|
||||||
horizontal: 16,
|
isCollapsed: true,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
),
|
||||||
|
border: InputBorder.none,
|
||||||
),
|
),
|
||||||
border: InputBorder.none,
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
),
|
),
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
.expandIndexed(
|
.expandIndexed(
|
||||||
@@ -373,6 +379,36 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
|||||||
)
|
)
|
||||||
else if (_writeController.isBusy)
|
else if (_writeController.isBusy)
|
||||||
const LinearProgressIndicator(value: null, minHeight: 2),
|
const LinearProgressIndicator(value: null, minHeight: 2),
|
||||||
|
Container(
|
||||||
|
child: _writeController.temporaryRestored
|
||||||
|
? Container(
|
||||||
|
padding: const EdgeInsets.only(top: 4, bottom: 4, left: 28, right: 22),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
bottom: BorderSide(
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
width: 1 / MediaQuery.of(context).devicePixelRatio,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.restore, size: 20),
|
||||||
|
const Gap(8),
|
||||||
|
Expanded(child: Text('postLocalDraftRestored').tr()),
|
||||||
|
InkWell(
|
||||||
|
child: Text('dialogDismiss').tr(),
|
||||||
|
onTap: () {
|
||||||
|
_writeController.reset();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
))
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
)
|
||||||
|
.height(_writeController.temporaryRestored ? 32 : 0, animate: true)
|
||||||
|
.animate(const Duration(milliseconds: 300), Curves.fastLinearToSlowEaseIn),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'package:styled_widget/styled_widget.dart';
|
|||||||
import 'package:surface/providers/post.dart';
|
import 'package:surface/providers/post.dart';
|
||||||
import 'package:surface/types/post.dart';
|
import 'package:surface/types/post.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
import 'package:surface/widgets/post/post_item.dart';
|
import 'package:surface/widgets/post/post_item.dart';
|
||||||
import 'package:surface/widgets/post/post_tags_field.dart';
|
import 'package:surface/widgets/post/post_tags_field.dart';
|
||||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||||
@@ -119,7 +120,7 @@ class _PostSearchScreenState extends State<PostSearchScreen> {
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
return Scaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text('screenPostSearch').tr(),
|
title: Text('screenPostSearch').tr(),
|
||||||
actions: [
|
actions: [
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import 'package:surface/types/post.dart';
|
|||||||
import 'package:surface/types/realm.dart';
|
import 'package:surface/types/realm.dart';
|
||||||
import 'package:surface/widgets/account/account_image.dart';
|
import 'package:surface/widgets/account/account_image.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
import 'package:surface/widgets/post/post_item.dart';
|
import 'package:surface/widgets/post/post_item.dart';
|
||||||
import 'package:surface/widgets/universal_image.dart';
|
import 'package:surface/widgets/universal_image.dart';
|
||||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||||
@@ -274,7 +275,7 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
|||||||
|
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
|
||||||
return Scaffold(
|
return AppScaffold(
|
||||||
body: NestedScrollView(
|
body: NestedScrollView(
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import 'package:surface/widgets/account/account_image.dart';
|
|||||||
import 'package:surface/widgets/app_bar_leading.dart';
|
import 'package:surface/widgets/app_bar_leading.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/loading_indicator.dart';
|
import 'package:surface/widgets/loading_indicator.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
import 'package:surface/widgets/unauthorized_hint.dart';
|
import 'package:surface/widgets/unauthorized_hint.dart';
|
||||||
import 'package:surface/widgets/universal_image.dart';
|
import 'package:surface/widgets/universal_image.dart';
|
||||||
|
|
||||||
@@ -83,7 +84,7 @@ class _RealmScreenState extends State<RealmScreen> {
|
|||||||
final ua = context.read<UserProvider>();
|
final ua = context.read<UserProvider>();
|
||||||
|
|
||||||
if (!ua.isAuthorized) {
|
if (!ua.isAuthorized) {
|
||||||
return Scaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: AutoAppBarLeading(),
|
leading: AutoAppBarLeading(),
|
||||||
title: Text('screenRealm').tr(),
|
title: Text('screenRealm').tr(),
|
||||||
@@ -94,7 +95,7 @@ class _RealmScreenState extends State<RealmScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: AutoAppBarLeading(),
|
leading: AutoAppBarLeading(),
|
||||||
title: Text('screenRealm').tr(),
|
title: Text('screenRealm').tr(),
|
||||||
@@ -118,113 +119,61 @@ class _RealmScreenState extends State<RealmScreen> {
|
|||||||
children: [
|
children: [
|
||||||
LoadingIndicator(isActive: _isBusy),
|
LoadingIndicator(isActive: _isBusy),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: RefreshIndicator(
|
child: MediaQuery.removePadding(
|
||||||
onRefresh: _fetchRealms,
|
context: context,
|
||||||
child: ListView.builder(
|
removeTop: true,
|
||||||
itemCount: _realms?.length ?? 0,
|
child: RefreshIndicator(
|
||||||
itemBuilder: (context, idx) {
|
onRefresh: _fetchRealms,
|
||||||
final realm = _realms![idx];
|
child: ListView.builder(
|
||||||
if (_isCompactView) {
|
itemCount: _realms?.length ?? 0,
|
||||||
return ListTile(
|
itemBuilder: (context, idx) {
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
final realm = _realms![idx];
|
||||||
leading: AccountImage(
|
if (_isCompactView) {
|
||||||
content: realm.avatar,
|
return ListTile(
|
||||||
fallbackWidget: const Icon(Symbols.group, size: 20),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
),
|
leading: AccountImage(
|
||||||
title: Text(realm.name),
|
content: realm.avatar,
|
||||||
subtitle: Text(
|
fallbackWidget: const Icon(Symbols.group, size: 20),
|
||||||
realm.description,
|
),
|
||||||
maxLines: 1,
|
title: Text(realm.name),
|
||||||
overflow: TextOverflow.ellipsis,
|
subtitle: Text(
|
||||||
),
|
realm.description,
|
||||||
trailing: PopupMenuButton(
|
maxLines: 1,
|
||||||
itemBuilder: (BuildContext context) => [
|
overflow: TextOverflow.ellipsis,
|
||||||
PopupMenuItem(
|
),
|
||||||
child: Row(
|
trailing: PopupMenuButton(
|
||||||
children: [
|
itemBuilder: (BuildContext context) => [
|
||||||
const Icon(Symbols.edit),
|
PopupMenuItem(
|
||||||
const Gap(16),
|
child: Row(
|
||||||
Text('edit').tr(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
GoRouter.of(context).pushNamed(
|
|
||||||
'realmManage',
|
|
||||||
queryParameters: {'editing': realm.alias},
|
|
||||||
).then((value) {
|
|
||||||
if (value != null) {
|
|
||||||
_fetchRealms();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
PopupMenuItem(
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Symbols.delete),
|
|
||||||
const Gap(16),
|
|
||||||
Text('delete').tr(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
_deleteRealm(realm);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
GoRouter.of(context).pushNamed(
|
|
||||||
'realmDetail',
|
|
||||||
pathParameters: {'alias': realm.alias},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
constraints: BoxConstraints(maxWidth: 640),
|
|
||||||
child: Card(
|
|
||||||
margin: const EdgeInsets.all(12),
|
|
||||||
child: InkWell(
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
AspectRatio(
|
|
||||||
aspectRatio: 16 / 7,
|
|
||||||
child: Stack(
|
|
||||||
clipBehavior: Clip.none,
|
|
||||||
fit: StackFit.expand,
|
|
||||||
children: [
|
children: [
|
||||||
Container(
|
const Icon(Symbols.edit),
|
||||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
const Gap(16),
|
||||||
child: (realm.banner?.isEmpty ?? true)
|
Text('edit').tr(),
|
||||||
? const SizedBox.shrink()
|
|
||||||
: AutoResizeUniversalImage(
|
|
||||||
sn.getAttachmentUrl(realm.banner!),
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
bottom: -30,
|
|
||||||
left: 18,
|
|
||||||
child: AccountImage(
|
|
||||||
content: realm.avatar,
|
|
||||||
radius: 24,
|
|
||||||
fallbackWidget: const Icon(Symbols.group, size: 24),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
onTap: () {
|
||||||
|
GoRouter.of(context).pushNamed(
|
||||||
|
'realmManage',
|
||||||
|
queryParameters: {'editing': realm.alias},
|
||||||
|
).then((value) {
|
||||||
|
if (value != null) {
|
||||||
|
_fetchRealms();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.delete),
|
||||||
|
const Gap(16),
|
||||||
|
Text('delete').tr(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
_deleteRealm(realm);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
const Gap(20 + 12),
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(realm.name).textStyle(Theme.of(context).textTheme.titleMedium!),
|
|
||||||
Text(realm.description).textStyle(Theme.of(context).textTheme.bodySmall!),
|
|
||||||
],
|
|
||||||
).padding(horizontal: 24, bottom: 14),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
@@ -233,10 +182,69 @@ class _RealmScreenState extends State<RealmScreen> {
|
|||||||
pathParameters: {'alias': realm.alias},
|
pathParameters: {'alias': realm.alias},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
constraints: BoxConstraints(maxWidth: 640),
|
||||||
|
child: Card(
|
||||||
|
margin: const EdgeInsets.all(12),
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
AspectRatio(
|
||||||
|
aspectRatio: 16 / 7,
|
||||||
|
child: Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
child: Container(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
child: (realm.banner?.isEmpty ?? true)
|
||||||
|
? const SizedBox.shrink()
|
||||||
|
: AutoResizeUniversalImage(
|
||||||
|
sn.getAttachmentUrl(realm.banner!),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
bottom: -30,
|
||||||
|
left: 18,
|
||||||
|
child: AccountImage(
|
||||||
|
content: realm.avatar,
|
||||||
|
radius: 24,
|
||||||
|
fallbackWidget: const Icon(Symbols.group, size: 24),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(20 + 12),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(realm.name).textStyle(Theme.of(context).textTheme.titleMedium!),
|
||||||
|
Text(realm.description).textStyle(Theme.of(context).textTheme.bodySmall!),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 24, bottom: 14),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
GoRouter.of(context).pushNamed(
|
||||||
|
'realmDetail',
|
||||||
|
pathParameters: {'alias': realm.alias},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
).center();
|
||||||
).center();
|
},
|
||||||
},
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import 'package:surface/types/realm.dart';
|
|||||||
import 'package:surface/widgets/account/account_image.dart';
|
import 'package:surface/widgets/account/account_image.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/loading_indicator.dart';
|
import 'package:surface/widgets/loading_indicator.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
import 'package:surface/widgets/universal_image.dart';
|
import 'package:surface/widgets/universal_image.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
@@ -179,7 +180,7 @@ class _RealmManageScreenState extends State<RealmManageScreen> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
|
||||||
return Scaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: widget.editingRealmAlias != null
|
title: widget.editingRealmAlias != null
|
||||||
? Text('screenRealmManage').tr()
|
? Text('screenRealmManage').tr()
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ import 'package:styled_widget/styled_widget.dart';
|
|||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
import 'package:surface/providers/user_directory.dart';
|
import 'package:surface/providers/user_directory.dart';
|
||||||
import 'package:surface/providers/userinfo.dart';
|
import 'package:surface/providers/userinfo.dart';
|
||||||
|
import 'package:surface/types/post.dart';
|
||||||
import 'package:surface/types/realm.dart';
|
import 'package:surface/types/realm.dart';
|
||||||
import 'package:surface/widgets/account/account_image.dart';
|
import 'package:surface/widgets/account/account_image.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||||
|
|
||||||
import '../../types/post.dart';
|
|
||||||
|
|
||||||
class RealmDetailScreen extends StatefulWidget {
|
class RealmDetailScreen extends StatefulWidget {
|
||||||
final String alias;
|
final String alias;
|
||||||
|
|
||||||
@@ -70,27 +70,19 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return DefaultTabController(
|
return DefaultTabController(
|
||||||
length: 3,
|
length: 3,
|
||||||
child: Scaffold(
|
child: AppScaffold(
|
||||||
body: NestedScrollView(
|
body: NestedScrollView(
|
||||||
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
||||||
// These are the slivers that show up in the "outer" scroll view.
|
|
||||||
return <Widget>[
|
return <Widget>[
|
||||||
SliverOverlapAbsorber(
|
SliverOverlapAbsorber(
|
||||||
// This widget takes the overlapping behavior of the SliverAppBar,
|
|
||||||
// and redirects it to the SliverOverlapInjector below. If it is
|
|
||||||
// missing, then it is possible for the nested "inner" scroll view
|
|
||||||
// below to end up under the SliverAppBar even when the inner
|
|
||||||
// scroll view thinks it has not been scrolled.
|
|
||||||
// This is not necessary if the "headerSliverBuilder" only builds
|
|
||||||
// widgets that do not overlap the next sliver.
|
|
||||||
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
||||||
sliver: SliverAppBar(
|
sliver: SliverAppBar(
|
||||||
title: Text(_realm?.name ?? 'loading'.tr()),
|
title: Text(_realm?.name ?? 'loading'.tr()),
|
||||||
bottom: TabBar(
|
bottom: TabBar(
|
||||||
tabs: [
|
tabs: [
|
||||||
Tab(icon: const Icon(Symbols.home)),
|
Tab(icon: Icon(Symbols.home, color: Theme.of(context).appBarTheme.foregroundColor)),
|
||||||
Tab(icon: const Icon(Symbols.group)),
|
Tab(icon: Icon(Symbols.group, color: Theme.of(context).appBarTheme.foregroundColor)),
|
||||||
Tab(icon: const Icon(Symbols.settings)),
|
Tab(icon: Icon(Symbols.settings, color: Theme.of(context).appBarTheme.foregroundColor)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -428,7 +420,7 @@ class _RealmSettingsWidgetState extends State<_RealmSettingsWidget> {
|
|||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
const Gap(16),
|
const Gap(8),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Symbols.edit),
|
leading: const Icon(Symbols.edit),
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import 'package:surface/providers/sn_network.dart';
|
|||||||
import 'package:surface/providers/theme.dart';
|
import 'package:surface/providers/theme.dart';
|
||||||
import 'package:surface/theme.dart';
|
import 'package:surface/theme.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
|
|
||||||
const Map<String, Color> kColorSchemes = {
|
const Map<String, Color> kColorSchemes = {
|
||||||
'colorSchemeIndigo': Colors.indigo,
|
'colorSchemeIndigo': Colors.indigo,
|
||||||
@@ -67,7 +68,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
|
||||||
return Scaffold(
|
return AppScaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: const PageBackButton(),
|
||||||
|
title: Text('screenSettings').tr(),
|
||||||
|
),
|
||||||
body: SingleChildScrollView(
|
body: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
spacing: 16,
|
spacing: 16,
|
||||||
@@ -120,7 +125,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
subtitle: Text('settingsThemeMaterial3Description').tr(),
|
subtitle: Text('settingsThemeMaterial3Description').tr(),
|
||||||
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
||||||
secondary: const Icon(Symbols.new_releases),
|
secondary: const Icon(Symbols.new_releases),
|
||||||
value: _prefs.getBool(kMaterialYouToggleStoreKey) ?? false,
|
value: _prefs.getBool(kMaterialYouToggleStoreKey) ?? true,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_prefs.setBool(
|
_prefs.setBool(
|
||||||
@@ -240,6 +245,61 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
CheckboxListTile(
|
||||||
|
secondary: const Icon(Symbols.left_panel_close),
|
||||||
|
title: Text('settingsDrawerPreferCollapse').tr(),
|
||||||
|
subtitle: Text('settingsDrawerPreferCollapseDescription').tr(),
|
||||||
|
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
||||||
|
value: _prefs.getBool(kAppDrawerPreferCollapse) ?? false,
|
||||||
|
onChanged: (value) {
|
||||||
|
_prefs.setBool(kAppDrawerPreferCollapse, value ?? false);
|
||||||
|
final cfg = context.read<ConfigProvider>();
|
||||||
|
cfg.calcDrawerSize(context);
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('settingsFeatures').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
|
||||||
|
CheckboxListTile(
|
||||||
|
secondary: const Icon(Symbols.vibration),
|
||||||
|
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
||||||
|
title: Text('settingsNotifyWithHaptic').tr(),
|
||||||
|
subtitle: Text('settingsNotifyWithHapticDescription').tr(),
|
||||||
|
value: _prefs.getBool(kAppNotifyWithHaptic) ?? true,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_prefs.setBool(kAppNotifyWithHaptic, value ?? false);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
CheckboxListTile(
|
||||||
|
secondary: const Icon(Symbols.link),
|
||||||
|
title: Text('settingsExpandPostLink').tr(),
|
||||||
|
subtitle: Text('settingsExpandPostLinkDescription').tr(),
|
||||||
|
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
||||||
|
value: _prefs.getBool(kAppExpandPostLink) ?? true,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_prefs.setBool(kAppExpandPostLink, value ?? false);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
CheckboxListTile(
|
||||||
|
secondary: const Icon(Symbols.chat),
|
||||||
|
title: Text('settingsExpandChatLink').tr(),
|
||||||
|
subtitle: Text('settingsExpandChatLinkDescription').tr(),
|
||||||
|
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
||||||
|
value: _prefs.getBool(kAppExpandChatLink) ?? true,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_prefs.setBool(kAppExpandChatLink, value ?? false);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Column(
|
Column(
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ Future<ThemeSet> createAppThemeSet({Color? seedColorOverride, bool? useMaterial3
|
|||||||
|
|
||||||
Future<ThemeData> createAppTheme(
|
Future<ThemeData> createAppTheme(
|
||||||
Brightness brightness, {
|
Brightness brightness, {
|
||||||
Color? seedColorOverride,
|
Color? seedColorOverride,
|
||||||
bool? useMaterial3,
|
bool? useMaterial3,
|
||||||
}) async {
|
}) async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
@@ -34,9 +34,10 @@ Future<ThemeData> createAppTheme(
|
|||||||
);
|
);
|
||||||
|
|
||||||
final hasAppBarBlurry = prefs.getBool(kAppbarTransparentStoreKey) ?? false;
|
final hasAppBarBlurry = prefs.getBool(kAppbarTransparentStoreKey) ?? false;
|
||||||
|
final useM3 = useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? true);
|
||||||
|
|
||||||
return ThemeData(
|
return ThemeData(
|
||||||
useMaterial3: useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? false),
|
useMaterial3: useM3,
|
||||||
colorScheme: colorScheme,
|
colorScheme: colorScheme,
|
||||||
brightness: brightness,
|
brightness: brightness,
|
||||||
iconTheme: IconThemeData(
|
iconTheme: IconThemeData(
|
||||||
@@ -45,12 +46,24 @@ Future<ThemeData> createAppTheme(
|
|||||||
opticalSize: 20,
|
opticalSize: 20,
|
||||||
color: colorScheme.onSurface,
|
color: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
|
snackBarTheme: SnackBarThemeData(
|
||||||
|
behavior: useM3 ? SnackBarBehavior.floating : SnackBarBehavior.fixed,
|
||||||
|
),
|
||||||
appBarTheme: AppBarTheme(
|
appBarTheme: AppBarTheme(
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
elevation: hasAppBarBlurry ? 0 : null,
|
elevation: hasAppBarBlurry ? 0 : null,
|
||||||
backgroundColor: hasAppBarBlurry ? colorScheme.surfaceContainer.withAlpha(200) : colorScheme.primary,
|
backgroundColor: hasAppBarBlurry ? colorScheme.primary.withOpacity(0.3) : colorScheme.primary,
|
||||||
foregroundColor: hasAppBarBlurry ? colorScheme.onSurface : colorScheme.onPrimary,
|
foregroundColor: hasAppBarBlurry ? colorScheme.onSurface : colorScheme.onPrimary,
|
||||||
),
|
),
|
||||||
scaffoldBackgroundColor: Colors.transparent,
|
pageTransitionsTheme: PageTransitionsTheme(
|
||||||
|
builders: {
|
||||||
|
TargetPlatform.android: PredictiveBackPageTransitionsBuilder(),
|
||||||
|
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
|
||||||
|
TargetPlatform.macOS: ZoomPageTransitionsBuilder(),
|
||||||
|
TargetPlatform.fuchsia: ZoomPageTransitionsBuilder(),
|
||||||
|
TargetPlatform.linux: ZoomPageTransitionsBuilder(),
|
||||||
|
TargetPlatform.windows: ZoomPageTransitionsBuilder(),
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,12 +38,13 @@ class SnAttachment with _$SnAttachment {
|
|||||||
required SnAttachment? ref,
|
required SnAttachment? ref,
|
||||||
required int? refId,
|
required int? refId,
|
||||||
required SnAttachmentPool? pool,
|
required SnAttachmentPool? pool,
|
||||||
required int poolId,
|
required int? poolId,
|
||||||
required int accountId,
|
required int accountId,
|
||||||
int? thumbnailId,
|
int? thumbnailId,
|
||||||
SnAttachment? thumbnail,
|
SnAttachment? thumbnail,
|
||||||
int? compressedId,
|
int? compressedId,
|
||||||
SnAttachment? compressed,
|
SnAttachment? compressed,
|
||||||
|
@Default([]) List<SnAttachmentBoost> boosts,
|
||||||
@Default({}) Map<String, dynamic> usermeta,
|
@Default({}) Map<String, dynamic> usermeta,
|
||||||
@Default({}) Map<String, dynamic> metadata,
|
@Default({}) Map<String, dynamic> metadata,
|
||||||
}) = _SnAttachment;
|
}) = _SnAttachment;
|
||||||
@@ -110,3 +111,69 @@ class SnAttachmentPool with _$SnAttachmentPool {
|
|||||||
|
|
||||||
factory SnAttachmentPool.fromJson(Map<String, Object?> json) => _$SnAttachmentPoolFromJson(json);
|
factory SnAttachmentPool.fromJson(Map<String, Object?> json) => _$SnAttachmentPoolFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class SnAttachmentDestination with _$SnAttachmentDestination {
|
||||||
|
const factory SnAttachmentDestination({
|
||||||
|
@Default(0) int id,
|
||||||
|
required String type,
|
||||||
|
required String label,
|
||||||
|
required String region,
|
||||||
|
required bool isBoost,
|
||||||
|
}) = _SnAttachmentDestination;
|
||||||
|
|
||||||
|
factory SnAttachmentDestination.fromJson(Map<String, Object?> json) => _$SnAttachmentDestinationFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class SnAttachmentBoost with _$SnAttachmentBoost {
|
||||||
|
const factory SnAttachmentBoost({
|
||||||
|
required int id,
|
||||||
|
required DateTime createdAt,
|
||||||
|
required DateTime updatedAt,
|
||||||
|
required DateTime? deletedAt,
|
||||||
|
required int status,
|
||||||
|
required int destination,
|
||||||
|
required int attachmentId,
|
||||||
|
required SnAttachment attachment,
|
||||||
|
required int account,
|
||||||
|
}) = _SnAttachmentBoost;
|
||||||
|
|
||||||
|
factory SnAttachmentBoost.fromJson(Map<String, Object?> json) => _$SnAttachmentBoostFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class SnSticker with _$SnSticker {
|
||||||
|
const factory SnSticker({
|
||||||
|
required int id,
|
||||||
|
required DateTime createdAt,
|
||||||
|
required DateTime updatedAt,
|
||||||
|
required DateTime? deletedAt,
|
||||||
|
required String alias,
|
||||||
|
required String name,
|
||||||
|
required int attachmentId,
|
||||||
|
required SnAttachment attachment,
|
||||||
|
required int packId,
|
||||||
|
required SnStickerPack pack,
|
||||||
|
required int accountId,
|
||||||
|
}) = _SnSticker;
|
||||||
|
|
||||||
|
factory SnSticker.fromJson(Map<String, Object?> json) => _$SnStickerFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class SnStickerPack with _$SnStickerPack {
|
||||||
|
const factory SnStickerPack({
|
||||||
|
required int id,
|
||||||
|
required DateTime createdAt,
|
||||||
|
required DateTime updatedAt,
|
||||||
|
required DateTime? deletedAt,
|
||||||
|
required String prefix,
|
||||||
|
required String name,
|
||||||
|
required String description,
|
||||||
|
required List<SnSticker>? stickers,
|
||||||
|
required int accountId,
|
||||||
|
}) = _SnStickerPack;
|
||||||
|
|
||||||
|
factory SnStickerPack.fromJson(Map<String, Object?> json) => _$SnStickerPackFromJson(json);
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -38,7 +38,7 @@ _$SnAttachmentImpl _$$SnAttachmentImplFromJson(Map<String, dynamic> json) =>
|
|||||||
pool: json['pool'] == null
|
pool: json['pool'] == null
|
||||||
? null
|
? null
|
||||||
: SnAttachmentPool.fromJson(json['pool'] as Map<String, dynamic>),
|
: SnAttachmentPool.fromJson(json['pool'] as Map<String, dynamic>),
|
||||||
poolId: (json['pool_id'] as num).toInt(),
|
poolId: (json['pool_id'] as num?)?.toInt(),
|
||||||
accountId: (json['account_id'] as num).toInt(),
|
accountId: (json['account_id'] as num).toInt(),
|
||||||
thumbnailId: (json['thumbnail_id'] as num?)?.toInt(),
|
thumbnailId: (json['thumbnail_id'] as num?)?.toInt(),
|
||||||
thumbnail: json['thumbnail'] == null
|
thumbnail: json['thumbnail'] == null
|
||||||
@@ -48,6 +48,11 @@ _$SnAttachmentImpl _$$SnAttachmentImplFromJson(Map<String, dynamic> json) =>
|
|||||||
compressed: json['compressed'] == null
|
compressed: json['compressed'] == null
|
||||||
? null
|
? null
|
||||||
: SnAttachment.fromJson(json['compressed'] as Map<String, dynamic>),
|
: SnAttachment.fromJson(json['compressed'] as Map<String, dynamic>),
|
||||||
|
boosts: (json['boosts'] as List<dynamic>?)
|
||||||
|
?.map(
|
||||||
|
(e) => SnAttachmentBoost.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList() ??
|
||||||
|
const [],
|
||||||
usermeta: json['usermeta'] as Map<String, dynamic>? ?? const {},
|
usermeta: json['usermeta'] as Map<String, dynamic>? ?? const {},
|
||||||
metadata: json['metadata'] as Map<String, dynamic>? ?? const {},
|
metadata: json['metadata'] as Map<String, dynamic>? ?? const {},
|
||||||
);
|
);
|
||||||
@@ -82,6 +87,7 @@ Map<String, dynamic> _$$SnAttachmentImplToJson(_$SnAttachmentImpl instance) =>
|
|||||||
'thumbnail': instance.thumbnail?.toJson(),
|
'thumbnail': instance.thumbnail?.toJson(),
|
||||||
'compressed_id': instance.compressedId,
|
'compressed_id': instance.compressedId,
|
||||||
'compressed': instance.compressed?.toJson(),
|
'compressed': instance.compressed?.toJson(),
|
||||||
|
'boosts': instance.boosts.map((e) => e.toJson()).toList(),
|
||||||
'usermeta': instance.usermeta,
|
'usermeta': instance.usermeta,
|
||||||
'metadata': instance.metadata,
|
'metadata': instance.metadata,
|
||||||
};
|
};
|
||||||
@@ -161,3 +167,117 @@ Map<String, dynamic> _$$SnAttachmentPoolImplToJson(
|
|||||||
'config': instance.config,
|
'config': instance.config,
|
||||||
'account_id': instance.accountId,
|
'account_id': instance.accountId,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_$SnAttachmentDestinationImpl _$$SnAttachmentDestinationImplFromJson(
|
||||||
|
Map<String, dynamic> json) =>
|
||||||
|
_$SnAttachmentDestinationImpl(
|
||||||
|
id: (json['id'] as num?)?.toInt() ?? 0,
|
||||||
|
type: json['type'] as String,
|
||||||
|
label: json['label'] as String,
|
||||||
|
region: json['region'] as String,
|
||||||
|
isBoost: json['is_boost'] as bool,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$SnAttachmentDestinationImplToJson(
|
||||||
|
_$SnAttachmentDestinationImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'type': instance.type,
|
||||||
|
'label': instance.label,
|
||||||
|
'region': instance.region,
|
||||||
|
'is_boost': instance.isBoost,
|
||||||
|
};
|
||||||
|
|
||||||
|
_$SnAttachmentBoostImpl _$$SnAttachmentBoostImplFromJson(
|
||||||
|
Map<String, dynamic> json) =>
|
||||||
|
_$SnAttachmentBoostImpl(
|
||||||
|
id: (json['id'] as num).toInt(),
|
||||||
|
createdAt: DateTime.parse(json['created_at'] as String),
|
||||||
|
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||||
|
deletedAt: json['deleted_at'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['deleted_at'] as String),
|
||||||
|
status: (json['status'] as num).toInt(),
|
||||||
|
destination: (json['destination'] as num).toInt(),
|
||||||
|
attachmentId: (json['attachment_id'] as num).toInt(),
|
||||||
|
attachment:
|
||||||
|
SnAttachment.fromJson(json['attachment'] as Map<String, dynamic>),
|
||||||
|
account: (json['account'] as num).toInt(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$SnAttachmentBoostImplToJson(
|
||||||
|
_$SnAttachmentBoostImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'created_at': instance.createdAt.toIso8601String(),
|
||||||
|
'updated_at': instance.updatedAt.toIso8601String(),
|
||||||
|
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||||
|
'status': instance.status,
|
||||||
|
'destination': instance.destination,
|
||||||
|
'attachment_id': instance.attachmentId,
|
||||||
|
'attachment': instance.attachment.toJson(),
|
||||||
|
'account': instance.account,
|
||||||
|
};
|
||||||
|
|
||||||
|
_$SnStickerImpl _$$SnStickerImplFromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SnStickerImpl(
|
||||||
|
id: (json['id'] as num).toInt(),
|
||||||
|
createdAt: DateTime.parse(json['created_at'] as String),
|
||||||
|
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||||
|
deletedAt: json['deleted_at'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['deleted_at'] as String),
|
||||||
|
alias: json['alias'] as String,
|
||||||
|
name: json['name'] as String,
|
||||||
|
attachmentId: (json['attachment_id'] as num).toInt(),
|
||||||
|
attachment:
|
||||||
|
SnAttachment.fromJson(json['attachment'] as Map<String, dynamic>),
|
||||||
|
packId: (json['pack_id'] as num).toInt(),
|
||||||
|
pack: SnStickerPack.fromJson(json['pack'] as Map<String, dynamic>),
|
||||||
|
accountId: (json['account_id'] as num).toInt(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$SnStickerImplToJson(_$SnStickerImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'created_at': instance.createdAt.toIso8601String(),
|
||||||
|
'updated_at': instance.updatedAt.toIso8601String(),
|
||||||
|
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||||
|
'alias': instance.alias,
|
||||||
|
'name': instance.name,
|
||||||
|
'attachment_id': instance.attachmentId,
|
||||||
|
'attachment': instance.attachment.toJson(),
|
||||||
|
'pack_id': instance.packId,
|
||||||
|
'pack': instance.pack.toJson(),
|
||||||
|
'account_id': instance.accountId,
|
||||||
|
};
|
||||||
|
|
||||||
|
_$SnStickerPackImpl _$$SnStickerPackImplFromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SnStickerPackImpl(
|
||||||
|
id: (json['id'] as num).toInt(),
|
||||||
|
createdAt: DateTime.parse(json['created_at'] as String),
|
||||||
|
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||||
|
deletedAt: json['deleted_at'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['deleted_at'] as String),
|
||||||
|
prefix: json['prefix'] as String,
|
||||||
|
name: json['name'] as String,
|
||||||
|
description: json['description'] as String,
|
||||||
|
stickers: (json['stickers'] as List<dynamic>?)
|
||||||
|
?.map((e) => SnSticker.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
accountId: (json['account_id'] as num).toInt(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$SnStickerPackImplToJson(_$SnStickerPackImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'created_at': instance.createdAt.toIso8601String(),
|
||||||
|
'updated_at': instance.updatedAt.toIso8601String(),
|
||||||
|
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||||
|
'prefix': instance.prefix,
|
||||||
|
'name': instance.name,
|
||||||
|
'description': instance.description,
|
||||||
|
'stickers': instance.stickers?.map((e) => e.toJson()).toList(),
|
||||||
|
'account_id': instance.accountId,
|
||||||
|
};
|
||||||
|
|||||||
38
lib/types/news.dart
Normal file
38
lib/types/news.dart
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'news.freezed.dart';
|
||||||
|
part 'news.g.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class SnNewsSource with _$SnNewsSource {
|
||||||
|
const factory SnNewsSource({
|
||||||
|
required String id,
|
||||||
|
required String label,
|
||||||
|
required String type,
|
||||||
|
required String source,
|
||||||
|
required int depth,
|
||||||
|
required bool enabled,
|
||||||
|
}) = _SnNewsSource;
|
||||||
|
|
||||||
|
factory SnNewsSource.fromJson(Map<String, dynamic> json) => _$SnNewsSourceFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class SnNewsArticle with _$SnNewsArticle {
|
||||||
|
const factory SnNewsArticle({
|
||||||
|
required int id,
|
||||||
|
required DateTime createdAt,
|
||||||
|
required DateTime updatedAt,
|
||||||
|
required dynamic deletedAt,
|
||||||
|
required String thumbnail,
|
||||||
|
required String title,
|
||||||
|
required String description,
|
||||||
|
required String content,
|
||||||
|
required String url,
|
||||||
|
required String hash,
|
||||||
|
required String source,
|
||||||
|
required DateTime? publishedAt,
|
||||||
|
}) = _SnNewsArticle;
|
||||||
|
|
||||||
|
factory SnNewsArticle.fromJson(Map<String, dynamic> json) => _$SnNewsArticleFromJson(json);
|
||||||
|
}
|
||||||
660
lib/types/news.freezed.dart
Normal file
660
lib/types/news.freezed.dart
Normal file
@@ -0,0 +1,660 @@
|
|||||||
|
// coverage:ignore-file
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||||
|
|
||||||
|
part of 'news.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// FreezedGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
T _$identity<T>(T value) => value;
|
||||||
|
|
||||||
|
final _privateConstructorUsedError = UnsupportedError(
|
||||||
|
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||||
|
|
||||||
|
SnNewsSource _$SnNewsSourceFromJson(Map<String, dynamic> json) {
|
||||||
|
return _SnNewsSource.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$SnNewsSource {
|
||||||
|
String get id => throw _privateConstructorUsedError;
|
||||||
|
String get label => throw _privateConstructorUsedError;
|
||||||
|
String get type => throw _privateConstructorUsedError;
|
||||||
|
String get source => throw _privateConstructorUsedError;
|
||||||
|
int get depth => throw _privateConstructorUsedError;
|
||||||
|
bool get enabled => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Serializes this SnNewsSource to a JSON map.
|
||||||
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Create a copy of SnNewsSource
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
$SnNewsSourceCopyWith<SnNewsSource> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class $SnNewsSourceCopyWith<$Res> {
|
||||||
|
factory $SnNewsSourceCopyWith(
|
||||||
|
SnNewsSource value, $Res Function(SnNewsSource) then) =
|
||||||
|
_$SnNewsSourceCopyWithImpl<$Res, SnNewsSource>;
|
||||||
|
@useResult
|
||||||
|
$Res call(
|
||||||
|
{String id,
|
||||||
|
String label,
|
||||||
|
String type,
|
||||||
|
String source,
|
||||||
|
int depth,
|
||||||
|
bool enabled});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class _$SnNewsSourceCopyWithImpl<$Res, $Val extends SnNewsSource>
|
||||||
|
implements $SnNewsSourceCopyWith<$Res> {
|
||||||
|
_$SnNewsSourceCopyWithImpl(this._value, this._then);
|
||||||
|
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Val _value;
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Res Function($Val) _then;
|
||||||
|
|
||||||
|
/// Create a copy of SnNewsSource
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? id = null,
|
||||||
|
Object? label = null,
|
||||||
|
Object? type = null,
|
||||||
|
Object? source = null,
|
||||||
|
Object? depth = null,
|
||||||
|
Object? enabled = null,
|
||||||
|
}) {
|
||||||
|
return _then(_value.copyWith(
|
||||||
|
id: null == id
|
||||||
|
? _value.id
|
||||||
|
: id // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
label: null == label
|
||||||
|
? _value.label
|
||||||
|
: label // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
type: null == type
|
||||||
|
? _value.type
|
||||||
|
: type // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
source: null == source
|
||||||
|
? _value.source
|
||||||
|
: source // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
depth: null == depth
|
||||||
|
? _value.depth
|
||||||
|
: depth // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
enabled: null == enabled
|
||||||
|
? _value.enabled
|
||||||
|
: enabled // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,
|
||||||
|
) as $Val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class _$$SnNewsSourceImplCopyWith<$Res>
|
||||||
|
implements $SnNewsSourceCopyWith<$Res> {
|
||||||
|
factory _$$SnNewsSourceImplCopyWith(
|
||||||
|
_$SnNewsSourceImpl value, $Res Function(_$SnNewsSourceImpl) then) =
|
||||||
|
__$$SnNewsSourceImplCopyWithImpl<$Res>;
|
||||||
|
@override
|
||||||
|
@useResult
|
||||||
|
$Res call(
|
||||||
|
{String id,
|
||||||
|
String label,
|
||||||
|
String type,
|
||||||
|
String source,
|
||||||
|
int depth,
|
||||||
|
bool enabled});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class __$$SnNewsSourceImplCopyWithImpl<$Res>
|
||||||
|
extends _$SnNewsSourceCopyWithImpl<$Res, _$SnNewsSourceImpl>
|
||||||
|
implements _$$SnNewsSourceImplCopyWith<$Res> {
|
||||||
|
__$$SnNewsSourceImplCopyWithImpl(
|
||||||
|
_$SnNewsSourceImpl _value, $Res Function(_$SnNewsSourceImpl) _then)
|
||||||
|
: super(_value, _then);
|
||||||
|
|
||||||
|
/// Create a copy of SnNewsSource
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? id = null,
|
||||||
|
Object? label = null,
|
||||||
|
Object? type = null,
|
||||||
|
Object? source = null,
|
||||||
|
Object? depth = null,
|
||||||
|
Object? enabled = null,
|
||||||
|
}) {
|
||||||
|
return _then(_$SnNewsSourceImpl(
|
||||||
|
id: null == id
|
||||||
|
? _value.id
|
||||||
|
: id // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
label: null == label
|
||||||
|
? _value.label
|
||||||
|
: label // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
type: null == type
|
||||||
|
? _value.type
|
||||||
|
: type // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
source: null == source
|
||||||
|
? _value.source
|
||||||
|
: source // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
depth: null == depth
|
||||||
|
? _value.depth
|
||||||
|
: depth // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
enabled: null == enabled
|
||||||
|
? _value.enabled
|
||||||
|
: enabled // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
class _$SnNewsSourceImpl implements _SnNewsSource {
|
||||||
|
const _$SnNewsSourceImpl(
|
||||||
|
{required this.id,
|
||||||
|
required this.label,
|
||||||
|
required this.type,
|
||||||
|
required this.source,
|
||||||
|
required this.depth,
|
||||||
|
required this.enabled});
|
||||||
|
|
||||||
|
factory _$SnNewsSourceImpl.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$$SnNewsSourceImplFromJson(json);
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String id;
|
||||||
|
@override
|
||||||
|
final String label;
|
||||||
|
@override
|
||||||
|
final String type;
|
||||||
|
@override
|
||||||
|
final String source;
|
||||||
|
@override
|
||||||
|
final int depth;
|
||||||
|
@override
|
||||||
|
final bool enabled;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SnNewsSource(id: $id, label: $label, type: $type, source: $source, depth: $depth, enabled: $enabled)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) ||
|
||||||
|
(other.runtimeType == runtimeType &&
|
||||||
|
other is _$SnNewsSourceImpl &&
|
||||||
|
(identical(other.id, id) || other.id == id) &&
|
||||||
|
(identical(other.label, label) || other.label == label) &&
|
||||||
|
(identical(other.type, type) || other.type == type) &&
|
||||||
|
(identical(other.source, source) || other.source == source) &&
|
||||||
|
(identical(other.depth, depth) || other.depth == depth) &&
|
||||||
|
(identical(other.enabled, enabled) || other.enabled == enabled));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
Object.hash(runtimeType, id, label, type, source, depth, enabled);
|
||||||
|
|
||||||
|
/// Create a copy of SnNewsSource
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$$SnNewsSourceImplCopyWith<_$SnNewsSourceImpl> get copyWith =>
|
||||||
|
__$$SnNewsSourceImplCopyWithImpl<_$SnNewsSourceImpl>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$$SnNewsSourceImplToJson(
|
||||||
|
this,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _SnNewsSource implements SnNewsSource {
|
||||||
|
const factory _SnNewsSource(
|
||||||
|
{required final String id,
|
||||||
|
required final String label,
|
||||||
|
required final String type,
|
||||||
|
required final String source,
|
||||||
|
required final int depth,
|
||||||
|
required final bool enabled}) = _$SnNewsSourceImpl;
|
||||||
|
|
||||||
|
factory _SnNewsSource.fromJson(Map<String, dynamic> json) =
|
||||||
|
_$SnNewsSourceImpl.fromJson;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get id;
|
||||||
|
@override
|
||||||
|
String get label;
|
||||||
|
@override
|
||||||
|
String get type;
|
||||||
|
@override
|
||||||
|
String get source;
|
||||||
|
@override
|
||||||
|
int get depth;
|
||||||
|
@override
|
||||||
|
bool get enabled;
|
||||||
|
|
||||||
|
/// Create a copy of SnNewsSource
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
_$$SnNewsSourceImplCopyWith<_$SnNewsSourceImpl> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
SnNewsArticle _$SnNewsArticleFromJson(Map<String, dynamic> json) {
|
||||||
|
return _SnNewsArticle.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$SnNewsArticle {
|
||||||
|
int get id => throw _privateConstructorUsedError;
|
||||||
|
DateTime get createdAt => throw _privateConstructorUsedError;
|
||||||
|
DateTime get updatedAt => throw _privateConstructorUsedError;
|
||||||
|
dynamic get deletedAt => throw _privateConstructorUsedError;
|
||||||
|
String get thumbnail => throw _privateConstructorUsedError;
|
||||||
|
String get title => throw _privateConstructorUsedError;
|
||||||
|
String get description => throw _privateConstructorUsedError;
|
||||||
|
String get content => throw _privateConstructorUsedError;
|
||||||
|
String get url => throw _privateConstructorUsedError;
|
||||||
|
String get hash => throw _privateConstructorUsedError;
|
||||||
|
String get source => throw _privateConstructorUsedError;
|
||||||
|
DateTime? get publishedAt => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Serializes this SnNewsArticle to a JSON map.
|
||||||
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Create a copy of SnNewsArticle
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
$SnNewsArticleCopyWith<SnNewsArticle> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class $SnNewsArticleCopyWith<$Res> {
|
||||||
|
factory $SnNewsArticleCopyWith(
|
||||||
|
SnNewsArticle value, $Res Function(SnNewsArticle) then) =
|
||||||
|
_$SnNewsArticleCopyWithImpl<$Res, SnNewsArticle>;
|
||||||
|
@useResult
|
||||||
|
$Res call(
|
||||||
|
{int id,
|
||||||
|
DateTime createdAt,
|
||||||
|
DateTime updatedAt,
|
||||||
|
dynamic deletedAt,
|
||||||
|
String thumbnail,
|
||||||
|
String title,
|
||||||
|
String description,
|
||||||
|
String content,
|
||||||
|
String url,
|
||||||
|
String hash,
|
||||||
|
String source,
|
||||||
|
DateTime? publishedAt});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class _$SnNewsArticleCopyWithImpl<$Res, $Val extends SnNewsArticle>
|
||||||
|
implements $SnNewsArticleCopyWith<$Res> {
|
||||||
|
_$SnNewsArticleCopyWithImpl(this._value, this._then);
|
||||||
|
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Val _value;
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Res Function($Val) _then;
|
||||||
|
|
||||||
|
/// Create a copy of SnNewsArticle
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? id = null,
|
||||||
|
Object? createdAt = null,
|
||||||
|
Object? updatedAt = null,
|
||||||
|
Object? deletedAt = freezed,
|
||||||
|
Object? thumbnail = null,
|
||||||
|
Object? title = null,
|
||||||
|
Object? description = null,
|
||||||
|
Object? content = null,
|
||||||
|
Object? url = null,
|
||||||
|
Object? hash = null,
|
||||||
|
Object? source = null,
|
||||||
|
Object? publishedAt = freezed,
|
||||||
|
}) {
|
||||||
|
return _then(_value.copyWith(
|
||||||
|
id: null == id
|
||||||
|
? _value.id
|
||||||
|
: id // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
createdAt: null == createdAt
|
||||||
|
? _value.createdAt
|
||||||
|
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,
|
||||||
|
updatedAt: null == updatedAt
|
||||||
|
? _value.updatedAt
|
||||||
|
: updatedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,
|
||||||
|
deletedAt: freezed == deletedAt
|
||||||
|
? _value.deletedAt
|
||||||
|
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as dynamic,
|
||||||
|
thumbnail: null == thumbnail
|
||||||
|
? _value.thumbnail
|
||||||
|
: thumbnail // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
title: null == title
|
||||||
|
? _value.title
|
||||||
|
: title // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
description: null == description
|
||||||
|
? _value.description
|
||||||
|
: description // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
content: null == content
|
||||||
|
? _value.content
|
||||||
|
: content // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
url: null == url
|
||||||
|
? _value.url
|
||||||
|
: url // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
hash: null == hash
|
||||||
|
? _value.hash
|
||||||
|
: hash // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
source: null == source
|
||||||
|
? _value.source
|
||||||
|
: source // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
publishedAt: freezed == publishedAt
|
||||||
|
? _value.publishedAt
|
||||||
|
: publishedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime?,
|
||||||
|
) as $Val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class _$$SnNewsArticleImplCopyWith<$Res>
|
||||||
|
implements $SnNewsArticleCopyWith<$Res> {
|
||||||
|
factory _$$SnNewsArticleImplCopyWith(
|
||||||
|
_$SnNewsArticleImpl value, $Res Function(_$SnNewsArticleImpl) then) =
|
||||||
|
__$$SnNewsArticleImplCopyWithImpl<$Res>;
|
||||||
|
@override
|
||||||
|
@useResult
|
||||||
|
$Res call(
|
||||||
|
{int id,
|
||||||
|
DateTime createdAt,
|
||||||
|
DateTime updatedAt,
|
||||||
|
dynamic deletedAt,
|
||||||
|
String thumbnail,
|
||||||
|
String title,
|
||||||
|
String description,
|
||||||
|
String content,
|
||||||
|
String url,
|
||||||
|
String hash,
|
||||||
|
String source,
|
||||||
|
DateTime? publishedAt});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class __$$SnNewsArticleImplCopyWithImpl<$Res>
|
||||||
|
extends _$SnNewsArticleCopyWithImpl<$Res, _$SnNewsArticleImpl>
|
||||||
|
implements _$$SnNewsArticleImplCopyWith<$Res> {
|
||||||
|
__$$SnNewsArticleImplCopyWithImpl(
|
||||||
|
_$SnNewsArticleImpl _value, $Res Function(_$SnNewsArticleImpl) _then)
|
||||||
|
: super(_value, _then);
|
||||||
|
|
||||||
|
/// Create a copy of SnNewsArticle
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? id = null,
|
||||||
|
Object? createdAt = null,
|
||||||
|
Object? updatedAt = null,
|
||||||
|
Object? deletedAt = freezed,
|
||||||
|
Object? thumbnail = null,
|
||||||
|
Object? title = null,
|
||||||
|
Object? description = null,
|
||||||
|
Object? content = null,
|
||||||
|
Object? url = null,
|
||||||
|
Object? hash = null,
|
||||||
|
Object? source = null,
|
||||||
|
Object? publishedAt = freezed,
|
||||||
|
}) {
|
||||||
|
return _then(_$SnNewsArticleImpl(
|
||||||
|
id: null == id
|
||||||
|
? _value.id
|
||||||
|
: id // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
createdAt: null == createdAt
|
||||||
|
? _value.createdAt
|
||||||
|
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,
|
||||||
|
updatedAt: null == updatedAt
|
||||||
|
? _value.updatedAt
|
||||||
|
: updatedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,
|
||||||
|
deletedAt: freezed == deletedAt
|
||||||
|
? _value.deletedAt
|
||||||
|
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as dynamic,
|
||||||
|
thumbnail: null == thumbnail
|
||||||
|
? _value.thumbnail
|
||||||
|
: thumbnail // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
title: null == title
|
||||||
|
? _value.title
|
||||||
|
: title // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
description: null == description
|
||||||
|
? _value.description
|
||||||
|
: description // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
content: null == content
|
||||||
|
? _value.content
|
||||||
|
: content // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
url: null == url
|
||||||
|
? _value.url
|
||||||
|
: url // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
hash: null == hash
|
||||||
|
? _value.hash
|
||||||
|
: hash // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
source: null == source
|
||||||
|
? _value.source
|
||||||
|
: source // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
publishedAt: freezed == publishedAt
|
||||||
|
? _value.publishedAt
|
||||||
|
: publishedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
class _$SnNewsArticleImpl implements _SnNewsArticle {
|
||||||
|
const _$SnNewsArticleImpl(
|
||||||
|
{required this.id,
|
||||||
|
required this.createdAt,
|
||||||
|
required this.updatedAt,
|
||||||
|
required this.deletedAt,
|
||||||
|
required this.thumbnail,
|
||||||
|
required this.title,
|
||||||
|
required this.description,
|
||||||
|
required this.content,
|
||||||
|
required this.url,
|
||||||
|
required this.hash,
|
||||||
|
required this.source,
|
||||||
|
required this.publishedAt});
|
||||||
|
|
||||||
|
factory _$SnNewsArticleImpl.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$$SnNewsArticleImplFromJson(json);
|
||||||
|
|
||||||
|
@override
|
||||||
|
final int id;
|
||||||
|
@override
|
||||||
|
final DateTime createdAt;
|
||||||
|
@override
|
||||||
|
final DateTime updatedAt;
|
||||||
|
@override
|
||||||
|
final dynamic deletedAt;
|
||||||
|
@override
|
||||||
|
final String thumbnail;
|
||||||
|
@override
|
||||||
|
final String title;
|
||||||
|
@override
|
||||||
|
final String description;
|
||||||
|
@override
|
||||||
|
final String content;
|
||||||
|
@override
|
||||||
|
final String url;
|
||||||
|
@override
|
||||||
|
final String hash;
|
||||||
|
@override
|
||||||
|
final String source;
|
||||||
|
@override
|
||||||
|
final DateTime? publishedAt;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SnNewsArticle(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, thumbnail: $thumbnail, title: $title, description: $description, content: $content, url: $url, hash: $hash, source: $source, publishedAt: $publishedAt)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) ||
|
||||||
|
(other.runtimeType == runtimeType &&
|
||||||
|
other is _$SnNewsArticleImpl &&
|
||||||
|
(identical(other.id, id) || other.id == id) &&
|
||||||
|
(identical(other.createdAt, createdAt) ||
|
||||||
|
other.createdAt == createdAt) &&
|
||||||
|
(identical(other.updatedAt, updatedAt) ||
|
||||||
|
other.updatedAt == updatedAt) &&
|
||||||
|
const DeepCollectionEquality().equals(other.deletedAt, deletedAt) &&
|
||||||
|
(identical(other.thumbnail, thumbnail) ||
|
||||||
|
other.thumbnail == thumbnail) &&
|
||||||
|
(identical(other.title, title) || other.title == title) &&
|
||||||
|
(identical(other.description, description) ||
|
||||||
|
other.description == description) &&
|
||||||
|
(identical(other.content, content) || other.content == content) &&
|
||||||
|
(identical(other.url, url) || other.url == url) &&
|
||||||
|
(identical(other.hash, hash) || other.hash == hash) &&
|
||||||
|
(identical(other.source, source) || other.source == source) &&
|
||||||
|
(identical(other.publishedAt, publishedAt) ||
|
||||||
|
other.publishedAt == publishedAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(
|
||||||
|
runtimeType,
|
||||||
|
id,
|
||||||
|
createdAt,
|
||||||
|
updatedAt,
|
||||||
|
const DeepCollectionEquality().hash(deletedAt),
|
||||||
|
thumbnail,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
content,
|
||||||
|
url,
|
||||||
|
hash,
|
||||||
|
source,
|
||||||
|
publishedAt);
|
||||||
|
|
||||||
|
/// Create a copy of SnNewsArticle
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$$SnNewsArticleImplCopyWith<_$SnNewsArticleImpl> get copyWith =>
|
||||||
|
__$$SnNewsArticleImplCopyWithImpl<_$SnNewsArticleImpl>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$$SnNewsArticleImplToJson(
|
||||||
|
this,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _SnNewsArticle implements SnNewsArticle {
|
||||||
|
const factory _SnNewsArticle(
|
||||||
|
{required final int id,
|
||||||
|
required final DateTime createdAt,
|
||||||
|
required final DateTime updatedAt,
|
||||||
|
required final dynamic deletedAt,
|
||||||
|
required final String thumbnail,
|
||||||
|
required final String title,
|
||||||
|
required final String description,
|
||||||
|
required final String content,
|
||||||
|
required final String url,
|
||||||
|
required final String hash,
|
||||||
|
required final String source,
|
||||||
|
required final DateTime? publishedAt}) = _$SnNewsArticleImpl;
|
||||||
|
|
||||||
|
factory _SnNewsArticle.fromJson(Map<String, dynamic> json) =
|
||||||
|
_$SnNewsArticleImpl.fromJson;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get id;
|
||||||
|
@override
|
||||||
|
DateTime get createdAt;
|
||||||
|
@override
|
||||||
|
DateTime get updatedAt;
|
||||||
|
@override
|
||||||
|
dynamic get deletedAt;
|
||||||
|
@override
|
||||||
|
String get thumbnail;
|
||||||
|
@override
|
||||||
|
String get title;
|
||||||
|
@override
|
||||||
|
String get description;
|
||||||
|
@override
|
||||||
|
String get content;
|
||||||
|
@override
|
||||||
|
String get url;
|
||||||
|
@override
|
||||||
|
String get hash;
|
||||||
|
@override
|
||||||
|
String get source;
|
||||||
|
@override
|
||||||
|
DateTime? get publishedAt;
|
||||||
|
|
||||||
|
/// Create a copy of SnNewsArticle
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
_$$SnNewsArticleImplCopyWith<_$SnNewsArticleImpl> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
61
lib/types/news.g.dart
Normal file
61
lib/types/news.g.dart
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'news.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
_$SnNewsSourceImpl _$$SnNewsSourceImplFromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SnNewsSourceImpl(
|
||||||
|
id: json['id'] as String,
|
||||||
|
label: json['label'] as String,
|
||||||
|
type: json['type'] as String,
|
||||||
|
source: json['source'] as String,
|
||||||
|
depth: (json['depth'] as num).toInt(),
|
||||||
|
enabled: json['enabled'] as bool,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$SnNewsSourceImplToJson(_$SnNewsSourceImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'label': instance.label,
|
||||||
|
'type': instance.type,
|
||||||
|
'source': instance.source,
|
||||||
|
'depth': instance.depth,
|
||||||
|
'enabled': instance.enabled,
|
||||||
|
};
|
||||||
|
|
||||||
|
_$SnNewsArticleImpl _$$SnNewsArticleImplFromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SnNewsArticleImpl(
|
||||||
|
id: (json['id'] as num).toInt(),
|
||||||
|
createdAt: DateTime.parse(json['created_at'] as String),
|
||||||
|
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||||
|
deletedAt: json['deleted_at'],
|
||||||
|
thumbnail: json['thumbnail'] as String,
|
||||||
|
title: json['title'] as String,
|
||||||
|
description: json['description'] as String,
|
||||||
|
content: json['content'] as String,
|
||||||
|
url: json['url'] as String,
|
||||||
|
hash: json['hash'] as String,
|
||||||
|
source: json['source'] as String,
|
||||||
|
publishedAt: json['published_at'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['published_at'] as String),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$SnNewsArticleImplToJson(_$SnNewsArticleImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'created_at': instance.createdAt.toIso8601String(),
|
||||||
|
'updated_at': instance.updatedAt.toIso8601String(),
|
||||||
|
'deleted_at': instance.deletedAt,
|
||||||
|
'thumbnail': instance.thumbnail,
|
||||||
|
'title': instance.title,
|
||||||
|
'description': instance.description,
|
||||||
|
'content': instance.content,
|
||||||
|
'url': instance.url,
|
||||||
|
'hash': instance.hash,
|
||||||
|
'source': instance.source,
|
||||||
|
'published_at': instance.publishedAt?.toIso8601String(),
|
||||||
|
};
|
||||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
class AboutScreen extends StatelessWidget {
|
class AboutScreen extends StatelessWidget {
|
||||||
@@ -12,97 +13,103 @@ class AboutScreen extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
const denseButtonStyle = ButtonStyle(visualDensity: VisualDensity(vertical: -4));
|
const denseButtonStyle = ButtonStyle(visualDensity: VisualDensity(vertical: -4));
|
||||||
|
|
||||||
return SizedBox(
|
return AppScaffold(
|
||||||
width: double.infinity,
|
appBar: AppBar(
|
||||||
child: Column(
|
leading: const PageBackButton(),
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
title: Text('screenAbout').tr(),
|
||||||
children: [
|
),
|
||||||
ClipRRect(
|
body: SizedBox(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
width: double.infinity,
|
||||||
child: Image.asset('assets/icon/icon-light-radius.png', width: 120, height: 120),
|
child: Column(
|
||||||
),
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
const Gap(8),
|
children: [
|
||||||
Text(
|
ClipRRect(
|
||||||
'Solian',
|
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(fontSize: 36),
|
child: Image.asset('assets/icon/icon-light-radius.png', width: 120, height: 120),
|
||||||
),
|
),
|
||||||
const Text(
|
const Gap(8),
|
||||||
'The Solar Network',
|
Text(
|
||||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
'Solian',
|
||||||
),
|
style: Theme.of(context).textTheme.titleLarge!.copyWith(fontSize: 36),
|
||||||
const Gap(8),
|
),
|
||||||
FutureBuilder(
|
const Text(
|
||||||
future: PackageInfo.fromPlatform(),
|
'The Solar Network',
|
||||||
builder: (context, snapshot) {
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
||||||
if (!snapshot.hasData) {
|
),
|
||||||
return const SizedBox.shrink();
|
const Gap(8),
|
||||||
}
|
FutureBuilder(
|
||||||
|
future: PackageInfo.fromPlatform(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (!snapshot.hasData) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
return Text(
|
return Text(
|
||||||
'v${snapshot.data!.version} · ${snapshot.data!.buildNumber}',
|
'v${snapshot.data!.version} · ${snapshot.data!.buildNumber}',
|
||||||
style: const TextStyle(fontFamily: 'monospace'),
|
style: const TextStyle(fontFamily: 'monospace'),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
Text('Copyright © ${DateTime.now().year} Solsynth LLC'),
|
Text('Copyright © ${DateTime.now().year} Solsynth LLC'),
|
||||||
const Gap(16),
|
const Gap(16),
|
||||||
Container(
|
Container(
|
||||||
constraints: const BoxConstraints(maxWidth: 280),
|
constraints: const BoxConstraints(maxWidth: 280),
|
||||||
child: Wrap(
|
child: Wrap(
|
||||||
spacing: 4,
|
spacing: 4,
|
||||||
runSpacing: 4,
|
runSpacing: 4,
|
||||||
alignment: WrapAlignment.center,
|
alignment: WrapAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
TextButton(
|
TextButton(
|
||||||
style: denseButtonStyle,
|
style: denseButtonStyle,
|
||||||
child: Text('appDetails').tr(),
|
child: Text('appDetails').tr(),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final info = await PackageInfo.fromPlatform();
|
final info = await PackageInfo.fromPlatform();
|
||||||
|
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
showAboutDialog(
|
showAboutDialog(
|
||||||
context: context,
|
context: context,
|
||||||
applicationName: 'Solian',
|
applicationName: 'Solian',
|
||||||
applicationVersion: '${info.version}+${info.buildNumber}',
|
applicationVersion: '${info.version}+${info.buildNumber}',
|
||||||
applicationLegalese:
|
applicationLegalese:
|
||||||
'The Solar Network App is an intuitive and open-source social network and computing platform. Experience the freedom of a user-friendly design that empowers you to create and connect with communities on your own terms. Embrace the future of social networking with a platform that prioritizes your independence and privacy.',
|
'The Solar Network App is an intuitive and open-source social network and computing platform. Experience the freedom of a user-friendly design that empowers you to create and connect with communities on your own terms. Embrace the future of social networking with a platform that prioritizes your independence and privacy.',
|
||||||
applicationIcon: ClipRRect(
|
applicationIcon: ClipRRect(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||||
child: Image.asset(
|
child: Image.asset(
|
||||||
'assets/icon/icon-light-radius.png',
|
'assets/icon/icon-light-radius.png',
|
||||||
width: 60,
|
width: 60,
|
||||||
height: 60,
|
height: 60,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
},
|
||||||
},
|
),
|
||||||
),
|
TextButton(
|
||||||
TextButton(
|
style: denseButtonStyle,
|
||||||
style: denseButtonStyle,
|
child: Text('termRelated').tr(),
|
||||||
child: Text('termRelated').tr(),
|
onPressed: () {
|
||||||
onPressed: () {
|
launchUrlString('https://solsynth.dev/terms');
|
||||||
launchUrlString('https://solsynth.dev/terms');
|
},
|
||||||
},
|
),
|
||||||
),
|
TextButton(
|
||||||
TextButton(
|
style: denseButtonStyle,
|
||||||
style: denseButtonStyle,
|
child: Text('serviceStatus').tr(),
|
||||||
child: Text('serviceStatus').tr(),
|
onPressed: () {
|
||||||
onPressed: () {
|
launchUrlString('https://status.solsynth.dev');
|
||||||
launchUrlString('https://status.solsynth.dev');
|
},
|
||||||
},
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
|
).center(),
|
||||||
|
const Gap(16),
|
||||||
|
const Text(
|
||||||
|
'Open-sourced under AGPLv3',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w300,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
).center(),
|
],
|
||||||
const Gap(16),
|
),
|
||||||
const Text(
|
|
||||||
'Open-sourced under AGPLv3',
|
|
||||||
style: TextStyle(
|
|
||||||
fontWeight: FontWeight.w300,
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
164
lib/widgets/account/account_popover.dart
Normal file
164
lib/widgets/account/account_popover.dart
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
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:relative_time/relative_time.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:surface/providers/experience.dart';
|
||||||
|
import 'package:surface/providers/sn_network.dart';
|
||||||
|
import 'package:surface/screens/account/profile_page.dart';
|
||||||
|
import 'package:surface/types/account.dart';
|
||||||
|
import 'package:surface/widgets/account/account_image.dart';
|
||||||
|
import 'package:surface/widgets/universal_image.dart';
|
||||||
|
|
||||||
|
class AccountPopoverCard extends StatelessWidget {
|
||||||
|
final SnAccount data;
|
||||||
|
|
||||||
|
const AccountPopoverCard({super.key, required this.data});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (data.banner.isNotEmpty)
|
||||||
|
Container(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: 16 / 7,
|
||||||
|
child: AutoResizeUniversalImage(
|
||||||
|
sn.getAttachmentUrl(data.banner),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Top padding
|
||||||
|
Gap(16),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
AccountImage(
|
||||||
|
content: data.avatar,
|
||||||
|
radius: 20,
|
||||||
|
),
|
||||||
|
Gap(16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(data.nick).bold(),
|
||||||
|
Text('@${data.name}').fontSize(13).opacity(0.75),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
GoRouter.of(context).pushNamed(
|
||||||
|
'accountProfilePage',
|
||||||
|
pathParameters: {'name': data.name},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: const Icon(Symbols.chevron_right),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
||||||
|
),
|
||||||
|
const Gap(8)
|
||||||
|
],
|
||||||
|
).padding(horizontal: 16),
|
||||||
|
const Gap(16),
|
||||||
|
Wrap(
|
||||||
|
children: data.badges
|
||||||
|
.map(
|
||||||
|
(ele) => Tooltip(
|
||||||
|
richMessage: TextSpan(
|
||||||
|
children: [
|
||||||
|
TextSpan(text: kBadgesMeta[ele.type]?.$1.tr() ?? 'unknown'.tr()),
|
||||||
|
if (ele.metadata['title'] != null)
|
||||||
|
TextSpan(
|
||||||
|
text: '\n${ele.metadata['title']}',
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
TextSpan(text: '\n'),
|
||||||
|
TextSpan(
|
||||||
|
text: DateFormat.yMEd().format(ele.createdAt),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
kBadgesMeta[ele.type]?.$2 ?? Symbols.question_mark,
|
||||||
|
color: kBadgesMeta[ele.type]?.$3,
|
||||||
|
fill: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
).padding(horizontal: 24),
|
||||||
|
const Gap(8),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.star),
|
||||||
|
const Gap(8),
|
||||||
|
Text('Lv${getLevelFromExp(data.profile?.experience ?? 0)}'),
|
||||||
|
const Gap(8),
|
||||||
|
Text(calcLevelUpProgressLevel(data.profile?.experience ?? 0)).fontSize(11).opacity(0.5),
|
||||||
|
const Gap(8),
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
constraints: const BoxConstraints(maxWidth: 160),
|
||||||
|
child: LinearProgressIndicator(
|
||||||
|
value: calcLevelUpProgress(data.profile?.experience ?? 0),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
).alignment(Alignment.centerLeft),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 24),
|
||||||
|
FutureBuilder(
|
||||||
|
future: sn.client.get('/cgi/id/users/${data.name}/status'),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final SnAccountStatusInfo? status =
|
||||||
|
snapshot.hasData ? SnAccountStatusInfo.fromJson(snapshot.data!.data) : null;
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Symbols.circle,
|
||||||
|
fill: 1,
|
||||||
|
size: 16,
|
||||||
|
color: (status?.isOnline ?? false) ? Colors.green : Colors.grey,
|
||||||
|
).padding(all: 4),
|
||||||
|
const Gap(8),
|
||||||
|
Text(
|
||||||
|
status != null
|
||||||
|
? status.isOnline
|
||||||
|
? 'accountStatusOnline'.tr()
|
||||||
|
: 'accountStatusOffline'.tr()
|
||||||
|
: 'loading'.tr(),
|
||||||
|
),
|
||||||
|
if (status != null && !status.isOnline && status.lastSeenAt != null)
|
||||||
|
Text(
|
||||||
|
'accountStatusLastSeen'.tr(args: [
|
||||||
|
status.lastSeenAt != null
|
||||||
|
? RelativeTime(context).format(
|
||||||
|
status.lastSeenAt!.toLocal(),
|
||||||
|
)
|
||||||
|
: 'unknown',
|
||||||
|
]),
|
||||||
|
).padding(left: 6).opacity(0.75),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 24);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
// Bottom padding
|
||||||
|
const Gap(16),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,8 +10,8 @@ import 'package:surface/widgets/dialog.dart';
|
|||||||
|
|
||||||
class AttachmentInputDialog extends StatefulWidget {
|
class AttachmentInputDialog extends StatefulWidget {
|
||||||
final String? title;
|
final String? title;
|
||||||
|
final bool? analyzeNow;
|
||||||
const AttachmentInputDialog({super.key, required this.title});
|
const AttachmentInputDialog({super.key, required this.title, this.analyzeNow = false});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<AttachmentInputDialog> createState() => _AttachmentInputDialogState();
|
State<AttachmentInputDialog> createState() => _AttachmentInputDialogState();
|
||||||
@@ -53,6 +53,7 @@ class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
|
|||||||
_thumbnailFile!.path,
|
_thumbnailFile!.path,
|
||||||
'interactive',
|
'interactive',
|
||||||
null,
|
null,
|
||||||
|
analyzeNow: widget.analyzeNow ?? false,
|
||||||
);
|
);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
Navigator.pop(context, attachment);
|
Navigator.pop(context, attachment);
|
||||||
@@ -77,7 +78,8 @@ class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
|
|||||||
controller: _randomIdController,
|
controller: _randomIdController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'fieldAttachmentRandomId'.tr(),
|
labelText: 'fieldAttachmentRandomId'.tr(),
|
||||||
border: const OutlineInputBorder(),
|
border: const UnderlineInputBorder(),
|
||||||
|
isDense: true,
|
||||||
),
|
),
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -20,9 +20,11 @@ import 'package:uuid/uuid.dart';
|
|||||||
class AttachmentItem extends StatelessWidget {
|
class AttachmentItem extends StatelessWidget {
|
||||||
final SnAttachment? data;
|
final SnAttachment? data;
|
||||||
final String? heroTag;
|
final String? heroTag;
|
||||||
|
final BoxFit fit;
|
||||||
|
|
||||||
const AttachmentItem({
|
const AttachmentItem({
|
||||||
super.key,
|
super.key,
|
||||||
|
this.fit = BoxFit.cover,
|
||||||
required this.data,
|
required this.data,
|
||||||
required this.heroTag,
|
required this.heroTag,
|
||||||
});
|
});
|
||||||
@@ -43,7 +45,7 @@ class AttachmentItem extends StatelessWidget {
|
|||||||
child: AutoResizeUniversalImage(
|
child: AutoResizeUniversalImage(
|
||||||
sn.getAttachmentUrl(data!.rid),
|
sn.getAttachmentUrl(data!.rid),
|
||||||
key: Key('attachment-${data!.rid}-$tag'),
|
key: Key('attachment-${data!.rid}-$tag'),
|
||||||
fit: BoxFit.cover,
|
fit: fit,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
case 'video':
|
case 'video':
|
||||||
@@ -313,6 +315,7 @@ class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo
|
|||||||
}
|
}
|
||||||
|
|
||||||
return MaterialDesktopVideoControlsTheme(
|
return MaterialDesktopVideoControlsTheme(
|
||||||
|
key: Key('material-desktop-video-controls-theme-$_showOriginal'),
|
||||||
normal: MaterialDesktopVideoControlsThemeData(
|
normal: MaterialDesktopVideoControlsThemeData(
|
||||||
buttonBarButtonSize: 24,
|
buttonBarButtonSize: 24,
|
||||||
buttonBarButtonColor: Colors.white,
|
buttonBarButtonColor: Colors.white,
|
||||||
@@ -322,12 +325,16 @@ class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo
|
|||||||
MaterialDesktopCustomButton(
|
MaterialDesktopCustomButton(
|
||||||
iconSize: 24,
|
iconSize: 24,
|
||||||
onPressed: _toggleOriginal,
|
onPressed: _toggleOriginal,
|
||||||
icon: _showOriginal ? const Icon(Symbols.high_quality, size: 24) : const Icon(Symbols.sd, size: 24),
|
icon: Icon(
|
||||||
|
_showOriginal ? Symbols.high_quality : Symbols.sd,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
fullscreen: const MaterialDesktopVideoControlsThemeData(),
|
fullscreen: const MaterialDesktopVideoControlsThemeData(),
|
||||||
child: MaterialVideoControlsTheme(
|
child: MaterialVideoControlsTheme(
|
||||||
|
key: Key('material-video-controls-theme-$_showOriginal'),
|
||||||
normal: MaterialVideoControlsThemeData(
|
normal: MaterialVideoControlsThemeData(
|
||||||
buttonBarButtonSize: 24,
|
buttonBarButtonSize: 24,
|
||||||
buttonBarButtonColor: Colors.white,
|
buttonBarButtonColor: Colors.white,
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import 'package:collection/collection.dart';
|
|||||||
import 'package:dismissible_page/dismissible_page.dart';
|
import 'package:dismissible_page/dismissible_page.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:responsive_framework/responsive_framework.dart';
|
|
||||||
import 'package:surface/types/attachment.dart';
|
import 'package:surface/types/attachment.dart';
|
||||||
import 'package:surface/widgets/attachment/attachment_zoom.dart';
|
import 'package:surface/widgets/attachment/attachment_zoom.dart';
|
||||||
import 'package:surface/widgets/attachment/attachment_item.dart';
|
import 'package:surface/widgets/attachment/attachment_item.dart';
|
||||||
@@ -14,19 +14,25 @@ import 'package:uuid/uuid.dart';
|
|||||||
class AttachmentList extends StatefulWidget {
|
class AttachmentList extends StatefulWidget {
|
||||||
final List<SnAttachment?> data;
|
final List<SnAttachment?> data;
|
||||||
final bool bordered;
|
final bool bordered;
|
||||||
final bool noGrow;
|
final bool gridded;
|
||||||
final bool isFlatted;
|
final bool columned;
|
||||||
|
final BoxFit fit;
|
||||||
final double? maxHeight;
|
final double? maxHeight;
|
||||||
final EdgeInsets? listPadding;
|
final double? minWidth;
|
||||||
|
final double? maxWidth;
|
||||||
|
final EdgeInsets? padding;
|
||||||
|
|
||||||
const AttachmentList({
|
const AttachmentList({
|
||||||
super.key,
|
super.key,
|
||||||
required this.data,
|
required this.data,
|
||||||
this.bordered = false,
|
this.bordered = false,
|
||||||
this.noGrow = false,
|
this.gridded = false,
|
||||||
this.isFlatted = false,
|
this.columned = false,
|
||||||
|
this.fit = BoxFit.cover,
|
||||||
this.maxHeight,
|
this.maxHeight,
|
||||||
this.listPadding,
|
this.minWidth,
|
||||||
|
this.maxWidth,
|
||||||
|
this.padding,
|
||||||
});
|
});
|
||||||
|
|
||||||
static const BorderRadius kDefaultRadius = BorderRadius.all(Radius.circular(8));
|
static const BorderRadius kDefaultRadius = BorderRadius.all(Radius.circular(8));
|
||||||
@@ -41,8 +47,6 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
(_) => const Uuid().v4(),
|
(_) => const Uuid().v4(),
|
||||||
);
|
);
|
||||||
|
|
||||||
static const double kAttachmentMaxWidth = 640;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
@@ -51,9 +55,8 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
widget.bordered ? BorderSide(width: 1, color: Theme.of(context).dividerColor) : BorderSide.none;
|
widget.bordered ? BorderSide(width: 1, color: Theme.of(context).dividerColor) : BorderSide.none;
|
||||||
final backgroundColor = Theme.of(context).colorScheme.surfaceContainer;
|
final backgroundColor = Theme.of(context).colorScheme.surfaceContainer;
|
||||||
final constraints = BoxConstraints(
|
final constraints = BoxConstraints(
|
||||||
minWidth: 80,
|
minWidth: widget.minWidth ?? 80,
|
||||||
maxHeight: widget.maxHeight ?? double.infinity,
|
maxHeight: widget.maxHeight ?? MediaQuery.of(context).size.height,
|
||||||
maxWidth: layoutConstraints.maxWidth - 20,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (widget.data.isEmpty) return const SizedBox.shrink();
|
if (widget.data.isEmpty) return const SizedBox.shrink();
|
||||||
@@ -67,121 +70,79 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
.toDouble();
|
.toDouble();
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
constraints: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
|
padding: widget.padding ?? EdgeInsets.zero,
|
||||||
? constraints.copyWith(
|
constraints: constraints,
|
||||||
maxWidth: math.min(
|
child: GestureDetector(
|
||||||
constraints.maxWidth,
|
child: AspectRatio(
|
||||||
kAttachmentMaxWidth,
|
aspectRatio: singleAspectRatio,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: backgroundColor,
|
||||||
|
border: Border.fromBorderSide(borderSide),
|
||||||
|
borderRadius: AttachmentList.kDefaultRadius,
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: AttachmentList.kDefaultRadius,
|
||||||
|
child: AttachmentItem(
|
||||||
|
data: widget.data[0],
|
||||||
|
heroTag: heroTags[0],
|
||||||
|
fit: widget.fit,
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
: null,
|
|
||||||
child: AspectRatio(
|
|
||||||
aspectRatio: singleAspectRatio,
|
|
||||||
child: GestureDetector(
|
|
||||||
child: Builder(
|
|
||||||
builder: (context) {
|
|
||||||
if (ResponsiveBreakpoints.of(context).largerThan(MOBILE) || widget.noGrow) {
|
|
||||||
return Padding(
|
|
||||||
// Single child list-like displaying
|
|
||||||
padding: widget.listPadding ?? EdgeInsets.zero,
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: backgroundColor,
|
|
||||||
border: Border(top: borderSide, bottom: borderSide),
|
|
||||||
borderRadius: AttachmentList.kDefaultRadius,
|
|
||||||
),
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: AttachmentList.kDefaultRadius,
|
|
||||||
child: AttachmentItem(
|
|
||||||
data: widget.data[0],
|
|
||||||
heroTag: heroTags[0],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: backgroundColor,
|
|
||||||
border: Border(top: borderSide, bottom: borderSide),
|
|
||||||
),
|
|
||||||
child: AttachmentItem(
|
|
||||||
data: widget.data[0],
|
|
||||||
heroTag: heroTags.first,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
onTap: () {
|
|
||||||
if (widget.data.firstOrNull?.mediaType != SnMediaType.image) return;
|
|
||||||
context.pushTransparentRoute(
|
|
||||||
AttachmentZoomView(
|
|
||||||
data: widget.data.where((ele) => ele != null).cast(),
|
|
||||||
initialIndex: 0,
|
|
||||||
heroTags: heroTags,
|
|
||||||
),
|
|
||||||
backgroundColor: Colors.black.withOpacity(0.7),
|
|
||||||
rootNavigator: true,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
|
onTap: () {
|
||||||
|
if (widget.data.firstOrNull?.mediaType != SnMediaType.image) return;
|
||||||
|
context.pushTransparentRoute(
|
||||||
|
AttachmentZoomView(
|
||||||
|
data: widget.data.where((ele) => ele != null).cast(),
|
||||||
|
initialIndex: 0,
|
||||||
|
heroTags: heroTags,
|
||||||
|
),
|
||||||
|
backgroundColor: Colors.black.withOpacity(0.7),
|
||||||
|
rootNavigator: true,
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (widget.isFlatted) {
|
final fullOfImage =
|
||||||
return Wrap(
|
widget.data.where((ele) => ele?.mediaType == SnMediaType.image).length == widget.data.length;
|
||||||
spacing: 4,
|
|
||||||
runSpacing: 4,
|
|
||||||
children: widget.data
|
|
||||||
.mapIndexed(
|
|
||||||
(idx, ele) => AspectRatio(
|
|
||||||
aspectRatio: (ele?.data['ratio'] ?? 1).toDouble(),
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: backgroundColor,
|
|
||||||
border: Border(
|
|
||||||
top: borderSide,
|
|
||||||
bottom: borderSide,
|
|
||||||
),
|
|
||||||
borderRadius: AttachmentList.kDefaultRadius,
|
|
||||||
),
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: AttachmentList.kDefaultRadius,
|
|
||||||
child: AttachmentItem(
|
|
||||||
data: ele,
|
|
||||||
heroTag: heroTags[idx],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return AspectRatio(
|
if (widget.gridded && fullOfImage) {
|
||||||
aspectRatio: (widget.data.firstOrNull?.data['ratio'] ?? 1).toDouble(),
|
return Container(
|
||||||
child: Container(
|
margin: widget.padding ?? EdgeInsets.zero,
|
||||||
constraints: BoxConstraints(maxHeight: constraints.maxHeight),
|
decoration: BoxDecoration(
|
||||||
child: ScrollConfiguration(
|
color: backgroundColor,
|
||||||
behavior: _AttachmentListScrollBehavior(),
|
border: Border(
|
||||||
child: ListView.separated(
|
top: borderSide,
|
||||||
shrinkWrap: true,
|
bottom: borderSide,
|
||||||
itemCount: widget.data.length,
|
),
|
||||||
itemBuilder: (context, idx) {
|
borderRadius: AttachmentList.kDefaultRadius,
|
||||||
return Container(
|
),
|
||||||
constraints: constraints,
|
child: ClipRRect(
|
||||||
child: AspectRatio(
|
borderRadius: AttachmentList.kDefaultRadius,
|
||||||
aspectRatio: (widget.data[idx]?.data['ratio'] ?? 1).toDouble(),
|
child: StaggeredGrid.count(
|
||||||
child: GestureDetector(
|
crossAxisCount: math.min(widget.data.length, 2),
|
||||||
|
crossAxisSpacing: 4,
|
||||||
|
mainAxisSpacing: 4,
|
||||||
|
children: widget.data
|
||||||
|
.mapIndexed(
|
||||||
|
(idx, ele) => GestureDetector(
|
||||||
|
child: Container(
|
||||||
|
constraints: constraints,
|
||||||
|
child: AttachmentItem(
|
||||||
|
data: ele,
|
||||||
|
heroTag: heroTags[idx],
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (widget.data[idx]?.mediaType != SnMediaType.image) return;
|
if (widget.data[idx]!.mediaType != SnMediaType.image) return;
|
||||||
context.pushTransparentRoute(
|
context.pushTransparentRoute(
|
||||||
AttachmentZoomView(
|
AttachmentZoomView(
|
||||||
data:
|
data: widget.data.where((ele) => ele != null).cast(),
|
||||||
widget.data.where((ele) => ele != null && ele.mediaType == SnMediaType.image).cast(),
|
|
||||||
initialIndex: idx,
|
initialIndex: idx,
|
||||||
heroTags: heroTags,
|
heroTags: heroTags,
|
||||||
),
|
),
|
||||||
@@ -189,45 +150,115 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
rootNavigator: true,
|
rootNavigator: true,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: Stack(
|
),
|
||||||
fit: StackFit.expand,
|
)
|
||||||
children: [
|
.toList(),
|
||||||
Container(
|
),
|
||||||
decoration: BoxDecoration(
|
),
|
||||||
color: backgroundColor,
|
);
|
||||||
border: Border(
|
}
|
||||||
top: borderSide,
|
|
||||||
bottom: borderSide,
|
if ((!fullOfImage && widget.gridded) || widget.columned) {
|
||||||
),
|
return Container(
|
||||||
borderRadius: AttachmentList.kDefaultRadius,
|
margin: widget.padding ?? EdgeInsets.zero,
|
||||||
),
|
decoration: BoxDecoration(
|
||||||
child: ClipRRect(
|
color: backgroundColor,
|
||||||
borderRadius: AttachmentList.kDefaultRadius,
|
border: Border(
|
||||||
child: AttachmentItem(
|
top: borderSide,
|
||||||
data: widget.data[idx],
|
bottom: borderSide,
|
||||||
heroTag: heroTags[idx],
|
),
|
||||||
),
|
borderRadius: AttachmentList.kDefaultRadius,
|
||||||
),
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: AttachmentList.kDefaultRadius,
|
||||||
|
child: Column(
|
||||||
|
children: widget.data
|
||||||
|
.mapIndexed(
|
||||||
|
(idx, ele) => GestureDetector(
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: ele?.data['ratio']?.toDouble() ?? 1,
|
||||||
|
child: Container(
|
||||||
|
constraints: constraints,
|
||||||
|
child: AttachmentItem(
|
||||||
|
data: ele,
|
||||||
|
heroTag: heroTags[idx],
|
||||||
|
fit: BoxFit.cover,
|
||||||
),
|
),
|
||||||
Positioned(
|
),
|
||||||
right: 8,
|
|
||||||
bottom: 8,
|
|
||||||
child: Chip(
|
|
||||||
label: Text('${idx + 1}/${widget.data.length}'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
);
|
.expand((ele) => [ele, const Divider(height: 1)])
|
||||||
},
|
.toList()
|
||||||
separatorBuilder: (context, index) => const Gap(8),
|
..removeLast(),
|
||||||
padding: widget.listPadding,
|
|
||||||
physics: const BouncingScrollPhysics(),
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
constraints: BoxConstraints(maxHeight: constraints.maxHeight),
|
||||||
|
child: ScrollConfiguration(
|
||||||
|
behavior: _AttachmentListScrollBehavior(),
|
||||||
|
child: ListView.separated(
|
||||||
|
padding: widget.padding,
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: widget.data.length,
|
||||||
|
itemBuilder: (context, idx) {
|
||||||
|
return Container(
|
||||||
|
constraints: constraints.copyWith(maxWidth: widget.maxWidth),
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: (widget.data[idx]?.data['ratio'] ?? 1).toDouble(),
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
if (widget.data[idx]?.mediaType != SnMediaType.image) return;
|
||||||
|
context.pushTransparentRoute(
|
||||||
|
AttachmentZoomView(
|
||||||
|
data: widget.data.where((ele) => ele != null && ele.mediaType == SnMediaType.image).cast(),
|
||||||
|
initialIndex: idx,
|
||||||
|
heroTags: heroTags,
|
||||||
|
),
|
||||||
|
backgroundColor: Colors.black.withOpacity(0.7),
|
||||||
|
rootNavigator: true,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: backgroundColor,
|
||||||
|
border: Border(
|
||||||
|
top: borderSide,
|
||||||
|
bottom: borderSide,
|
||||||
|
),
|
||||||
|
borderRadius: AttachmentList.kDefaultRadius,
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: AttachmentList.kDefaultRadius,
|
||||||
|
child: AttachmentItem(
|
||||||
|
data: widget.data[idx],
|
||||||
|
heroTag: heroTags[idx],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
right: 8,
|
||||||
|
bottom: 8,
|
||||||
|
child: Chip(
|
||||||
|
label: Text('${idx + 1}/${widget.data.length}'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
separatorBuilder: (context, index) => const Gap(8),
|
||||||
|
physics: const BouncingScrollPhysics(),
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -129,6 +129,8 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
|||||||
|
|
||||||
Color get _unFocusColor => Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
Color get _unFocusColor => Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
||||||
|
|
||||||
|
bool _showDetail = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
@@ -144,223 +146,350 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
|||||||
onDismissed: () {
|
onDismissed: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
direction: DismissiblePageDismissDirection.down,
|
direction: DismissiblePageDismissDirection.none,
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
isFullScreen: true,
|
isFullScreen: true,
|
||||||
child: Scaffold(
|
child: GestureDetector(
|
||||||
body: Stack(
|
behavior: HitTestBehavior.translucent,
|
||||||
children: [
|
child: Scaffold(
|
||||||
Builder(builder: (context) {
|
body: Stack(
|
||||||
if (widget.data.length == 1) {
|
children: [
|
||||||
final heroTag = widget.heroTags?.first ?? uuid.v4();
|
Builder(builder: (context) {
|
||||||
return Hero(
|
if (widget.data.length == 1) {
|
||||||
tag: 'attachment-${widget.data.first.rid}-$heroTag',
|
final heroTag = widget.heroTags?.first ?? uuid.v4();
|
||||||
child: PhotoView(
|
return Hero(
|
||||||
key: Key('attachment-detail-${widget.data.first.rid}-$heroTag'),
|
tag: 'attachment-${widget.data.first.rid}-$heroTag',
|
||||||
backgroundDecoration: BoxDecoration(color: Colors.transparent),
|
child: PhotoView(
|
||||||
imageProvider: UniversalImage.provider(
|
key: Key('attachment-detail-${widget.data.first.rid}-$heroTag'),
|
||||||
sn.getAttachmentUrl(widget.data.first.rid),
|
backgroundDecoration: BoxDecoration(color: Colors.transparent),
|
||||||
),
|
imageProvider: UniversalImage.provider(
|
||||||
),
|
sn.getAttachmentUrl(widget.data.first.rid),
|
||||||
);
|
),
|
||||||
}
|
|
||||||
|
|
||||||
return PhotoViewGallery.builder(
|
|
||||||
pageController: _pageController,
|
|
||||||
scrollPhysics: const BouncingScrollPhysics(),
|
|
||||||
builder: (context, idx) {
|
|
||||||
final heroTag = widget.heroTags?.elementAt(idx) ?? uuid.v4();
|
|
||||||
return PhotoViewGalleryPageOptions(
|
|
||||||
imageProvider: UniversalImage.provider(
|
|
||||||
sn.getAttachmentUrl(widget.data.elementAt(idx).rid),
|
|
||||||
),
|
|
||||||
heroAttributes: PhotoViewHeroAttributes(
|
|
||||||
tag: 'attachment-${widget.data.first.rid}-$heroTag',
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
itemCount: widget.data.length,
|
|
||||||
loadingBuilder: (context, event) => Center(
|
return PhotoViewGallery.builder(
|
||||||
child: SizedBox(
|
pageController: _pageController,
|
||||||
width: 20.0,
|
scrollPhysics: const BouncingScrollPhysics(),
|
||||||
height: 20.0,
|
builder: (context, idx) {
|
||||||
child: CircularProgressIndicator(
|
final heroTag = widget.heroTags?.elementAt(idx) ?? uuid.v4();
|
||||||
value: event == null ? 0 : event.cumulativeBytesLoaded / (event.expectedTotalBytes ?? 1),
|
return PhotoViewGalleryPageOptions(
|
||||||
|
imageProvider: UniversalImage.provider(
|
||||||
|
sn.getAttachmentUrl(widget.data.elementAt(idx).rid),
|
||||||
|
),
|
||||||
|
heroAttributes: PhotoViewHeroAttributes(
|
||||||
|
tag: 'attachment-${widget.data.first.rid}-$heroTag',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
itemCount: widget.data.length,
|
||||||
|
loadingBuilder: (context, event) => Center(
|
||||||
|
child: SizedBox(
|
||||||
|
width: 20.0,
|
||||||
|
height: 20.0,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
value: event == null ? 0 : event.cumulativeBytesLoaded / (event.expectedTotalBytes ?? 1),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
backgroundDecoration: BoxDecoration(color: Colors.transparent),
|
||||||
backgroundDecoration: BoxDecoration(color: Colors.transparent),
|
);
|
||||||
);
|
}),
|
||||||
}),
|
Align(
|
||||||
Align(
|
alignment: Alignment.bottomCenter,
|
||||||
alignment: Alignment.bottomCenter,
|
child: IgnorePointer(
|
||||||
child: IgnorePointer(
|
child: Container(
|
||||||
child: Container(
|
height: 300,
|
||||||
height: 300,
|
decoration: BoxDecoration(
|
||||||
decoration: BoxDecoration(
|
gradient: LinearGradient(
|
||||||
gradient: LinearGradient(
|
begin: Alignment.bottomCenter,
|
||||||
begin: Alignment.bottomCenter,
|
end: Alignment.topCenter,
|
||||||
end: Alignment.topCenter,
|
colors: [
|
||||||
colors: [
|
Theme.of(context).colorScheme.surface,
|
||||||
Theme.of(context).colorScheme.surface,
|
Colors.transparent,
|
||||||
Colors.transparent,
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
Positioned(
|
||||||
Positioned(
|
left: 16,
|
||||||
left: 16,
|
right: 16,
|
||||||
right: 16,
|
bottom: 16 + MediaQuery.of(context).padding.bottom,
|
||||||
bottom: 16 + MediaQuery.of(context).padding.bottom,
|
child: Material(
|
||||||
child: Material(
|
color: Colors.transparent,
|
||||||
color: Colors.transparent,
|
child: Builder(builder: (context) {
|
||||||
child: Builder(builder: (context) {
|
final ud = context.read<UserDirectoryProvider>();
|
||||||
final ud = context.read<UserDirectoryProvider>();
|
final item = widget.data.elementAt(
|
||||||
final item = widget.data.elementAt(
|
widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0,
|
||||||
widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0,
|
);
|
||||||
);
|
final account = ud.getAccountFromCache(item.accountId);
|
||||||
final account = ud.getAccountFromCache(item.accountId);
|
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (item.accountId > 0)
|
if (item.accountId > 0)
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
IgnorePointer(
|
IgnorePointer(
|
||||||
child: AccountImage(
|
child: AccountImage(
|
||||||
content: account!.avatar,
|
content: account?.avatar,
|
||||||
radius: 19,
|
radius: 19,
|
||||||
),
|
|
||||||
),
|
|
||||||
const Gap(8),
|
|
||||||
Expanded(
|
|
||||||
child: IgnorePointer(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'attachmentUploadBy'.tr(),
|
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
account.nick,
|
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const Gap(8),
|
||||||
if (widget.data.length > 1)
|
Expanded(
|
||||||
IgnorePointer(
|
child: IgnorePointer(
|
||||||
child: Text(
|
child: Column(
|
||||||
'${(_pageController.page?.round() ?? 0) + 1}/${widget.data.length}',
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
style: GoogleFonts.robotoMono(fontSize: 13),
|
children: [
|
||||||
).padding(right: 8),
|
Text(
|
||||||
),
|
'attachmentUploadBy'.tr(),
|
||||||
InkWell(
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
|
||||||
onTap: _isDownloading
|
|
||||||
? null
|
|
||||||
: () => _saveToAlbum(widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0),
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.all(6),
|
|
||||||
child: !_isDownloading
|
|
||||||
? !_isCompletedDownload
|
|
||||||
? const Icon(Symbols.save_alt)
|
|
||||||
: const Icon(Symbols.download_done)
|
|
||||||
: SizedBox(
|
|
||||||
width: 24,
|
|
||||||
height: 24,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
value: _progressOfDownload,
|
|
||||||
strokeWidth: 3,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
Text(
|
||||||
|
account?.nick ?? 'unknown'.tr(),
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
if (widget.data.length > 1)
|
||||||
|
IgnorePointer(
|
||||||
|
child: Text(
|
||||||
|
'${(_pageController.page?.round() ?? 0) + 1}/${widget.data.length}',
|
||||||
|
style: GoogleFonts.robotoMono(fontSize: 13),
|
||||||
|
).padding(right: 8),
|
||||||
|
),
|
||||||
|
InkWell(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||||
|
onTap: _isDownloading
|
||||||
|
? null
|
||||||
|
: () =>
|
||||||
|
_saveToAlbum(widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(6),
|
||||||
|
child: !_isDownloading
|
||||||
|
? !_isCompletedDownload
|
||||||
|
? const Icon(Symbols.save_alt)
|
||||||
|
: const Icon(Symbols.download_done)
|
||||||
|
: SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
value: _progressOfDownload,
|
||||||
|
strokeWidth: 3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Gap(4),
|
||||||
|
IgnorePointer(
|
||||||
|
child: Text(
|
||||||
|
item.alt,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
const Gap(4),
|
|
||||||
IgnorePointer(
|
|
||||||
child: Text(
|
|
||||||
item.alt,
|
|
||||||
maxLines: 2,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 15,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const Gap(2),
|
||||||
const Gap(2),
|
IgnorePointer(
|
||||||
IgnorePointer(
|
child: Wrap(
|
||||||
child: Wrap(
|
spacing: 6,
|
||||||
spacing: 6,
|
children: [
|
||||||
children: [
|
if (item.metadata['exif'] == null)
|
||||||
if (item.metadata['exif'] == null)
|
Text(
|
||||||
|
'#${item.rid}',
|
||||||
|
style: metaTextStyle,
|
||||||
|
),
|
||||||
|
if (item.metadata['exif']?['Model'] != null)
|
||||||
|
Text(
|
||||||
|
'attachmentShotOn'.tr(args: [
|
||||||
|
item.metadata['exif']?['Model'],
|
||||||
|
]),
|
||||||
|
style: metaTextStyle,
|
||||||
|
).padding(right: 2),
|
||||||
|
if (item.metadata['exif']?['ISO'] != null)
|
||||||
|
Text(
|
||||||
|
'ISO${item.metadata['exif']?['ISO']}',
|
||||||
|
style: metaTextStyle,
|
||||||
|
).padding(right: 2),
|
||||||
|
if (item.metadata['exif']?['Aperture'] != null)
|
||||||
|
Text(
|
||||||
|
'f/${item.metadata['exif']?['Aperture']}',
|
||||||
|
style: metaTextStyle,
|
||||||
|
).padding(right: 2),
|
||||||
|
if (item.metadata['exif']?['Megapixels'] != null &&
|
||||||
|
item.metadata['exif']?['Model'] != null)
|
||||||
|
Text(
|
||||||
|
'${item.metadata['exif']?['Megapixels']}MP',
|
||||||
|
style: metaTextStyle,
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Text(
|
||||||
|
item.size.formatBytes(),
|
||||||
|
style: metaTextStyle,
|
||||||
|
),
|
||||||
|
if (item.metadata['width'] != null && item.metadata['height'] != null)
|
||||||
|
Text(
|
||||||
|
'${item.metadata['width']}x${item.metadata['height']}',
|
||||||
|
style: metaTextStyle,
|
||||||
|
),
|
||||||
|
if (item.metadata['ratio'] != null)
|
||||||
|
Text(
|
||||||
|
(item.metadata['ratio'] as num).toStringAsFixed(2),
|
||||||
|
style: metaTextStyle,
|
||||||
|
),
|
||||||
Text(
|
Text(
|
||||||
'#${item.rid}',
|
item.mimetype,
|
||||||
style: metaTextStyle,
|
style: metaTextStyle,
|
||||||
),
|
),
|
||||||
if (item.metadata['exif']?['Model'] != null)
|
],
|
||||||
Text(
|
),
|
||||||
'attachmentShotOn'.tr(args: [
|
|
||||||
item.metadata['exif']?['Model'],
|
|
||||||
]),
|
|
||||||
style: metaTextStyle,
|
|
||||||
).padding(right: 2),
|
|
||||||
if (item.metadata['exif']?['ShutterSpeed'] != null)
|
|
||||||
Text(
|
|
||||||
item.metadata['exif']?['ShutterSpeed'],
|
|
||||||
style: metaTextStyle,
|
|
||||||
).padding(right: 2),
|
|
||||||
if (item.metadata['exif']?['ISO'] != null)
|
|
||||||
Text(
|
|
||||||
'ISO${item.metadata['exif']?['ISO']}',
|
|
||||||
style: metaTextStyle,
|
|
||||||
).padding(right: 2),
|
|
||||||
if (item.metadata['exif']?['Aperture'] != null)
|
|
||||||
Text(
|
|
||||||
'f/${item.metadata['exif']?['Aperture']}',
|
|
||||||
style: metaTextStyle,
|
|
||||||
).padding(right: 2),
|
|
||||||
if (item.metadata['exif']?['Megapixels'] != null && item.metadata['exif']?['Model'] != null)
|
|
||||||
Text(
|
|
||||||
'${item.metadata['exif']?['Megapixels']}MP',
|
|
||||||
style: metaTextStyle,
|
|
||||||
)
|
|
||||||
else
|
|
||||||
Text(
|
|
||||||
'${item.size} Bytes',
|
|
||||||
style: metaTextStyle,
|
|
||||||
),
|
|
||||||
if (item.metadata['width'] != null && item.metadata['height'] != null)
|
|
||||||
Text(
|
|
||||||
'${item.metadata['width']}x${item.metadata['height']}',
|
|
||||||
style: metaTextStyle,
|
|
||||||
),
|
|
||||||
if (item.metadata['ratio'] != null)
|
|
||||||
Text(
|
|
||||||
(item.metadata['ratio'] as num).toStringAsFixed(2),
|
|
||||||
style: metaTextStyle,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
item.mimetype,
|
|
||||||
style: metaTextStyle,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
);
|
||||||
);
|
}),
|
||||||
}),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
|
onVerticalDragUpdate: (details) {
|
||||||
|
if (_showDetail) return;
|
||||||
|
if (details.delta.dy <= -40) {
|
||||||
|
_showDetail = true;
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => _AttachmentZoomDetailPopup(
|
||||||
|
data: widget.data.elementAt(widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0),
|
||||||
|
),
|
||||||
|
).then((_) {
|
||||||
|
_showDetail = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AttachmentZoomDetailPopup extends StatelessWidget {
|
||||||
|
final SnAttachment data;
|
||||||
|
|
||||||
|
const _AttachmentZoomDetailPopup({required this.data});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final ud = context.read<UserDirectoryProvider>();
|
||||||
|
final account = ud.getAccountFromCache(data.accountId);
|
||||||
|
|
||||||
|
const tableGap = TableRow(
|
||||||
|
children: [
|
||||||
|
TableCell(child: SizedBox(height: 16)),
|
||||||
|
TableCell(child: SizedBox(height: 16)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return SizedBox(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.info, size: 24),
|
||||||
|
const Gap(16),
|
||||||
|
Text('attachmentDetailInfo').tr().textStyle(Theme.of(context).textTheme.titleLarge!),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||||
|
Expanded(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Table(
|
||||||
|
columnWidths: {
|
||||||
|
0: IntrinsicColumnWidth(),
|
||||||
|
1: FlexColumnWidth(),
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
TableRow(
|
||||||
|
children: [
|
||||||
|
TableCell(
|
||||||
|
child: Text('attachmentUploadBy').tr().padding(right: 16),
|
||||||
|
),
|
||||||
|
TableCell(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
if (data.accountId > 0)
|
||||||
|
AccountImage(
|
||||||
|
content: account?.avatar,
|
||||||
|
radius: 8,
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
Text(data.accountId > 0 ? account?.nick ?? 'unknown'.tr() : 'unknown'.tr()),
|
||||||
|
const Gap(8),
|
||||||
|
Text('#${data.accountId}', style: GoogleFonts.robotoMono()).opacity(0.75),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
tableGap,
|
||||||
|
TableRow(
|
||||||
|
children: [
|
||||||
|
TableCell(child: Text('Mimetype').padding(right: 16)),
|
||||||
|
TableCell(child: Text(data.mimetype)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
TableRow(
|
||||||
|
children: [
|
||||||
|
TableCell(child: Text('Size').padding(right: 16)),
|
||||||
|
TableCell(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text(data.size.formatBytes()),
|
||||||
|
const Gap(12),
|
||||||
|
Text('${data.size} Bytes', style: GoogleFonts.robotoMono()).opacity(0.75),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
TableRow(
|
||||||
|
children: [
|
||||||
|
TableCell(child: Text('Name').padding(right: 16)),
|
||||||
|
TableCell(child: Text(data.name)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (data.hash.isNotEmpty)
|
||||||
|
TableRow(
|
||||||
|
children: [
|
||||||
|
TableCell(child: Text('Hash').padding(right: 16)),
|
||||||
|
TableCell(child: Text(data.hash, style: GoogleFonts.robotoMono(fontSize: 11)).opacity(0.9)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
tableGap,
|
||||||
|
...(data.metadata['exif']?.keys.map((k) => TableRow(
|
||||||
|
children: [
|
||||||
|
TableCell(child: Text(k).padding(right: 16)),
|
||||||
|
TableCell(child: Text(data.metadata['exif'][k].toString())),
|
||||||
|
],
|
||||||
|
)) ??
|
||||||
|
[]),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 20, vertical: 8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
86
lib/widgets/attachment/pending_attachment_alt.dart
Normal file
86
lib/widgets/attachment/pending_attachment_alt.dart
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:surface/controllers/post_write_controller.dart';
|
||||||
|
import 'package:surface/providers/sn_attachment.dart';
|
||||||
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
|
||||||
|
class PendingAttachmentAltDialog extends StatefulWidget {
|
||||||
|
final PostWriteMedia media;
|
||||||
|
const PendingAttachmentAltDialog({super.key, required this.media});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PendingAttachmentAltDialog> createState() => _PendingAttachmentAltDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PendingAttachmentAltDialogState extends State<PendingAttachmentAltDialog> {
|
||||||
|
final _contentController = TextEditingController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_contentController.text = widget.media.attachment!.alt;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isBusy = false;
|
||||||
|
|
||||||
|
Future<void> _performAction() async {
|
||||||
|
if (_isBusy) return;
|
||||||
|
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final attach = context.read<SnAttachmentProvider>();
|
||||||
|
final result = await attach.updateOne(
|
||||||
|
widget.media.attachment!,
|
||||||
|
alt: _contentController.text,
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
attach.putCache([result]);
|
||||||
|
Navigator.pop(context, result);
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_contentController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text('attachmentSetAlt').tr(),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
controller: _contentController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'fieldAttachmentAlt'.tr(),
|
||||||
|
border: const UnderlineInputBorder(),
|
||||||
|
),
|
||||||
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: _isBusy ? null : () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
child: Text('dialogDismiss'.tr()),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: _isBusy ? null : () => _performAction(),
|
||||||
|
child: Text('dialogConfirm'.tr()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
120
lib/widgets/attachment/pending_attachment_boost.dart
Normal file
120
lib/widgets/attachment/pending_attachment_boost.dart
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.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/attachment.dart';
|
||||||
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
|
||||||
|
class PendingAttachmentBoostDialog extends StatefulWidget {
|
||||||
|
final PostWriteMedia media;
|
||||||
|
|
||||||
|
const PendingAttachmentBoostDialog({super.key, required this.media});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PendingAttachmentBoostDialog> createState() => _PendingAttachmentBoostDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PendingAttachmentBoostDialogState extends State<PendingAttachmentBoostDialog> {
|
||||||
|
List<SnAttachmentDestination>? _regions;
|
||||||
|
SnAttachmentDestination? _selectedRegion;
|
||||||
|
|
||||||
|
Future<void> _fetchRegions() async {
|
||||||
|
try {
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
final resp = await sn.client.get('/cgi/uc/destinations');
|
||||||
|
setState(() {
|
||||||
|
_regions = List<SnAttachmentDestination>.from(
|
||||||
|
resp.data?.map((e) => SnAttachmentDestination.fromJson(e)) ?? [],
|
||||||
|
).cast<SnAttachmentDestination>().where((ele) => ele.isBoost).toList();
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isBusy = false;
|
||||||
|
|
||||||
|
Future<void> _performAction() async {
|
||||||
|
if (_isBusy) return;
|
||||||
|
if (_selectedRegion == null) return;
|
||||||
|
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
final resp = await sn.client.post('/cgi/uc/boosts', data: {
|
||||||
|
'attachment': widget.media.attachment!.id,
|
||||||
|
'destination': _selectedRegion!.id,
|
||||||
|
});
|
||||||
|
if (!mounted) return;
|
||||||
|
Navigator.pop(context, SnAttachmentBoost.fromJson(resp.data));
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_fetchRegions();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text('attachmentBoost').tr(),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('attachmentBoostHint').tr(),
|
||||||
|
const Gap(16),
|
||||||
|
Text('attachmentDestinationRegion').tr().fontSize(18),
|
||||||
|
const Gap(8),
|
||||||
|
Card(
|
||||||
|
child: _regions == null
|
||||||
|
? const CircularProgressIndicator().center().padding(all: 16)
|
||||||
|
: Column(
|
||||||
|
children: _regions!.map(
|
||||||
|
(ele) {
|
||||||
|
return RadioListTile(
|
||||||
|
title: Text(ele.label).tr(),
|
||||||
|
subtitle: Text(
|
||||||
|
'attachmentDestinationRegion${ele.region}'.trExists()
|
||||||
|
? 'attachmentDestinationRegion${ele.region}'.tr()
|
||||||
|
: ele.region,
|
||||||
|
),
|
||||||
|
selected: _selectedRegion == ele,
|
||||||
|
value: ele,
|
||||||
|
groupValue: _selectedRegion,
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value != null) setState(() => _selectedRegion = value);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: _isBusy ? null : () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
child: Text('dialogDismiss'.tr()),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: _isBusy ? null : () => _performAction(),
|
||||||
|
child: Text('dialogConfirm'.tr()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,19 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.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:popover/popover.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:surface/providers/config.dart';
|
||||||
import 'package:surface/providers/user_directory.dart';
|
import 'package:surface/providers/user_directory.dart';
|
||||||
import 'package:surface/providers/userinfo.dart';
|
import 'package:surface/providers/userinfo.dart';
|
||||||
import 'package:surface/types/chat.dart';
|
import 'package:surface/types/chat.dart';
|
||||||
import 'package:surface/widgets/account/account_image.dart';
|
import 'package:surface/widgets/account/account_image.dart';
|
||||||
|
import 'package:surface/widgets/account/account_popover.dart';
|
||||||
import 'package:surface/widgets/attachment/attachment_list.dart';
|
import 'package:surface/widgets/attachment/attachment_list.dart';
|
||||||
import 'package:surface/widgets/context_menu.dart';
|
import 'package:surface/widgets/context_menu.dart';
|
||||||
import 'package:surface/widgets/link_preview.dart';
|
import 'package:surface/widgets/link_preview.dart';
|
||||||
@@ -24,6 +29,7 @@ class ChatMessage extends StatelessWidget {
|
|||||||
final Function(SnChatMessage)? onReply;
|
final Function(SnChatMessage)? onReply;
|
||||||
final Function(SnChatMessage)? onEdit;
|
final Function(SnChatMessage)? onEdit;
|
||||||
final Function(SnChatMessage)? onDelete;
|
final Function(SnChatMessage)? onDelete;
|
||||||
|
final EdgeInsets padding;
|
||||||
|
|
||||||
const ChatMessage({
|
const ChatMessage({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -35,6 +41,7 @@ class ChatMessage extends StatelessWidget {
|
|||||||
this.onReply,
|
this.onReply,
|
||||||
this.onEdit,
|
this.onEdit,
|
||||||
this.onDelete,
|
this.onDelete,
|
||||||
|
this.padding = const EdgeInsets.only(left: 12, right: 12),
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -47,13 +54,15 @@ class ChatMessage extends StatelessWidget {
|
|||||||
|
|
||||||
final dateFormatter = DateFormat('MM/dd HH:mm');
|
final dateFormatter = DateFormat('MM/dd HH:mm');
|
||||||
|
|
||||||
|
final cfg = context.read<ConfigProvider>();
|
||||||
|
|
||||||
return SwipeTo(
|
return SwipeTo(
|
||||||
key: Key('chat-message-${data.id}'),
|
key: Key('chat-message-${data.id}'),
|
||||||
iconOnLeftSwipe: Symbols.reply,
|
iconOnLeftSwipe: Symbols.reply,
|
||||||
iconOnRightSwipe: Symbols.edit,
|
iconOnRightSwipe: Symbols.edit,
|
||||||
swipeSensitivity: 20,
|
swipeSensitivity: 20,
|
||||||
onLeftSwipe: onReply != null ? (_) => onReply!(data) : null,
|
onLeftSwipe: onReply != null ? (_) => onReply!(data) : null,
|
||||||
onRightSwipe: onEdit != null ? (_) => onEdit!(data) : null,
|
onRightSwipe: (onEdit != null && isOwner) ? (_) => onEdit!(data) : null,
|
||||||
child: ContextMenuArea(
|
child: ContextMenuArea(
|
||||||
contextMenu: ContextMenu(
|
contextMenu: ContextMenu(
|
||||||
entries: [
|
entries: [
|
||||||
@@ -87,83 +96,120 @@ class ChatMessage extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Padding(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
padding: isCompact ? EdgeInsets.zero : padding,
|
||||||
children: [
|
child: Row(
|
||||||
if (!isMerged && !isCompact)
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
AccountImage(
|
children: [
|
||||||
content: user?.avatar,
|
if (!isMerged && !isCompact)
|
||||||
)
|
GestureDetector(
|
||||||
else if (isMerged)
|
child: AccountImage(
|
||||||
const Gap(40),
|
content: user?.avatar,
|
||||||
const Gap(8),
|
),
|
||||||
Expanded(
|
onTap: () {
|
||||||
child: Column(
|
if (user == null) return;
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
showPopover(
|
||||||
children: [
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
if (!isMerged)
|
context: context,
|
||||||
Row(
|
transition: PopoverTransition.other,
|
||||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
bodyBuilder: (context) => SizedBox(
|
||||||
textBaseline: TextBaseline.alphabetic,
|
width: math.min(400, MediaQuery.of(context).size.width - 10),
|
||||||
children: [
|
child: AccountPopoverCard(
|
||||||
if (isCompact)
|
data: user,
|
||||||
AccountImage(
|
|
||||||
content: user?.avatar,
|
|
||||||
radius: 12,
|
|
||||||
).padding(right: 6),
|
|
||||||
Text(
|
|
||||||
(data.sender.nick?.isNotEmpty ?? false) ? data.sender.nick! : user?.nick ?? 'unknown',
|
|
||||||
).bold(),
|
|
||||||
const Gap(6),
|
|
||||||
Text(
|
|
||||||
dateFormatter.format(data.createdAt.toLocal()),
|
|
||||||
).fontSize(13),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if (isCompact) const Gap(4),
|
|
||||||
if (data.preload?.quoteEvent != null)
|
|
||||||
StyledWidget(Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
|
||||||
border: Border.all(
|
|
||||||
color: Theme.of(context).dividerColor,
|
|
||||||
width: 1,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
padding: const EdgeInsets.only(
|
direction: PopoverDirection.bottom,
|
||||||
left: 4,
|
arrowHeight: 5,
|
||||||
right: 4,
|
arrowWidth: 15,
|
||||||
top: 8,
|
arrowDxOffset: -190,
|
||||||
bottom: 6,
|
);
|
||||||
),
|
|
||||||
child: ChatMessage(
|
|
||||||
data: data.preload!.quoteEvent!,
|
|
||||||
isCompact: true,
|
|
||||||
onReply: onReply,
|
|
||||||
onEdit: onEdit,
|
|
||||||
onDelete: onDelete,
|
|
||||||
),
|
|
||||||
)).padding(bottom: 4, top: 4),
|
|
||||||
switch (data.type) {
|
|
||||||
'messages.new' => _ChatMessageText(data: data),
|
|
||||||
_ => _ChatMessageSystemNotify(data: data),
|
|
||||||
},
|
},
|
||||||
],
|
)
|
||||||
),
|
else if (isMerged)
|
||||||
)
|
const Gap(40),
|
||||||
],
|
const Gap(8),
|
||||||
).opacity(isPending ? 0.5 : 1),
|
Expanded(
|
||||||
if (data.body['text'] != null && (data.body['text']?.isNotEmpty ?? false))
|
child: Container(
|
||||||
|
constraints: BoxConstraints(maxWidth: 480),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (!isMerged)
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
if (isCompact)
|
||||||
|
AccountImage(
|
||||||
|
content: user?.avatar,
|
||||||
|
radius: 12,
|
||||||
|
).padding(right: 8),
|
||||||
|
Text(
|
||||||
|
(data.sender.nick?.isNotEmpty ?? false) ? data.sender.nick! : user?.nick ?? 'unknown',
|
||||||
|
).bold(),
|
||||||
|
const Gap(8),
|
||||||
|
Text(
|
||||||
|
dateFormatter.format(data.createdAt.toLocal()),
|
||||||
|
).fontSize(13),
|
||||||
|
],
|
||||||
|
).height(21),
|
||||||
|
if (isCompact) const Gap(8),
|
||||||
|
if (data.preload?.quoteEvent != null)
|
||||||
|
StyledWidget(Container(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxWidth: 480,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
left: 4,
|
||||||
|
right: 4,
|
||||||
|
top: 8,
|
||||||
|
bottom: 6,
|
||||||
|
),
|
||||||
|
child: ChatMessage(
|
||||||
|
data: data.preload!.quoteEvent!,
|
||||||
|
isCompact: true,
|
||||||
|
onReply: onReply,
|
||||||
|
onEdit: onEdit,
|
||||||
|
onDelete: onDelete,
|
||||||
|
),
|
||||||
|
)).padding(bottom: 4, top: 4),
|
||||||
|
switch (data.type) {
|
||||||
|
'messages.new' => _ChatMessageText(
|
||||||
|
data: data,
|
||||||
|
onReply: onReply,
|
||||||
|
onEdit: onEdit,
|
||||||
|
onDelete: onDelete,
|
||||||
|
),
|
||||||
|
_ => _ChatMessageSystemNotify(data: data),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
).opacity(isPending ? 0.5 : 1),
|
||||||
|
),
|
||||||
|
if (data.body['text'] != null &&
|
||||||
|
data.type == 'messages.new' &&
|
||||||
|
(data.body['text']?.isNotEmpty ?? false) &&
|
||||||
|
(cfg.prefs.getBool(kAppExpandChatLink) ?? true))
|
||||||
LinkPreviewWidget(text: data.body['text']!),
|
LinkPreviewWidget(text: data.body['text']!),
|
||||||
if (data.preload?.attachments?.isNotEmpty ?? false)
|
if (data.preload?.attachments?.isNotEmpty ?? false)
|
||||||
AttachmentList(
|
AttachmentList(
|
||||||
data: data.preload!.attachments!,
|
data: data.preload!.attachments!,
|
||||||
bordered: true,
|
bordered: true,
|
||||||
noGrow: true,
|
maxHeight: 560,
|
||||||
maxHeight: 520,
|
maxWidth: 480,
|
||||||
listPadding: const EdgeInsets.only(top: 8),
|
minWidth: 480,
|
||||||
|
padding: padding.copyWith(top: 8),
|
||||||
),
|
),
|
||||||
if (!hasMerged && !isCompact) const Gap(12) else if (!isCompact) const Gap(6),
|
if (!hasMerged && !isCompact) const Gap(12) else if (!isCompact) const Gap(8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -173,19 +219,75 @@ class ChatMessage extends StatelessWidget {
|
|||||||
|
|
||||||
class _ChatMessageText extends StatelessWidget {
|
class _ChatMessageText extends StatelessWidget {
|
||||||
final SnChatMessage data;
|
final SnChatMessage data;
|
||||||
|
final Function(SnChatMessage)? onReply;
|
||||||
|
final Function(SnChatMessage)? onEdit;
|
||||||
|
final Function(SnChatMessage)? onDelete;
|
||||||
|
|
||||||
const _ChatMessageText({required this.data});
|
const _ChatMessageText({required this.data, this.onReply, this.onEdit, this.onDelete});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final ua = context.read<UserProvider>();
|
||||||
|
|
||||||
|
final isOwner = ua.isAuthorized && data.sender.accountId == ua.user?.id;
|
||||||
|
|
||||||
if (data.body['text'] != null && data.body['text'].isNotEmpty) {
|
if (data.body['text'] != null && data.body['text'].isNotEmpty) {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
MarkdownTextContent(
|
SelectionArea(
|
||||||
content: data.body['text'],
|
contextMenuBuilder: (context, editableTextState) {
|
||||||
isSelectable: true,
|
final List<ContextMenuButtonItem> items = editableTextState.contextMenuButtonItems;
|
||||||
isAutoWarp: true,
|
|
||||||
|
if (onReply != null) {
|
||||||
|
items.insert(
|
||||||
|
0,
|
||||||
|
ContextMenuButtonItem(
|
||||||
|
label: 'reply'.tr(),
|
||||||
|
onPressed: () {
|
||||||
|
ContextMenuController.removeAny();
|
||||||
|
onReply?.call(data);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (isOwner && onEdit != null) {
|
||||||
|
items.insert(
|
||||||
|
1,
|
||||||
|
ContextMenuButtonItem(
|
||||||
|
label: 'edit'.tr(),
|
||||||
|
onPressed: () {
|
||||||
|
ContextMenuController.removeAny();
|
||||||
|
onEdit?.call(data);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (isOwner && onDelete != null) {
|
||||||
|
items.insert(
|
||||||
|
2,
|
||||||
|
ContextMenuButtonItem(
|
||||||
|
label: 'delete'.tr(),
|
||||||
|
onPressed: () {
|
||||||
|
ContextMenuController.removeAny();
|
||||||
|
onDelete?.call(data);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return AdaptiveTextSelectionToolbar.buttonItems(
|
||||||
|
anchors: editableTextState.contextMenuAnchors,
|
||||||
|
buttonItems: items,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 480),
|
||||||
|
child: MarkdownTextContent(
|
||||||
|
content: data.body['text'],
|
||||||
|
isAutoWarp: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (data.updatedAt != data.createdAt)
|
if (data.updatedAt != data.createdAt)
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import 'package:surface/providers/user_directory.dart';
|
|||||||
import 'package:surface/types/attachment.dart';
|
import 'package:surface/types/attachment.dart';
|
||||||
import 'package:surface/types/chat.dart';
|
import 'package:surface/types/chat.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/markdown_content.dart';
|
|
||||||
import 'package:surface/widgets/post/post_media_pending_list.dart';
|
import 'package:surface/widgets/post/post_media_pending_list.dart';
|
||||||
|
|
||||||
class ChatMessageInput extends StatefulWidget {
|
class ChatMessageInput extends StatefulWidget {
|
||||||
@@ -33,12 +32,24 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
final TextEditingController _contentController = TextEditingController();
|
final TextEditingController _contentController = TextEditingController();
|
||||||
final FocusNode _focusNode = FocusNode();
|
final FocusNode _focusNode = FocusNode();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_contentController.addListener(() {
|
||||||
|
if (_contentController.text.isNotEmpty) {
|
||||||
|
widget.controller.pingTypingStatus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void setReply(SnChatMessage? value) {
|
void setReply(SnChatMessage? value) {
|
||||||
setState(() => _replyingMessage = value);
|
setState(() => _replyingMessage = value);
|
||||||
}
|
}
|
||||||
|
|
||||||
void setEdit(SnChatMessage? value) {
|
void setEdit(SnChatMessage? value) {
|
||||||
_contentController.text = value?.body['text'] ?? '';
|
_contentController.text = value?.body['text'] ?? '';
|
||||||
|
_attachments.clear();
|
||||||
|
_attachments.addAll(value?.preload?.attachments?.map((e) => PostWriteMedia(e)) ?? []);
|
||||||
setState(() => _editingMessage = value);
|
setState(() => _editingMessage = value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,6 +94,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
media.toFile()!,
|
media.toFile()!,
|
||||||
place.$1,
|
place.$1,
|
||||||
place.$2,
|
place.$2,
|
||||||
|
analyzeNow: media.type == SnMediaType.image,
|
||||||
onProgress: (progress) {
|
onProgress: (progress) {
|
||||||
// Calculate overall progress for attachments
|
// Calculate overall progress for attachments
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -91,7 +103,9 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
_attachments[i] = PostWriteMedia(item);
|
setState(() {
|
||||||
|
_attachments[i] = PostWriteMedia(item);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@@ -103,7 +117,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
// Send the message
|
// Send the message
|
||||||
// NOTICE This future should not be awaited, so that the message can be sent in the background and the user can continue to type
|
// NOTICE This future should not be awaited, so that the message can be sent in the background and the user can continue to type
|
||||||
widget.controller.sendMessage(
|
widget.controller.sendMessage(
|
||||||
'messages.new',
|
_editingMessage != null ? 'messages.edit' : 'messages.new',
|
||||||
_contentController.text,
|
_contentController.text,
|
||||||
attachments: _attachments.where((e) => e.attachment != null).map((e) => e.attachment!.rid).toList(),
|
attachments: _attachments.where((e) => e.attachment != null).map((e) => e.attachment!.rid).toList(),
|
||||||
relatedId: _editingMessage?.id,
|
relatedId: _editingMessage?.id,
|
||||||
@@ -160,75 +174,84 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
|
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
|
||||||
SingleChildScrollView(
|
SingleChildScrollView(
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
child: Padding(
|
child: _replyingMessage != null
|
||||||
padding: _replyingMessage != null ? const EdgeInsets.only(top: 8) : EdgeInsets.zero,
|
? Container(
|
||||||
child: _replyingMessage != null
|
padding: const EdgeInsets.only(left: 16, right: 16),
|
||||||
? MaterialBanner(
|
decoration: BoxDecoration(
|
||||||
padding: const EdgeInsets.only(left: 16.0),
|
border: Border(
|
||||||
leading: const Icon(Symbols.reply),
|
bottom: BorderSide(
|
||||||
backgroundColor: Colors.transparent,
|
color: Theme.of(context).dividerColor,
|
||||||
content: SingleChildScrollView(
|
width: 1 / MediaQuery.of(context).devicePixelRatio,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
if (_replyingMessage?.body['text'] != null)
|
|
||||||
MarkdownTextContent(
|
|
||||||
content: _replyingMessage?.body['text'],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
),
|
||||||
TextButton(
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.reply, size: 20),
|
||||||
|
const Gap(8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
_replyingMessage?.body['text'] ?? '${_replyingMessage?.sender.nick}',
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(16),
|
||||||
|
InkWell(
|
||||||
child: Text('cancel'.tr()),
|
child: Text('cancel'.tr()),
|
||||||
onPressed: () {
|
onTap: () {
|
||||||
|
_attachments.clear();
|
||||||
setState(() => _replyingMessage = null);
|
setState(() => _replyingMessage = null);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
).padding(vertical: 8),
|
||||||
: const SizedBox.shrink(),
|
)
|
||||||
),
|
: const SizedBox.shrink(),
|
||||||
)
|
)
|
||||||
.height(_replyingMessage != null ? 54 + 8 : 0, animate: true)
|
.height(_replyingMessage != null ? 38 : 0, animate: true)
|
||||||
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
|
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
|
||||||
SingleChildScrollView(
|
SingleChildScrollView(
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
child: Padding(
|
child: _editingMessage != null
|
||||||
padding: _editingMessage != null ? const EdgeInsets.only(top: 8) : EdgeInsets.zero,
|
? Container(
|
||||||
child: _editingMessage != null
|
padding: const EdgeInsets.only(left: 16, right: 16),
|
||||||
? MaterialBanner(
|
decoration: BoxDecoration(
|
||||||
padding: const EdgeInsets.only(left: 16.0),
|
border: Border(
|
||||||
leading: const Icon(Symbols.edit),
|
bottom: BorderSide(
|
||||||
backgroundColor: Colors.transparent,
|
color: Theme.of(context).dividerColor,
|
||||||
content: SingleChildScrollView(
|
width: 1 / MediaQuery.of(context).devicePixelRatio,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
if (_editingMessage?.body['text'] != null)
|
|
||||||
MarkdownTextContent(
|
|
||||||
content: _editingMessage?.body['text'],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
),
|
||||||
TextButton(
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.edit, size: 20),
|
||||||
|
const Gap(8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
_editingMessage?.body['text'] ?? '${_editingMessage?.sender.nick}',
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(16),
|
||||||
|
InkWell(
|
||||||
child: Text('cancel'.tr()),
|
child: Text('cancel'.tr()),
|
||||||
onPressed: () {
|
onTap: () {
|
||||||
|
_attachments.clear();
|
||||||
|
_contentController.clear();
|
||||||
setState(() => _editingMessage = null);
|
setState(() => _editingMessage = null);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
).padding(vertical: 8),
|
||||||
: const SizedBox.shrink(),
|
)
|
||||||
),
|
: const SizedBox.shrink(),
|
||||||
)
|
)
|
||||||
.height(_editingMessage != null ? 54 + 8 : 0, animate: true)
|
.height(_editingMessage != null ? 38 : 0, animate: true)
|
||||||
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
|
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 56,
|
height: 56,
|
||||||
|
|||||||
53
lib/widgets/chat/chat_typing_indicator.dart
Normal file
53
lib/widgets/chat/chat_typing_indicator.dart
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:surface/controllers/chat_message_controller.dart';
|
||||||
|
import 'package:surface/providers/user_directory.dart';
|
||||||
|
|
||||||
|
class ChatTypingIndicator extends StatelessWidget {
|
||||||
|
final ChatMessageController controller;
|
||||||
|
|
||||||
|
const ChatTypingIndicator({super.key, required this.controller});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final ud = context.read<UserDirectoryProvider>();
|
||||||
|
|
||||||
|
return StyledWidget(controller.typingMembers.isEmpty
|
||||||
|
? const SizedBox.shrink()
|
||||||
|
: Container(
|
||||||
|
padding: const EdgeInsets.only(left: 16, right: 16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
bottom: BorderSide(
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
width: 1 / MediaQuery.of(context).devicePixelRatio,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.more_horiz, weight: 600, size: 20),
|
||||||
|
const Gap(8),
|
||||||
|
Text(
|
||||||
|
'messageTyping'.plural(controller.typingMembers.length, args: [
|
||||||
|
controller.typingMembers
|
||||||
|
.map((ele) => (ele.nick?.isNotEmpty ?? false)
|
||||||
|
? ele.nick!
|
||||||
|
: ud.getAccountFromCache(ele.accountId)?.name ?? 'unknown')
|
||||||
|
.join(', '),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
))
|
||||||
|
.height(controller.typingMembers.isNotEmpty ? 38 : 0, animate: true)
|
||||||
|
.animate(
|
||||||
|
const Duration(milliseconds: 300),
|
||||||
|
Curves.fastLinearToSlowEaseIn,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import 'package:easy_localization/easy_localization.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:styled_widget/styled_widget.dart';
|
||||||
import 'package:surface/providers/userinfo.dart';
|
import 'package:surface/providers/userinfo.dart';
|
||||||
@@ -16,45 +18,49 @@ class ConnectionIndicator extends StatelessWidget {
|
|||||||
listenable: ws,
|
listenable: ws,
|
||||||
builder: (context, _) {
|
builder: (context, _) {
|
||||||
final ua = context.read<UserProvider>();
|
final ua = context.read<UserProvider>();
|
||||||
|
final show = (ws.isBusy || !ws.isConnected) && ua.isAuthorized;
|
||||||
|
|
||||||
return GestureDetector(
|
return IgnorePointer(
|
||||||
child: Container(
|
ignoring: !show,
|
||||||
padding: EdgeInsets.only(
|
child: GestureDetector(
|
||||||
bottom: 8,
|
child: Material(
|
||||||
top: MediaQuery.of(context).padding.top + 8,
|
elevation: 2,
|
||||||
left: 24,
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))),
|
||||||
right: 24,
|
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
),
|
child: ua.isAuthorized
|
||||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
? Row(
|
||||||
child: ua.isAuthorized
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
? Row(
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
children: [
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
if (ws.isBusy)
|
||||||
children: [
|
Text('serverConnecting').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer)
|
||||||
if (ws.isBusy)
|
else if (!ws.isConnected)
|
||||||
Text('serverConnecting').tr().textColor(
|
Text('serverDisconnected').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer)
|
||||||
Theme.of(context).colorScheme.onSecondaryContainer)
|
else
|
||||||
else if (!ws.isConnected)
|
Text('serverConnected').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer),
|
||||||
Text('serverDisconnected').tr().textColor(
|
const Gap(8),
|
||||||
Theme.of(context).colorScheme.onSecondaryContainer),
|
if (ws.isBusy)
|
||||||
],
|
const CircularProgressIndicator(strokeWidth: 2.5)
|
||||||
)
|
.width(12)
|
||||||
: const SizedBox.shrink(),
|
.height(12)
|
||||||
)
|
.padding(horizontal: 4, right: 4)
|
||||||
.height(
|
else if (!ws.isConnected)
|
||||||
(ws.isBusy || !ws.isConnected) && ua.isAuthorized
|
const Icon(Symbols.power_off, size: 18)
|
||||||
? MediaQuery.of(context).padding.top + 36
|
else
|
||||||
: 0,
|
const Icon(Symbols.power, size: 18),
|
||||||
animate: true)
|
],
|
||||||
.animate(
|
).padding(horizontal: 8, vertical: 4)
|
||||||
const Duration(milliseconds: 300),
|
: const SizedBox.shrink(),
|
||||||
Curves.easeInOut,
|
).opacity(show ? 1 : 0, animate: true).animate(
|
||||||
),
|
const Duration(milliseconds: 300),
|
||||||
onTap: () {
|
Curves.easeInOut,
|
||||||
if (!ws.isConnected && !ws.isBusy) {
|
),
|
||||||
ws.connect();
|
onTap: () {
|
||||||
}
|
if (!ws.isConnected && !ws.isBusy) {
|
||||||
},
|
ws.connect();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_animate/flutter_animate.dart';
|
import 'package:flutter_animate/flutter_animate.dart';
|
||||||
import 'package:flutter_context_menu/flutter_context_menu.dart';
|
import 'package:flutter_context_menu/flutter_context_menu.dart';
|
||||||
import 'package:responsive_framework/responsive_framework.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:surface/providers/config.dart';
|
||||||
|
|
||||||
class ContextMenuArea extends StatelessWidget {
|
class ContextMenuArea extends StatelessWidget {
|
||||||
final ContextMenu contextMenu;
|
final ContextMenu contextMenu;
|
||||||
@@ -22,11 +23,10 @@ class ContextMenuArea extends StatelessWidget {
|
|||||||
return Listener(
|
return Listener(
|
||||||
onPointerDown: (event) {
|
onPointerDown: (event) {
|
||||||
mousePosition = event.position;
|
mousePosition = event.position;
|
||||||
final isCollapseDrawer = ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE);
|
final cfg = context.read<ConfigProvider>();
|
||||||
if (!isCollapseDrawer) {
|
if (!cfg.drawerIsCollapsed) {
|
||||||
final isExpandDrawer = ResponsiveBreakpoints.of(context).largerThan(TABLET);
|
|
||||||
// Leave padding for side navigation
|
// Leave padding for side navigation
|
||||||
mousePosition = isExpandDrawer
|
mousePosition = cfg.drawerIsExpanded
|
||||||
? mousePosition.copyWith(dx: mousePosition.dx - 304 * 2)
|
? mousePosition.copyWith(dx: mousePosition.dx - 304 * 2)
|
||||||
: mousePosition.copyWith(dx: mousePosition.dx - 72 * 2);
|
: mousePosition.copyWith(dx: mousePosition.dx - 72 * 2);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,12 +7,11 @@ import 'package:marquee/marquee.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:responsive_framework/responsive_framework.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/link_preview.dart';
|
||||||
import 'package:surface/types/link.dart';
|
import 'package:surface/types/link.dart';
|
||||||
import 'package:surface/widgets/universal_image.dart';
|
import 'package:surface/widgets/universal_image.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
import '../providers/link_preview.dart';
|
|
||||||
|
|
||||||
class LinkPreviewWidget extends StatefulWidget {
|
class LinkPreviewWidget extends StatefulWidget {
|
||||||
final String text;
|
final String text;
|
||||||
|
|
||||||
@@ -81,8 +80,9 @@ class _LinkPreviewEntry extends StatelessWidget {
|
|||||||
child: AspectRatio(
|
child: AspectRatio(
|
||||||
aspectRatio: 16 / 9,
|
aspectRatio: 16 / 9,
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
child: AutoResizeUniversalImage(
|
child: AutoResizeUniversalImage(
|
||||||
meta.image!,
|
meta.image!.startsWith('//') ? 'https:${meta.image}' : meta.image!,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -94,11 +94,14 @@ class _LinkPreviewEntry extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
if (meta.icon?.isNotEmpty ?? false)
|
if (meta.icon?.isNotEmpty ?? false)
|
||||||
StyledWidget(
|
SizedBox(
|
||||||
meta.icon!.endsWith('.svg')
|
width: 36,
|
||||||
? SvgPicture.network(meta.icon!)
|
height: 36,
|
||||||
|
child: meta.icon!.endsWith('.svg')
|
||||||
|
? SvgPicture.network(meta.icon!, width: 36, height: 36)
|
||||||
: UniversalImage(
|
: UniversalImage(
|
||||||
meta.icon!,
|
meta.icon!,
|
||||||
|
noErrorWidget: true,
|
||||||
width: 36,
|
width: 36,
|
||||||
height: 36,
|
height: 36,
|
||||||
cacheHeight: 36,
|
cacheHeight: 36,
|
||||||
|
|||||||
@@ -1,39 +1,38 @@
|
|||||||
import 'dart:ui';
|
|
||||||
|
|
||||||
import 'package:dismissible_page/dismissible_page.dart';
|
import 'package:dismissible_page/dismissible_page.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:markdown/markdown.dart' as markdown;
|
import 'package:markdown/markdown.dart' as markdown;
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:surface/providers/sn_network.dart';
|
||||||
|
import 'package:surface/providers/sn_sticker.dart';
|
||||||
import 'package:surface/types/attachment.dart';
|
import 'package:surface/types/attachment.dart';
|
||||||
import 'package:surface/widgets/attachment/attachment_item.dart';
|
import 'package:surface/widgets/attachment/attachment_item.dart';
|
||||||
import 'package:surface/widgets/universal_image.dart';
|
import 'package:surface/widgets/universal_image.dart';
|
||||||
import 'package:syntax_highlight/syntax_highlight.dart';
|
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
import 'package:path/path.dart';
|
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
import 'attachment/attachment_zoom.dart';
|
import 'attachment/attachment_zoom.dart';
|
||||||
|
|
||||||
class MarkdownTextContent extends StatelessWidget {
|
class MarkdownTextContent extends StatelessWidget {
|
||||||
final String content;
|
final String content;
|
||||||
final bool isSelectable;
|
|
||||||
final bool isAutoWarp;
|
final bool isAutoWarp;
|
||||||
|
final bool isEnlargeSticker;
|
||||||
final TextScaler? textScaler;
|
final TextScaler? textScaler;
|
||||||
final List<SnAttachment?>? attachments;
|
final List<SnAttachment?>? attachments;
|
||||||
|
|
||||||
const MarkdownTextContent({
|
const MarkdownTextContent({
|
||||||
super.key,
|
super.key,
|
||||||
required this.content,
|
required this.content,
|
||||||
this.isSelectable = false,
|
|
||||||
this.isAutoWarp = false,
|
this.isAutoWarp = false,
|
||||||
|
this.isEnlargeSticker = false,
|
||||||
this.textScaler,
|
this.textScaler,
|
||||||
this.attachments,
|
this.attachments,
|
||||||
});
|
});
|
||||||
|
|
||||||
Widget _buildContent(BuildContext context) {
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
return Markdown(
|
return Markdown(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
@@ -42,33 +41,33 @@ class MarkdownTextContent extends StatelessWidget {
|
|||||||
styleSheet: MarkdownStyleSheet.fromTheme(
|
styleSheet: MarkdownStyleSheet.fromTheme(
|
||||||
Theme.of(context),
|
Theme.of(context),
|
||||||
).copyWith(
|
).copyWith(
|
||||||
textScaler: textScaler,
|
textScaler: textScaler,
|
||||||
blockquote: TextStyle(
|
blockquote: TextStyle(
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
blockquoteDecoration: BoxDecoration(
|
blockquoteDecoration: BoxDecoration(
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
||||||
),
|
),
|
||||||
horizontalRuleDecoration: BoxDecoration(
|
horizontalRuleDecoration: BoxDecoration(
|
||||||
border: Border(
|
border: Border(
|
||||||
top: BorderSide(
|
top: BorderSide(
|
||||||
width: 1.0,
|
width: 1.0,
|
||||||
color: Theme.of(context).dividerColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
codeblockDecoration: BoxDecoration(
|
|
||||||
border: Border.all(
|
|
||||||
color: Theme.of(context).dividerColor,
|
color: Theme.of(context).dividerColor,
|
||||||
width: 0.3,
|
|
||||||
),
|
),
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
),
|
||||||
color: Theme.of(context).colorScheme.surface.withOpacity(0.5),
|
),
|
||||||
)),
|
codeblockDecoration: BoxDecoration(
|
||||||
builders: {
|
border: Border.all(
|
||||||
'code': _MarkdownTextCodeElement(),
|
color: Theme.of(context).dividerColor,
|
||||||
},
|
width: 0.3,
|
||||||
|
),
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
||||||
|
color: Theme.of(context).colorScheme.surface.withOpacity(0.5),
|
||||||
|
),
|
||||||
|
code: GoogleFonts.robotoMono(height: 1),
|
||||||
|
),
|
||||||
|
builders: {},
|
||||||
softLineBreak: true,
|
softLineBreak: true,
|
||||||
extensionSet: markdown.ExtensionSet(
|
extensionSet: markdown.ExtensionSet(
|
||||||
<markdown.BlockSyntax>[
|
<markdown.BlockSyntax>[
|
||||||
@@ -78,6 +77,7 @@ class MarkdownTextContent extends StatelessWidget {
|
|||||||
<markdown.InlineSyntax>[
|
<markdown.InlineSyntax>[
|
||||||
if (isAutoWarp) markdown.LineBreakSyntax(),
|
if (isAutoWarp) markdown.LineBreakSyntax(),
|
||||||
_UserNameCardInlineSyntax(),
|
_UserNameCardInlineSyntax(),
|
||||||
|
_CustomEmoteInlineSyntax(context),
|
||||||
markdown.AutolinkSyntax(),
|
markdown.AutolinkSyntax(),
|
||||||
markdown.AutolinkExtensionSyntax(),
|
markdown.AutolinkExtensionSyntax(),
|
||||||
markdown.CodeSyntax(),
|
markdown.CodeSyntax(),
|
||||||
@@ -108,9 +108,41 @@ class MarkdownTextContent extends StatelessWidget {
|
|||||||
if (url.startsWith('solink://')) {
|
if (url.startsWith('solink://')) {
|
||||||
final segments = url.replaceFirst('solink://', '').split('/');
|
final segments = url.replaceFirst('solink://', '').split('/');
|
||||||
switch (segments[0]) {
|
switch (segments[0]) {
|
||||||
|
case 'stickers':
|
||||||
|
final alias = segments[1];
|
||||||
|
final st = context.read<SnStickerProvider>();
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
final double size = isEnlargeSticker ? 128 : 32;
|
||||||
|
return Container(
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
child: FutureBuilder<SnSticker?>(
|
||||||
|
future: st.lookupSticker(alias),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.hasData) {
|
||||||
|
return UniversalImage(
|
||||||
|
sn.getAttachmentUrl(snapshot.data!.attachment.rid),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
cacheHeight: size,
|
||||||
|
cacheWidth: size,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
case 'attachments':
|
case 'attachments':
|
||||||
final attachment = attachments?.firstWhere(
|
final attachment = attachments?.firstWhere(
|
||||||
(ele) => ele?.rid == segments[1],
|
(ele) => ele?.rid == segments[1],
|
||||||
orElse: () => null,
|
orElse: () => null,
|
||||||
);
|
);
|
||||||
if (attachment != null) {
|
if (attachment != null) {
|
||||||
@@ -168,14 +200,6 @@ class MarkdownTextContent extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
if (isSelectable) {
|
|
||||||
return SelectionArea(child: _buildContent(context));
|
|
||||||
}
|
|
||||||
return _buildContent(context);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _UserNameCardInlineSyntax extends markdown.InlineSyntax {
|
class _UserNameCardInlineSyntax extends markdown.InlineSyntax {
|
||||||
@@ -194,45 +218,24 @@ class _UserNameCardInlineSyntax extends markdown.InlineSyntax {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MarkdownTextCodeElement extends MarkdownElementBuilder {
|
class _CustomEmoteInlineSyntax extends markdown.InlineSyntax {
|
||||||
@override
|
final BuildContext context;
|
||||||
Widget? visitElementAfter(
|
|
||||||
markdown.Element element,
|
|
||||||
TextStyle? preferredStyle,
|
|
||||||
) {
|
|
||||||
var language = '';
|
|
||||||
|
|
||||||
if (element.attributes['class'] != null) {
|
_CustomEmoteInlineSyntax(this.context) : super(r':([-\w]+):');
|
||||||
String lg = element.attributes['class'] as String;
|
|
||||||
language = lg.substring(9).trim();
|
@override
|
||||||
|
bool onMatch(markdown.InlineParser parser, Match match) {
|
||||||
|
final SnStickerProvider st = context.read<SnStickerProvider>();
|
||||||
|
final alias = match[1]!.toUpperCase();
|
||||||
|
if (st.hasNotSticker(alias)) {
|
||||||
|
parser.advanceBy(1);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
return SizedBox(
|
|
||||||
child: FutureBuilder(
|
final element = markdown.Element.empty('img');
|
||||||
future: (() async {
|
element.attributes['src'] = 'solink://stickers/$alias';
|
||||||
final docPath = '../../../';
|
parser.addNode(element);
|
||||||
final highlightingPath = join(docPath, 'assets/highlighting', language);
|
|
||||||
await Highlighter.initialize([highlightingPath]);
|
return true;
|
||||||
return Highlighter(
|
|
||||||
language: highlightingPath,
|
|
||||||
theme: PlatformDispatcher.instance.platformBrightness == Brightness.light
|
|
||||||
? await HighlighterTheme.loadLightTheme()
|
|
||||||
: await HighlighterTheme.loadDarkTheme(),
|
|
||||||
);
|
|
||||||
})(),
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
if (snapshot.hasData) {
|
|
||||||
final highlighter = snapshot.data!;
|
|
||||||
return Text.rich(
|
|
||||||
highlighter.highlight(element.textContent.trim()),
|
|
||||||
style: GoogleFonts.robotoMono(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return Text(
|
|
||||||
element.textContent.trim(),
|
|
||||||
style: GoogleFonts.robotoMono(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
).padding(all: 8);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/foundation.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:provider/provider.dart';
|
import 'package:provider/provider.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/config.dart';
|
||||||
import 'package:surface/providers/navigation.dart';
|
import 'package:surface/providers/navigation.dart';
|
||||||
import 'package:surface/widgets/version_label.dart';
|
import 'package:surface/widgets/version_label.dart';
|
||||||
|
|
||||||
@@ -28,8 +32,9 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final nav = context.watch<NavigationProvider>();
|
final nav = context.watch<NavigationProvider>();
|
||||||
|
final cfg = context.watch<ConfigProvider>();
|
||||||
|
|
||||||
final backgroundColor = ResponsiveBreakpoints.of(context).largerThan(TABLET) ? Colors.transparent : null;
|
final backgroundColor = cfg.drawerIsExpanded ? Colors.transparent : null;
|
||||||
|
|
||||||
return ListenableBuilder(
|
return ListenableBuilder(
|
||||||
listenable: nav,
|
listenable: nav,
|
||||||
@@ -44,6 +49,18 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
|
|||||||
backgroundColor: backgroundColor,
|
backgroundColor: backgroundColor,
|
||||||
selectedIndex: nav.currentIndex,
|
selectedIndex: nav.currentIndex,
|
||||||
children: [
|
children: [
|
||||||
|
if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS) && !cfg.drawerIsExpanded)
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
bottom: BorderSide(
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
width: 1 / MediaQuery.of(context).devicePixelRatio,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: WindowTitleBarBox(),
|
||||||
|
),
|
||||||
Column(
|
Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
|||||||
@@ -18,9 +18,7 @@ class _AppRailNavigationState extends State<AppRailNavigation> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
context
|
context.read<NavigationProvider>().autoDetectIndex(GoRouter.maybeOf(context));
|
||||||
.read<NavigationProvider>()
|
|
||||||
.autoDetectIndex(GoRouter.maybeOf(context));
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,11 +29,11 @@ class _AppRailNavigationState extends State<AppRailNavigation> {
|
|||||||
return ListenableBuilder(
|
return ListenableBuilder(
|
||||||
listenable: nav,
|
listenable: nav,
|
||||||
builder: (context, _) {
|
builder: (context, _) {
|
||||||
final destinations =
|
final destinations = nav.destinations.where((ele) => ele.isPinned).toList();
|
||||||
nav.destinations.where((ele) => ele.isPinned).toList();
|
|
||||||
|
|
||||||
return NavigationRail(
|
return NavigationRail(
|
||||||
selectedIndex: nav.currentIndex,
|
selectedIndex:
|
||||||
|
nav.currentIndex != null && nav.currentIndex! < nav.pinnedDestinationCount ? nav.currentIndex : null,
|
||||||
destinations: [
|
destinations: [
|
||||||
...destinations.where((ele) => ele.isPinned).map((ele) {
|
...destinations.where((ele) => ele.isPinned).map((ele) {
|
||||||
return NavigationRailDestination(
|
return NavigationRailDestination(
|
||||||
|
|||||||
@@ -1,51 +1,94 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
import 'package:bitsdojo_window/bitsdojo_window.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:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
import 'package:responsive_framework/responsive_framework.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/config.dart';
|
||||||
import 'package:surface/providers/navigation.dart';
|
import 'package:surface/providers/navigation.dart';
|
||||||
import 'package:surface/widgets/connection_indicator.dart';
|
import 'package:surface/widgets/connection_indicator.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';
|
import 'package:surface/widgets/navigation/app_drawer_navigation.dart';
|
||||||
import 'package:surface/widgets/navigation/app_rail_navigation.dart';
|
import 'package:surface/widgets/navigation/app_rail_navigation.dart';
|
||||||
|
import 'package:surface/widgets/notify_indicator.dart';
|
||||||
|
|
||||||
final globalRootScaffoldKey = GlobalKey<ScaffoldState>();
|
final globalRootScaffoldKey = GlobalKey<ScaffoldState>();
|
||||||
|
|
||||||
class AppPageScaffold extends StatelessWidget {
|
class AppScaffold extends StatelessWidget {
|
||||||
final String? title;
|
|
||||||
final Widget? body;
|
final Widget? body;
|
||||||
final bool showAppBar;
|
final PreferredSizeWidget? bottomNavigationBar;
|
||||||
final bool showBottomNavigation;
|
final PreferredSizeWidget? bottomSheet;
|
||||||
|
final Drawer? drawer;
|
||||||
|
final Widget? endDrawer;
|
||||||
|
final FloatingActionButtonAnimator? floatingActionButtonAnimator;
|
||||||
|
final FloatingActionButtonLocation? floatingActionButtonLocation;
|
||||||
|
final Widget? floatingActionButton;
|
||||||
|
final AppBar? appBar;
|
||||||
|
final DrawerCallback? onDrawerChanged;
|
||||||
|
final DrawerCallback? onEndDrawerChanged;
|
||||||
|
|
||||||
const AppPageScaffold({
|
const AppScaffold({
|
||||||
super.key,
|
super.key,
|
||||||
this.title,
|
this.appBar,
|
||||||
this.body,
|
this.body,
|
||||||
this.showAppBar = true,
|
this.floatingActionButton,
|
||||||
this.showBottomNavigation = false,
|
this.floatingActionButtonLocation,
|
||||||
|
this.floatingActionButtonAnimator,
|
||||||
|
this.bottomNavigationBar,
|
||||||
|
this.bottomSheet,
|
||||||
|
this.drawer,
|
||||||
|
this.endDrawer,
|
||||||
|
this.onDrawerChanged,
|
||||||
|
this.onEndDrawerChanged,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final state = GoRouter.maybeOf(context);
|
final appBarHeight = appBar?.preferredSize.height ?? 0;
|
||||||
final routeName = state?.routerDelegate.currentConfiguration.last.route.name;
|
final safeTop = MediaQuery.of(context).padding.top;
|
||||||
|
|
||||||
final autoTitle = state != null ? 'screen${routeName?.capitalize()}' : 'screen';
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: showAppBar
|
extendBody: true,
|
||||||
? AppBar(
|
extendBodyBehindAppBar: true,
|
||||||
title: Text(title ?? autoTitle.tr()),
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
)
|
body: SizedBox.expand(
|
||||||
: null,
|
child: AppBackground(
|
||||||
body: body,
|
child: Column(
|
||||||
|
children: [
|
||||||
|
IgnorePointer(child: SizedBox(height: appBar != null ? appBarHeight + safeTop : 0)),
|
||||||
|
if (body != null) Expanded(child: body!),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
appBar: appBar,
|
||||||
|
bottomNavigationBar: bottomNavigationBar,
|
||||||
|
bottomSheet: bottomSheet,
|
||||||
|
drawer: drawer,
|
||||||
|
endDrawer: endDrawer,
|
||||||
|
floatingActionButton: floatingActionButton,
|
||||||
|
floatingActionButtonAnimator: floatingActionButtonAnimator,
|
||||||
|
floatingActionButtonLocation: floatingActionButtonLocation,
|
||||||
|
onDrawerChanged: onDrawerChanged,
|
||||||
|
onEndDrawerChanged: onEndDrawerChanged,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PageBackButton extends StatelessWidget {
|
||||||
|
const PageBackButton({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BackButton(
|
||||||
|
onPressed: () {
|
||||||
|
GoRouter.of(context).pop();
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -57,10 +100,11 @@ class AppRootScaffold extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final cfg = context.watch<ConfigProvider>();
|
||||||
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||||
|
|
||||||
final isCollapseDrawer = ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE);
|
final isCollapseDrawer = cfg.drawerIsCollapsed;
|
||||||
final isExpandDrawer = ResponsiveBreakpoints.of(context).largerThan(TABLET);
|
final isExpandedDrawer = cfg.drawerIsExpanded;
|
||||||
|
|
||||||
final routeName = GoRouter.of(context).routerDelegate.currentConfiguration.last.route.name;
|
final routeName = GoRouter.of(context).routerDelegate.currentConfiguration.last.route.name;
|
||||||
final isShowBottomNavigation = NavigationProvider.kShowBottomNavScreen.contains(routeName)
|
final isShowBottomNavigation = NavigationProvider.kShowBottomNavScreen.contains(routeName)
|
||||||
@@ -81,7 +125,7 @@ class AppRootScaffold extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: isExpandDrawer ? AppNavigationDrawer(elevation: 0) : AppRailNavigation(),
|
child: isExpandedDrawer ? AppNavigationDrawer(elevation: 0) : AppRailNavigation(),
|
||||||
),
|
),
|
||||||
Expanded(child: body),
|
Expanded(child: body),
|
||||||
],
|
],
|
||||||
@@ -95,62 +139,64 @@ class AppRootScaffold extends StatelessWidget {
|
|||||||
iconMouseDown: Theme.of(context).colorScheme.primary,
|
iconMouseDown: Theme.of(context).colorScheme.primary,
|
||||||
);
|
);
|
||||||
|
|
||||||
return AppBackground(
|
final safeTop = MediaQuery.of(context).padding.top;
|
||||||
isRoot: true,
|
|
||||||
child: Scaffold(
|
return Scaffold(
|
||||||
key: globalRootScaffoldKey,
|
key: globalRootScaffoldKey,
|
||||||
body: Column(
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
children: [
|
body: Stack(
|
||||||
if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS))
|
children: [
|
||||||
Container(
|
Column(
|
||||||
decoration: BoxDecoration(
|
children: [
|
||||||
border: Border(
|
if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS))
|
||||||
bottom: BorderSide(
|
WindowTitleBarBox(
|
||||||
color: Theme.of(context).dividerColor,
|
child: Container(
|
||||||
width: 1 / devicePixelRatio,
|
decoration: BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
bottom: BorderSide(
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
width: 1 / devicePixelRatio,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: MoveWindow(
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
mainAxisAlignment: Platform.isMacOS ? MainAxisAlignment.center : MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Solar Network',
|
||||||
|
style: GoogleFonts.spaceGrotesk(),
|
||||||
|
).padding(horizontal: 12, vertical: 5),
|
||||||
|
if (!Platform.isMacOS)
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Expanded(child: MoveWindow()),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
MinimizeWindowButton(colors: windowButtonColor),
|
||||||
|
MaximizeWindowButton(colors: windowButtonColor),
|
||||||
|
CloseWindowButton(colors: windowButtonColor),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Row(
|
Expanded(child: innerWidget),
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
],
|
||||||
mainAxisAlignment: Platform.isMacOS ? MainAxisAlignment.center : MainAxisAlignment.start,
|
),
|
||||||
children: [
|
Positioned(top: safeTop > 0 ? safeTop : 16, right: 8, child: NotifyIndicator()),
|
||||||
WindowTitleBarBox(
|
Positioned(top: safeTop > 0 ? safeTop : 16, left: 8, child: ConnectionIndicator()),
|
||||||
child: MoveWindow(
|
],
|
||||||
child: Text(
|
|
||||||
'Solar Network',
|
|
||||||
style: GoogleFonts.spaceGrotesk(),
|
|
||||||
).padding(horizontal: 12, vertical: 5),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (!Platform.isMacOS)
|
|
||||||
Expanded(
|
|
||||||
child: WindowTitleBarBox(
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Expanded(child: MoveWindow()),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
MinimizeWindowButton(colors: windowButtonColor),
|
|
||||||
MaximizeWindowButton(colors: windowButtonColor),
|
|
||||||
CloseWindowButton(colors: windowButtonColor),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ConnectionIndicator(),
|
|
||||||
Expanded(child: innerWidget),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
drawer: !isExpandDrawer ? AppNavigationDrawer() : null,
|
|
||||||
drawerEdgeDragWidth: isPopable ? 0 : null,
|
|
||||||
bottomNavigationBar: isShowBottomNavigation ? AppBottomNavigationBar() : null,
|
|
||||||
),
|
),
|
||||||
|
drawer: !isExpandedDrawer ? AppNavigationDrawer() : null,
|
||||||
|
drawerEdgeDragWidth: isPopable ? 0 : null,
|
||||||
|
bottomNavigationBar: isShowBottomNavigation ? AppBottomNavigationBar() : null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
63
lib/widgets/notify_indicator.dart
Normal file
63
lib/widgets/notify_indicator.dart
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
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/notification.dart';
|
||||||
|
import 'package:surface/providers/userinfo.dart';
|
||||||
|
|
||||||
|
class NotifyIndicator extends StatelessWidget {
|
||||||
|
const NotifyIndicator({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final ua = context.read<UserProvider>();
|
||||||
|
final nty = context.watch<NotificationProvider>();
|
||||||
|
|
||||||
|
final show = nty.notifications.isNotEmpty && ua.isAuthorized;
|
||||||
|
|
||||||
|
return ListenableBuilder(
|
||||||
|
listenable: nty,
|
||||||
|
builder: (context, _) {
|
||||||
|
return IgnorePointer(
|
||||||
|
ignoring: !show,
|
||||||
|
child: GestureDetector(
|
||||||
|
child: Material(
|
||||||
|
elevation: 2,
|
||||||
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))),
|
||||||
|
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
|
child: ua.isAuthorized
|
||||||
|
? Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
nty.notifications.lastOrNull?.title ??
|
||||||
|
'notificationUnreadCount'.plural(nty.notifications.length),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
if (nty.notifications.lastOrNull?.body != null)
|
||||||
|
Text(
|
||||||
|
nty.notifications.lastOrNull!.body,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
).padding(left: 4),
|
||||||
|
const Gap(8),
|
||||||
|
const Icon(Symbols.notifications_unread, size: 18),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 8, vertical: 4)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
).opacity(show ? 1 : 0, animate: true).animate(
|
||||||
|
const Duration(milliseconds: 300),
|
||||||
|
Curves.easeInOut,
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
nty.clear();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,9 +18,9 @@ import 'package:screenshot/screenshot.dart';
|
|||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:surface/providers/config.dart';
|
import 'package:surface/providers/config.dart';
|
||||||
import 'package:surface/providers/link_preview.dart';
|
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
import 'package:surface/providers/userinfo.dart';
|
import 'package:surface/providers/userinfo.dart';
|
||||||
|
import 'package:surface/types/attachment.dart';
|
||||||
import 'package:surface/types/post.dart';
|
import 'package:surface/types/post.dart';
|
||||||
import 'package:surface/types/reaction.dart';
|
import 'package:surface/types/reaction.dart';
|
||||||
import 'package:surface/widgets/account/account_image.dart';
|
import 'package:surface/widgets/account/account_image.dart';
|
||||||
@@ -85,7 +85,6 @@ class PostItem extends StatelessWidget {
|
|||||||
child: MultiProvider(
|
child: MultiProvider(
|
||||||
providers: [
|
providers: [
|
||||||
Provider<SnNetworkProvider>(create: (_) => context.read()),
|
Provider<SnNetworkProvider>(create: (_) => context.read()),
|
||||||
Provider<SnLinkPreviewProvider>(create: (_) => context.read()),
|
|
||||||
ChangeNotifierProvider<ConfigProvider>(create: (_) => context.read()),
|
ChangeNotifierProvider<ConfigProvider>(create: (_) => context.read()),
|
||||||
],
|
],
|
||||||
child: ResponsiveBreakpoints.builder(
|
child: ResponsiveBreakpoints.builder(
|
||||||
@@ -114,7 +113,7 @@ class PostItem extends StatelessWidget {
|
|||||||
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
|
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await FileSaver.instance.saveFile(name: 'Solar Network Post #${data.id}', file: imageFile);
|
await FileSaver.instance.saveFile(name: 'Solar Network Post #${data.id}.png', file: imageFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
await imageFile.delete();
|
await imageFile.delete();
|
||||||
@@ -200,6 +199,12 @@ class PostItem extends StatelessWidget {
|
|||||||
).center();
|
).center();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final displayableAttachments = data.preload?.attachments
|
||||||
|
?.where((ele) => ele?.mediaType != SnMediaType.image || data.type != 'article')
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final cfg = context.read<ConfigProvider>();
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
@@ -249,14 +254,16 @@ class PostItem extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if ((data.preload?.attachments?.isNotEmpty ?? false) && data.type != 'article')
|
if (displayableAttachments?.isNotEmpty ?? false)
|
||||||
AttachmentList(
|
AttachmentList(
|
||||||
data: data.preload!.attachments!,
|
data: displayableAttachments!,
|
||||||
bordered: true,
|
bordered: true,
|
||||||
maxHeight: 560,
|
maxHeight: showFullPost ? null : 480,
|
||||||
listPadding: const EdgeInsets.symmetric(horizontal: 12),
|
maxWidth: MediaQuery.of(context).size.width - 20,
|
||||||
|
fit: showFullPost ? BoxFit.cover : BoxFit.contain,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
),
|
),
|
||||||
if (data.body['content'] != null)
|
if (data.body['content'] != null && (cfg.prefs.getBool(kAppExpandPostLink) ?? true))
|
||||||
LinkPreviewWidget(
|
LinkPreviewWidget(
|
||||||
text: data.body['content'],
|
text: data.body['content'],
|
||||||
).padding(horizontal: 4),
|
).padding(horizontal: 4),
|
||||||
@@ -334,17 +341,12 @@ class PostShareImageWidget extends StatelessWidget {
|
|||||||
_PostQuoteContent(
|
_PostQuoteContent(
|
||||||
child: data.repostTo!,
|
child: data.repostTo!,
|
||||||
isRelativeDate: false,
|
isRelativeDate: false,
|
||||||
isFlatted: true,
|
|
||||||
).padding(horizontal: 16, bottom: 8),
|
).padding(horizontal: 16, bottom: 8),
|
||||||
if (data.type != 'article' && (data.preload?.attachments?.isNotEmpty ?? false))
|
if (data.type != 'article' && (data.preload?.attachments?.isNotEmpty ?? false))
|
||||||
AttachmentList(
|
StyledWidget(AttachmentList(
|
||||||
data: data.preload!.attachments!,
|
data: data.preload!.attachments!,
|
||||||
isFlatted: true,
|
columned: true,
|
||||||
).padding(horizontal: 16, bottom: 8),
|
)).padding(horizontal: 16, bottom: 8),
|
||||||
if (data.body['content'] != null)
|
|
||||||
LinkPreviewWidget(
|
|
||||||
text: data.body['content'],
|
|
||||||
).padding(horizontal: 4),
|
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@@ -878,23 +880,27 @@ class _PostContentBody extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (data.body['content'] == null) return const SizedBox.shrink();
|
if (data.body['content'] == null) return const SizedBox.shrink();
|
||||||
return MarkdownTextContent(
|
final content = MarkdownTextContent(
|
||||||
isSelectable: isSelectable,
|
isAutoWarp: data.type == 'story',
|
||||||
|
isEnlargeSticker: true,
|
||||||
textScaler: isEnlarge ? TextScaler.linear(1.1) : null,
|
textScaler: isEnlarge ? TextScaler.linear(1.1) : null,
|
||||||
content: data.body['content'],
|
content: data.body['content'],
|
||||||
attachments: data.preload?.attachments,
|
attachments: data.preload?.attachments,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (isSelectable) {
|
||||||
|
return SelectionArea(child: content);
|
||||||
|
}
|
||||||
|
return content;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PostQuoteContent extends StatelessWidget {
|
class _PostQuoteContent extends StatelessWidget {
|
||||||
final SnPost child;
|
final SnPost child;
|
||||||
final bool isRelativeDate;
|
final bool isRelativeDate;
|
||||||
final bool isFlatted;
|
|
||||||
|
|
||||||
const _PostQuoteContent({
|
const _PostQuoteContent({
|
||||||
this.isRelativeDate = true,
|
this.isRelativeDate = true,
|
||||||
this.isFlatted = false,
|
|
||||||
required this.child,
|
required this.child,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -936,12 +942,15 @@ class _PostQuoteContent extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
child: AttachmentList(
|
child: AttachmentList(
|
||||||
data: child.preload!.attachments!,
|
data: child.preload!.attachments!,
|
||||||
isFlatted: isFlatted,
|
maxHeight: 360,
|
||||||
listPadding: const EdgeInsets.symmetric(horizontal: 12),
|
minWidth: 640,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
gridded: true,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
),
|
),
|
||||||
).padding(
|
).padding(
|
||||||
top: 8,
|
top: 8,
|
||||||
bottom: (child.preload?.attachments?.length ?? 0) > 1 ? 12 : 0,
|
bottom: 12,
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ 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_input.dart';
|
import 'package:surface/widgets/attachment/attachment_input.dart';
|
||||||
import 'package:surface/widgets/attachment/attachment_zoom.dart';
|
import 'package:surface/widgets/attachment/attachment_zoom.dart';
|
||||||
|
import 'package:surface/widgets/attachment/pending_attachment_alt.dart';
|
||||||
|
import 'package:surface/widgets/attachment/pending_attachment_boost.dart';
|
||||||
import 'package:surface/widgets/context_menu.dart';
|
import 'package:surface/widgets/context_menu.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/universal_image.dart';
|
import 'package:surface/widgets/universal_image.dart';
|
||||||
@@ -93,18 +95,23 @@ class PostMediaPendingList extends StatelessWidget {
|
|||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AttachmentInputDialog(
|
builder: (context) => AttachmentInputDialog(
|
||||||
title: 'attachmentSetThumbnail'.tr(),
|
title: 'attachmentSetThumbnail'.tr(),
|
||||||
|
analyzeNow: true,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (thumbnail == null) return;
|
if (thumbnail == null) return;
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
|
|
||||||
final attach = context.read<SnAttachmentProvider>();
|
try {
|
||||||
final newAttach = await attach.updateOne(
|
final attach = context.read<SnAttachmentProvider>();
|
||||||
attachments[idx].attachment!,
|
final newAttach = await attach.updateOne(
|
||||||
thumbnailId: thumbnail.id,
|
attachments[idx].attachment!,
|
||||||
);
|
thumbnailId: thumbnail.id,
|
||||||
|
);
|
||||||
onUpdate!(idx, PostWriteMedia(newAttach));
|
onUpdate!(idx, PostWriteMedia(newAttach));
|
||||||
|
} catch (err) {
|
||||||
|
if (!context.mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _deleteAttachment(BuildContext context, int idx) async {
|
Future<void> _deleteAttachment(BuildContext context, int idx) async {
|
||||||
@@ -124,6 +131,23 @@ class PostMediaPendingList extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _createBoost(BuildContext context, int idx) async {
|
||||||
|
if (attachments[idx].attachment == null) return;
|
||||||
|
|
||||||
|
final result = await showDialog<SnAttachmentBoost?>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => PendingAttachmentBoostDialog(media: attachments[idx]),
|
||||||
|
);
|
||||||
|
if (result == null) return;
|
||||||
|
|
||||||
|
final newAttach = attachments[idx].attachment!.copyWith(
|
||||||
|
boosts: [...attachments[idx].attachment!.boosts, result],
|
||||||
|
);
|
||||||
|
final newMedia = PostWriteMedia(newAttach);
|
||||||
|
|
||||||
|
onUpdate!(idx, newMedia);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _compressVideo(BuildContext context, int idx) async {
|
Future<void> _compressVideo(BuildContext context, int idx) async {
|
||||||
final result = await showDialog<PostWriteMedia?>(
|
final result = await showDialog<PostWriteMedia?>(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -134,6 +158,16 @@ class PostMediaPendingList extends StatelessWidget {
|
|||||||
onUpdate!(idx, result);
|
onUpdate!(idx, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _setAlt(BuildContext context, int idx) async {
|
||||||
|
final result = await showDialog<SnAttachment?>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => PendingAttachmentAltDialog(media: attachments[idx]),
|
||||||
|
);
|
||||||
|
if (result == null) return;
|
||||||
|
|
||||||
|
onUpdate!(idx, PostWriteMedia(result));
|
||||||
|
}
|
||||||
|
|
||||||
ContextMenu _createContextMenu(BuildContext context, int idx, PostWriteMedia media) {
|
ContextMenu _createContextMenu(BuildContext context, int idx, PostWriteMedia media) {
|
||||||
final canCompressVideo = !kIsWeb && (Platform.isAndroid || Platform.isIOS || Platform.isMacOS);
|
final canCompressVideo = !kIsWeb && (Platform.isAndroid || Platform.isIOS || Platform.isMacOS);
|
||||||
return ContextMenu(
|
return ContextMenu(
|
||||||
@@ -146,6 +180,22 @@ class PostMediaPendingList extends StatelessWidget {
|
|||||||
_compressVideo(context, idx);
|
_compressVideo(context, idx);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
if (media.attachment != null)
|
||||||
|
MenuItem(
|
||||||
|
label: 'attachmentSetAlt'.tr(),
|
||||||
|
icon: Symbols.description,
|
||||||
|
onSelected: () {
|
||||||
|
_setAlt(context, idx);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (media.attachment != null)
|
||||||
|
MenuItem(
|
||||||
|
label: 'attachmentBoost'.tr(),
|
||||||
|
icon: Symbols.bolt,
|
||||||
|
onSelected: () {
|
||||||
|
_createBoost(context, idx);
|
||||||
|
},
|
||||||
|
),
|
||||||
if (media.attachment != null && media.type == SnMediaType.video)
|
if (media.attachment != null && media.type == SnMediaType.video)
|
||||||
MenuItem(
|
MenuItem(
|
||||||
label: 'attachmentSetThumbnail'.tr(),
|
label: 'attachmentSetThumbnail'.tr(),
|
||||||
@@ -389,11 +439,19 @@ class _PostMediaPendingItem extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (media.attachment != null && media.attachment!.compressedId != null)
|
if (media.attachment != null && media.attachment!.boosts.isNotEmpty)
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Symbols.bolt, size: 16),
|
Icon(Symbols.bolt, size: 16),
|
||||||
const Gap(4),
|
const Gap(4),
|
||||||
|
Text('attachmentGotBoosted').tr().fontSize(13),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (media.attachment != null && media.attachment!.compressedId != null)
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(Symbols.compress, size: 16),
|
||||||
|
const Gap(4),
|
||||||
Text('attachmentCopyCompressed').tr().fontSize(13),
|
Text('attachmentCopyCompressed').tr().fontSize(13),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class PostMiniEditor extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _PostMiniEditorState extends State<PostMiniEditor> {
|
class _PostMiniEditorState extends State<PostMiniEditor> {
|
||||||
final PostWriteController _writeController = PostWriteController();
|
final PostWriteController _writeController = PostWriteController(doLoadFromTemporary: false);
|
||||||
|
|
||||||
bool _isFetching = false;
|
bool _isFetching = false;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.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:provider/provider.dart';
|
||||||
@@ -12,6 +13,7 @@ import 'package:surface/widgets/dialog.dart';
|
|||||||
class PostReactionPopup extends StatefulWidget {
|
class PostReactionPopup extends StatefulWidget {
|
||||||
final SnPost data;
|
final SnPost data;
|
||||||
final Function(Map<String, int> value, int attr, int delta)? onChanged;
|
final Function(Map<String, int> value, int attr, int delta)? onChanged;
|
||||||
|
|
||||||
const PostReactionPopup({super.key, required this.data, this.onChanged});
|
const PostReactionPopup({super.key, required this.data, this.onChanged});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -59,6 +61,7 @@ class _PostReactionPopupState extends State<PostReactionPopup> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
HapticFeedback.mediumImpact();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
if (context.mounted) context.showErrorDialog(err);
|
if (context.mounted) context.showErrorDialog(err);
|
||||||
@@ -84,9 +87,7 @@ class _PostReactionPopupState extends State<PostReactionPopup> {
|
|||||||
children: [
|
children: [
|
||||||
const Icon(Symbols.mood, size: 24),
|
const Icon(Symbols.mood, size: 24),
|
||||||
const Gap(16),
|
const Gap(16),
|
||||||
Text('postReactions')
|
Text('postReactions').tr().textStyle(Theme.of(context).textTheme.titleLarge!),
|
||||||
.tr()
|
|
||||||
.textStyle(Theme.of(context).textTheme.titleLarge!),
|
|
||||||
],
|
],
|
||||||
).padding(horizontal: 20, top: 16, bottom: 12),
|
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||||
Container(
|
Container(
|
||||||
@@ -102,9 +103,7 @@ class _PostReactionPopupState extends State<PostReactionPopup> {
|
|||||||
Text('postReactionDownvote').plural(widget.data.totalDownvote),
|
Text('postReactionDownvote').plural(widget.data.totalDownvote),
|
||||||
const Gap(24),
|
const Gap(24),
|
||||||
Icon(
|
Icon(
|
||||||
widget.data.totalUpvote >= widget.data.totalDownvote
|
widget.data.totalUpvote >= widget.data.totalDownvote ? Symbols.trending_up : Symbols.trending_down,
|
||||||
? Symbols.trending_up
|
|
||||||
: Symbols.trending_down,
|
|
||||||
size: 16,
|
size: 16,
|
||||||
),
|
),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
|
|||||||
@@ -55,17 +55,20 @@ class UniversalImage extends StatelessWidget {
|
|||||||
? null
|
? null
|
||||||
: (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) {
|
: (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) {
|
||||||
if (loadingProgress == null) return child;
|
if (loadingProgress == null) return child;
|
||||||
return Center(
|
return Container(
|
||||||
child: TweenAnimationBuilder(
|
constraints: BoxConstraints(maxHeight: 80),
|
||||||
tween: Tween(
|
child: Center(
|
||||||
begin: 0,
|
child: TweenAnimationBuilder(
|
||||||
end: loadingProgress.expectedTotalBytes != null
|
tween: Tween(
|
||||||
? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes!
|
begin: 0,
|
||||||
: 0,
|
end: loadingProgress.expectedTotalBytes != null
|
||||||
),
|
? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes!
|
||||||
duration: const Duration(milliseconds: 300),
|
: 0,
|
||||||
builder: (context, value, _) => CircularProgressIndicator(
|
),
|
||||||
value: loadingProgress.expectedTotalBytes != null ? value.toDouble() : null,
|
duration: const Duration(milliseconds: 300),
|
||||||
|
builder: (context, value, _) => CircularProgressIndicator(
|
||||||
|
value: loadingProgress.expectedTotalBytes != null ? value.toDouble() : null,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import file_selector_macos
|
|||||||
import firebase_analytics
|
import firebase_analytics
|
||||||
import firebase_core
|
import firebase_core
|
||||||
import firebase_messaging
|
import firebase_messaging
|
||||||
|
import flutter_inappwebview_macos
|
||||||
import flutter_udid
|
import flutter_udid
|
||||||
import flutter_webrtc
|
import flutter_webrtc
|
||||||
import gal
|
import gal
|
||||||
@@ -40,6 +41,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
FLTFirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAnalyticsPlugin"))
|
FLTFirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAnalyticsPlugin"))
|
||||||
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
|
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
|
||||||
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
|
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
|
||||||
|
InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
|
||||||
FlutterUdidPlugin.register(with: registry.registrar(forPlugin: "FlutterUdidPlugin"))
|
FlutterUdidPlugin.register(with: registry.registrar(forPlugin: "FlutterUdidPlugin"))
|
||||||
FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin"))
|
FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin"))
|
||||||
GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin"))
|
GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin"))
|
||||||
|
|||||||
@@ -12,59 +12,59 @@ PODS:
|
|||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- file_selector_macos (0.0.1):
|
- file_selector_macos (0.0.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- Firebase/Analytics (11.4.0):
|
- Firebase/Analytics (11.6.0):
|
||||||
- Firebase/Core
|
- Firebase/Core
|
||||||
- Firebase/Core (11.4.0):
|
- Firebase/Core (11.6.0):
|
||||||
- Firebase/CoreOnly
|
- Firebase/CoreOnly
|
||||||
- FirebaseAnalytics (~> 11.4.0)
|
- FirebaseAnalytics (~> 11.6.0)
|
||||||
- Firebase/CoreOnly (11.4.0):
|
- Firebase/CoreOnly (11.6.0):
|
||||||
- FirebaseCore (= 11.4.0)
|
- FirebaseCore (~> 11.6.0)
|
||||||
- Firebase/Messaging (11.4.0):
|
- Firebase/Messaging (11.6.0):
|
||||||
- Firebase/CoreOnly
|
- Firebase/CoreOnly
|
||||||
- FirebaseMessaging (~> 11.4.0)
|
- FirebaseMessaging (~> 11.6.0)
|
||||||
- firebase_analytics (11.3.6):
|
- firebase_analytics (11.4.0):
|
||||||
- Firebase/Analytics (= 11.4.0)
|
- Firebase/Analytics (= 11.6.0)
|
||||||
- firebase_core
|
- firebase_core
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- firebase_core (3.9.0):
|
- firebase_core (3.10.0):
|
||||||
- Firebase/CoreOnly (~> 11.4.0)
|
- Firebase/CoreOnly (~> 11.6.0)
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- firebase_messaging (15.1.6):
|
- firebase_messaging (15.2.0):
|
||||||
- Firebase/CoreOnly (~> 11.4.0)
|
- Firebase/CoreOnly (~> 11.6.0)
|
||||||
- Firebase/Messaging (~> 11.4.0)
|
- Firebase/Messaging (~> 11.6.0)
|
||||||
- firebase_core
|
- firebase_core
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- FirebaseAnalytics (11.4.0):
|
- FirebaseAnalytics (11.6.0):
|
||||||
- FirebaseAnalytics/AdIdSupport (= 11.4.0)
|
- FirebaseAnalytics/AdIdSupport (= 11.6.0)
|
||||||
- FirebaseCore (~> 11.0)
|
- FirebaseCore (~> 11.6.0)
|
||||||
- FirebaseInstallations (~> 11.0)
|
- FirebaseInstallations (~> 11.0)
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||||
- GoogleUtilities/Network (~> 8.0)
|
- GoogleUtilities/Network (~> 8.0)
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- FirebaseAnalytics/AdIdSupport (11.4.0):
|
- FirebaseAnalytics/AdIdSupport (11.6.0):
|
||||||
- FirebaseCore (~> 11.0)
|
- FirebaseCore (~> 11.6.0)
|
||||||
- FirebaseInstallations (~> 11.0)
|
- FirebaseInstallations (~> 11.0)
|
||||||
- GoogleAppMeasurement (= 11.4.0)
|
- GoogleAppMeasurement (= 11.6.0)
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||||
- GoogleUtilities/Network (~> 8.0)
|
- GoogleUtilities/Network (~> 8.0)
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- FirebaseCore (11.4.0):
|
- FirebaseCore (11.6.0):
|
||||||
- FirebaseCoreInternal (~> 11.0)
|
- FirebaseCoreInternal (~> 11.6.0)
|
||||||
- GoogleUtilities/Environment (~> 8.0)
|
- GoogleUtilities/Environment (~> 8.0)
|
||||||
- GoogleUtilities/Logger (~> 8.0)
|
- GoogleUtilities/Logger (~> 8.0)
|
||||||
- FirebaseCoreInternal (11.6.0):
|
- FirebaseCoreInternal (11.6.0):
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||||
- FirebaseInstallations (11.4.0):
|
- FirebaseInstallations (11.6.0):
|
||||||
- FirebaseCore (~> 11.0)
|
- FirebaseCore (~> 11.6.0)
|
||||||
- GoogleUtilities/Environment (~> 8.0)
|
- GoogleUtilities/Environment (~> 8.0)
|
||||||
- GoogleUtilities/UserDefaults (~> 8.0)
|
- GoogleUtilities/UserDefaults (~> 8.0)
|
||||||
- PromisesObjC (~> 2.4)
|
- PromisesObjC (~> 2.4)
|
||||||
- FirebaseMessaging (11.4.0):
|
- FirebaseMessaging (11.6.0):
|
||||||
- FirebaseCore (~> 11.0)
|
- FirebaseCore (~> 11.6.0)
|
||||||
- FirebaseInstallations (~> 11.0)
|
- FirebaseInstallations (~> 11.0)
|
||||||
- GoogleDataTransport (~> 10.0)
|
- GoogleDataTransport (~> 10.0)
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||||
@@ -72,31 +72,34 @@ PODS:
|
|||||||
- GoogleUtilities/Reachability (~> 8.0)
|
- GoogleUtilities/Reachability (~> 8.0)
|
||||||
- GoogleUtilities/UserDefaults (~> 8.0)
|
- GoogleUtilities/UserDefaults (~> 8.0)
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
|
- flutter_inappwebview_macos (0.0.1):
|
||||||
|
- FlutterMacOS
|
||||||
|
- OrderedSet (~> 6.0.3)
|
||||||
- flutter_udid (0.0.1):
|
- flutter_udid (0.0.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- SAMKeychain
|
- SAMKeychain
|
||||||
- flutter_webrtc (0.12.2):
|
- flutter_webrtc (0.12.6):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- WebRTC-SDK (= 125.6422.06)
|
- WebRTC-SDK (= 125.6422.06)
|
||||||
- FlutterMacOS (1.0.0)
|
- FlutterMacOS (1.0.0)
|
||||||
- gal (1.0.0):
|
- gal (1.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- GoogleAppMeasurement (11.4.0):
|
- GoogleAppMeasurement (11.6.0):
|
||||||
- GoogleAppMeasurement/AdIdSupport (= 11.4.0)
|
- GoogleAppMeasurement/AdIdSupport (= 11.6.0)
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||||
- GoogleUtilities/Network (~> 8.0)
|
- GoogleUtilities/Network (~> 8.0)
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- GoogleAppMeasurement/AdIdSupport (11.4.0):
|
- GoogleAppMeasurement/AdIdSupport (11.6.0):
|
||||||
- GoogleAppMeasurement/WithoutAdIdSupport (= 11.4.0)
|
- GoogleAppMeasurement/WithoutAdIdSupport (= 11.6.0)
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||||
- GoogleUtilities/Network (~> 8.0)
|
- GoogleUtilities/Network (~> 8.0)
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- GoogleAppMeasurement/WithoutAdIdSupport (11.4.0):
|
- GoogleAppMeasurement/WithoutAdIdSupport (11.6.0):
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||||
- GoogleUtilities/Network (~> 8.0)
|
- GoogleUtilities/Network (~> 8.0)
|
||||||
@@ -134,7 +137,7 @@ PODS:
|
|||||||
- GoogleUtilities/Privacy
|
- GoogleUtilities/Privacy
|
||||||
- in_app_review (2.0.0):
|
- in_app_review (2.0.0):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- livekit_client (2.3.3):
|
- livekit_client (2.3.5):
|
||||||
- flutter_webrtc
|
- flutter_webrtc
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- WebRTC-SDK (= 125.6422.06)
|
- WebRTC-SDK (= 125.6422.06)
|
||||||
@@ -149,6 +152,7 @@ PODS:
|
|||||||
- nanopb/encode (= 3.30910.0)
|
- nanopb/encode (= 3.30910.0)
|
||||||
- nanopb/decode (3.30910.0)
|
- nanopb/decode (3.30910.0)
|
||||||
- nanopb/encode (3.30910.0)
|
- nanopb/encode (3.30910.0)
|
||||||
|
- OrderedSet (6.0.3)
|
||||||
- package_info_plus (0.0.1):
|
- package_info_plus (0.0.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- pasteboard (0.0.1):
|
- pasteboard (0.0.1):
|
||||||
@@ -186,6 +190,7 @@ DEPENDENCIES:
|
|||||||
- firebase_analytics (from `Flutter/ephemeral/.symlinks/plugins/firebase_analytics/macos`)
|
- firebase_analytics (from `Flutter/ephemeral/.symlinks/plugins/firebase_analytics/macos`)
|
||||||
- firebase_core (from `Flutter/ephemeral/.symlinks/plugins/firebase_core/macos`)
|
- firebase_core (from `Flutter/ephemeral/.symlinks/plugins/firebase_core/macos`)
|
||||||
- firebase_messaging (from `Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos`)
|
- firebase_messaging (from `Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos`)
|
||||||
|
- flutter_inappwebview_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos`)
|
||||||
- flutter_udid (from `Flutter/ephemeral/.symlinks/plugins/flutter_udid/macos`)
|
- flutter_udid (from `Flutter/ephemeral/.symlinks/plugins/flutter_udid/macos`)
|
||||||
- flutter_webrtc (from `Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos`)
|
- flutter_webrtc (from `Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos`)
|
||||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||||
@@ -218,6 +223,7 @@ SPEC REPOS:
|
|||||||
- GoogleDataTransport
|
- GoogleDataTransport
|
||||||
- GoogleUtilities
|
- GoogleUtilities
|
||||||
- nanopb
|
- nanopb
|
||||||
|
- OrderedSet
|
||||||
- PromisesObjC
|
- PromisesObjC
|
||||||
- SAMKeychain
|
- SAMKeychain
|
||||||
- WebRTC-SDK
|
- WebRTC-SDK
|
||||||
@@ -241,6 +247,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: Flutter/ephemeral/.symlinks/plugins/firebase_core/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/firebase_core/macos
|
||||||
firebase_messaging:
|
firebase_messaging:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos
|
||||||
|
flutter_inappwebview_macos:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos
|
||||||
flutter_udid:
|
flutter_udid:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/flutter_udid/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/flutter_udid/macos
|
||||||
flutter_webrtc:
|
flutter_webrtc:
|
||||||
@@ -287,28 +295,30 @@ SPEC CHECKSUMS:
|
|||||||
device_info_plus: 1b14eed9bf95428983aed283a8d51cce3d8c4215
|
device_info_plus: 1b14eed9bf95428983aed283a8d51cce3d8c4215
|
||||||
file_saver: 44e6fbf666677faf097302460e214e977fdd977b
|
file_saver: 44e6fbf666677faf097302460e214e977fdd977b
|
||||||
file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d
|
file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d
|
||||||
Firebase: cf1b19f21410b029b6786a54e9764a0cacad3c99
|
Firebase: 374a441a91ead896215703a674d58cdb3e9d772b
|
||||||
firebase_analytics: a80b3d6645f2f12d626fde928b61dae12e5ea2ef
|
firebase_analytics: 5249f87da6fed852901581aab2602e0280ec2fdb
|
||||||
firebase_core: 1dfe1f4d02ad78be0277e320aa3d8384cf46231f
|
firebase_core: 6d9bb8b0ea817e8fe0d928177d42275b45fdba6f
|
||||||
firebase_messaging: 61f678060b69a7ae1013e3a939ec8e1c56ef6fcf
|
firebase_messaging: ae8e88b586e4d50abc7cac5bacf74d21967fd226
|
||||||
FirebaseAnalytics: 3feef9ae8733c567866342a1000691baaa7cad49
|
FirebaseAnalytics: 7114c698cac995602e3b1b96663473e50d54d6e7
|
||||||
FirebaseCore: e0510f1523bc0eb21653cac00792e1e2bd6f1771
|
FirebaseCore: 48b0dd707581cf9c1a1220da68223fb0a562afaa
|
||||||
FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2
|
FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2
|
||||||
FirebaseInstallations: 6ef4a1c7eb2a61ee1f74727d7f6ce2e72acf1414
|
FirebaseInstallations: efc0946fc756e4d22d8113f7c761948120322e8c
|
||||||
FirebaseMessaging: f8a160d99c2c2e5babbbcc90c4a3e15db036aee2
|
FirebaseMessaging: e1aca1fcc23e8b9eddb0e33f375ff90944623021
|
||||||
|
flutter_inappwebview_macos: bdf207b8f4ebd58e86ae06cd96b147de99a67c9b
|
||||||
flutter_udid: 2e7b3da4b5fdfba86a396b97898f5fe8f4ec1a52
|
flutter_udid: 2e7b3da4b5fdfba86a396b97898f5fe8f4ec1a52
|
||||||
flutter_webrtc: 53c9e1285ab32dfb58afb1e1471416a877e23d7a
|
flutter_webrtc: d55fd3f5c75b42940b6b4b2cf376a5797398d1f8
|
||||||
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
||||||
gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5
|
gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5
|
||||||
GoogleAppMeasurement: 987769c4ca6b968f2479fbcc9fe3ce34af454b8e
|
GoogleAppMeasurement: 6a9e6317b6a6d810ad03d4a66564ca6c4c5818a3
|
||||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||||
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
|
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
|
||||||
in_app_review: a6a031b9acd03c7d103e341aa334adf2c493fb93
|
in_app_review: a6a031b9acd03c7d103e341aa334adf2c493fb93
|
||||||
livekit_client: 8b1b90a6f2445d127a018ce93cc8cf6d8ab62982
|
livekit_client: 91c68237edede55f8891a166a28c1daec8a1e4b1
|
||||||
media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82
|
media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82
|
||||||
media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5
|
media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5
|
||||||
media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5
|
media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5
|
||||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||||
|
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
||||||
package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b
|
package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b
|
||||||
pasteboard: 9b69dba6fedbb04866be632205d532fe2f6b1d99
|
pasteboard: 9b69dba6fedbb04866be632205d532fe2f6b1d99
|
||||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||||
|
|||||||
218
pubspec.lock
218
pubspec.lock
@@ -13,10 +13,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: _flutterfire_internals
|
name: _flutterfire_internals
|
||||||
sha256: daa1d780fdecf8af925680c06c86563cdd445deea995d5c9176f1302a2b10bbe
|
sha256: "27899c95f9e7ec06c8310e6e0eac967707714b9f1450c4a58fa00ca011a4a8ae"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.48"
|
version: "1.3.49"
|
||||||
_macros:
|
_macros:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: dart
|
description: dart
|
||||||
@@ -266,10 +266,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: connectivity_plus
|
name: connectivity_plus
|
||||||
sha256: e0817759ec6d2d8e57eb234e6e57d2173931367a865850c7acea40d4b4f9c27d
|
sha256: "8a68739d3ee113e51ad35583fdf9ab82c55d09d693d3c39da1aebab87c938412"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.1.1"
|
version: "6.1.2"
|
||||||
connectivity_plus_platform_interface:
|
connectivity_plus_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -290,10 +290,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: croppy
|
name: croppy
|
||||||
sha256: "14bb40fd6c1771b093a907ddbf24df9aa49a4e6e379dd630602eb446e30ec629"
|
sha256: bf99b00023df0d7d047e04d27d496d87cbefd968f578d0bd30f342ff75570a12
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.1"
|
version: "1.3.3"
|
||||||
cross_file:
|
cross_file:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -354,18 +354,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: dbus
|
name: dbus
|
||||||
sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac"
|
sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.10"
|
version: "0.7.11"
|
||||||
device_info_plus:
|
device_info_plus:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: device_info_plus
|
name: device_info_plus
|
||||||
sha256: "4fa68e53e26ab17b70ca39f072c285562cfc1589df5bb1e9295db90f6645f431"
|
sha256: b37d37c2f912ad4e8ec694187de87d05de2a3cb82b465ff1f65f65a2d05de544
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "11.2.0"
|
version: "11.2.1"
|
||||||
device_info_plus_platform_interface:
|
device_info_plus_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -538,34 +538,34 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: firebase_analytics
|
name: firebase_analytics
|
||||||
sha256: "366140abb55418ea23060b779893fa997c2d8e1974a4d1cc4d9590933b65c5fd"
|
sha256: "498c6cb8468e348a556709c745d92a52173ab3a9b906aa0593393f0787f201ea"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "11.3.6"
|
version: "11.4.0"
|
||||||
firebase_analytics_platform_interface:
|
firebase_analytics_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: firebase_analytics_platform_interface
|
name: firebase_analytics_platform_interface
|
||||||
sha256: "8e987cf977c0c8f4ad02d9950a9b25b1a9606899f37b66a322a43af05be0246b"
|
sha256: ccbb350554e98afdb4b59852689292d194d31232a2647b5012a66622b3711df9
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.2.8"
|
version: "4.3.0"
|
||||||
firebase_analytics_web:
|
firebase_analytics_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: firebase_analytics_web
|
name: firebase_analytics_web
|
||||||
sha256: "0b64ef9060d394bba3d3b4777f49ee098efeeea7b0afb04663c956de6a3da170"
|
sha256: "68e1f18fc16482c211c658e739c25f015b202a260d9ad8249c6d3d7963b8105f"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.10+5"
|
version: "0.5.10+6"
|
||||||
firebase_core:
|
firebase_core:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: firebase_core
|
name: firebase_core
|
||||||
sha256: "15d761b95dfa2906dfcc31b7fc6fe293188533d1a3ffe78389ba9e69bd7fdbde"
|
sha256: "0307c1fde82e2b8b97e0be2dab93612aff9a72f31ebe9bfac66ed8b37ef7c568"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.9.0"
|
version: "3.10.0"
|
||||||
firebase_core_platform_interface:
|
firebase_core_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -586,26 +586,26 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: firebase_messaging
|
name: firebase_messaging
|
||||||
sha256: "151a3ee68736abf293aab66d1317ade53c88abe1db09c75a0460aebf7767bbdf"
|
sha256: "48a8a59197c1c5174060ba9aa1e0036e9b5a0d28a0cc22d19c1fcabc67fafe3c"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "15.1.6"
|
version: "15.2.0"
|
||||||
firebase_messaging_platform_interface:
|
firebase_messaging_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: firebase_messaging_platform_interface
|
name: firebase_messaging_platform_interface
|
||||||
sha256: f331ee51e40c243f90cc7bc059222dfec4e5df53125b08d31fb28961b00d2a9d
|
sha256: "9770a8e91f54296829dcaa61ce9b7c2f9ae9abbf99976dd3103a60470d5264dd"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.5.49"
|
version: "4.6.0"
|
||||||
firebase_messaging_web:
|
firebase_messaging_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: firebase_messaging_web
|
name: firebase_messaging_web
|
||||||
sha256: efaf3fdc54cd77e0eedb8e75f7f01c808828c64d052ddbf94d3009974e47d30f
|
sha256: "329ca4ef45ec616abe6f1d5e58feed0934a50840a65aa327052354ad3c64ed77"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.9.5"
|
version: "3.10.0"
|
||||||
fixnum:
|
fixnum:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -618,10 +618,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: fl_chart
|
name: fl_chart
|
||||||
sha256: c724234b05e378383e958f3e82ca84a3e1e3c06a0898462044dd8a24b1ee9864
|
sha256: "5276944c6ffc975ae796569a826c38a62d2abcf264e26b88fa6f482e107f4237"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.70.0"
|
version: "0.70.2"
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -675,14 +675,78 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.0"
|
version: "2.3.0"
|
||||||
|
flutter_inappwebview:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_inappwebview
|
||||||
|
sha256: "80092d13d3e29b6227e25b67973c67c7210bd5e35c4b747ca908e31eb71a46d5"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.1.5"
|
||||||
|
flutter_inappwebview_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_inappwebview_android
|
||||||
|
sha256: "62557c15a5c2db5d195cb3892aab74fcaec266d7b86d59a6f0027abd672cddba"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.3"
|
||||||
|
flutter_inappwebview_internal_annotations:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_inappwebview_internal_annotations
|
||||||
|
sha256: "787171d43f8af67864740b6f04166c13190aa74a1468a1f1f1e9ee5b90c359cd"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.0"
|
||||||
|
flutter_inappwebview_ios:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_inappwebview_ios
|
||||||
|
sha256: "5818cf9b26cf0cbb0f62ff50772217d41ea8d3d9cc00279c45f8aabaa1b4025d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.2"
|
||||||
|
flutter_inappwebview_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_inappwebview_macos
|
||||||
|
sha256: c1fbb86af1a3738e3541364d7d1866315ffb0468a1a77e34198c9be571287da1
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.2"
|
||||||
|
flutter_inappwebview_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_inappwebview_platform_interface
|
||||||
|
sha256: cf5323e194096b6ede7a1ca808c3e0a078e4b33cc3f6338977d75b4024ba2500
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.0+1"
|
||||||
|
flutter_inappwebview_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_inappwebview_web
|
||||||
|
sha256: "55f89c83b0a0d3b7893306b3bb545ba4770a4df018204917148ebb42dc14a598"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.2"
|
||||||
|
flutter_inappwebview_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_inappwebview_windows
|
||||||
|
sha256: "8b4d3a46078a2cdc636c4a3d10d10f2a16882f6be607962dbfff8874d1642055"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.6.0"
|
||||||
flutter_launcher_icons:
|
flutter_launcher_icons:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: flutter_launcher_icons
|
name: flutter_launcher_icons
|
||||||
sha256: "31cd0885738e87c72d6f055564d37fabcdacee743b396b78c7636c169cac64f5"
|
sha256: bfa04787c85d80ecb3f8777bde5fc10c3de809240c48fa061a2c2bf15ea5211c
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.14.2"
|
version: "0.14.3"
|
||||||
flutter_lints:
|
flutter_lints:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@@ -700,18 +764,18 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_markdown
|
name: flutter_markdown
|
||||||
sha256: "255b00afa1a7bad19727da6a7780cf3db6c3c12e68d302d85e0ff1fdf173db9e"
|
sha256: e37f4c69a07b07bb92622ef6b131a53c9aae48f64b176340af9e8e5238718487
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.4+3"
|
version: "0.7.5"
|
||||||
flutter_native_splash:
|
flutter_native_splash:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: flutter_native_splash
|
name: flutter_native_splash
|
||||||
sha256: "1152ab0067ca5a2ebeb862fe0a762057202cceb22b7e62692dcbabf6483891bb"
|
sha256: "7062602e0dbd29141fb8eb19220b5871ca650be5197ab9c1f193a28b17537bc7"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.3"
|
version: "2.4.4"
|
||||||
flutter_plugin_android_lifecycle:
|
flutter_plugin_android_lifecycle:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -740,10 +804,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_svg
|
name: flutter_svg
|
||||||
sha256: "54900a1a1243f3c4a5506d853a2b5c2dbc38d5f27e52a52618a8054401431123"
|
sha256: c200fd79c918a40c5cd50ea0877fa13f81bdaf6f0a5d3dbcc2a13e3285d6aa1b
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.16"
|
version: "2.0.17"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -766,10 +830,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_webrtc
|
name: flutter_webrtc
|
||||||
sha256: "3efe9828f19a07d29a51a726759ad0c70a840d231548a1c7d0332075a94db1df"
|
sha256: "188401cc3275bc4f1f965babdff6cac612a4b46572f1e49f49db8af5361d5712"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.5+hotfix.1"
|
version: "0.12.6"
|
||||||
freezed:
|
freezed:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@@ -822,10 +886,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: go_router
|
name: go_router
|
||||||
sha256: "2fd11229f59e23e967b0775df8d5948a519cd7e1e8b6e849729e010587b46539"
|
sha256: "7c2d40b59890a929824f30d442e810116caf5088482629c894b9e4478c67472d"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "14.6.2"
|
version: "14.6.3"
|
||||||
google_fonts:
|
google_fonts:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -875,7 +939,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.0"
|
version: "0.7.0"
|
||||||
html:
|
html:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: html
|
name: html
|
||||||
sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec"
|
sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec"
|
||||||
@@ -902,10 +966,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: http_parser
|
name: http_parser
|
||||||
sha256: "76d306a1c3afb33fe82e2bbacad62a61f409b5634c915fceb0d799de1a913360"
|
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.1"
|
version: "4.1.2"
|
||||||
icons_launcher:
|
icons_launcher:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@@ -934,10 +998,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: image_picker_android
|
name: image_picker_android
|
||||||
sha256: aa6f1280b670861ac45220cc95adc59bb6ae130259d36f980ccb62220dc5e59f
|
sha256: b62d34a506e12bb965e824b6db4fbf709ee4589cf5d3e99b45ab2287b008ee0c
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.8.12+19"
|
version: "0.8.12+20"
|
||||||
image_picker_for_web:
|
image_picker_for_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -950,10 +1014,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: image_picker_ios
|
name: image_picker_ios
|
||||||
sha256: "4f0568120c6fcc0aaa04511cb9f9f4d29fc3d0139884b1d06be88dcec7641d6b"
|
sha256: "05da758e67bc7839e886b3959848aa6b44ff123ab4b28f67891008afe8ef9100"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.8.12+1"
|
version: "0.8.12+2"
|
||||||
image_picker_linux:
|
image_picker_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -974,10 +1038,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: image_picker_platform_interface
|
name: image_picker_platform_interface
|
||||||
sha256: "9ec26d410ff46f483c5519c29c02ef0e02e13a543f882b152d4bfd2f06802f80"
|
sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.10.0"
|
version: "2.10.1"
|
||||||
image_picker_windows:
|
image_picker_windows:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1086,10 +1150,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: livekit_client
|
name: livekit_client
|
||||||
sha256: a3ff529fe6745ee40cdedcd021d81c4a6ad946dd495e782596f2856eeeabc739
|
sha256: "02b4653d903852d0ae86b15fbe4324747606dae6410fe860d0c07a11c79988de"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.3"
|
version: "2.3.5"
|
||||||
logging:
|
logging:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1110,10 +1174,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: markdown
|
name: markdown
|
||||||
sha256: ef2a1298144e3f985cc736b22e0ccdaf188b5b3970648f2d9dc13efd1d9df051
|
sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.2.2"
|
version: "7.3.0"
|
||||||
marquee:
|
marquee:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -1142,10 +1206,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: material_symbols_icons
|
name: material_symbols_icons
|
||||||
sha256: "64404f47f8e0a9d20478468e5decef867a688660bad7173adcd20418d7f892c9"
|
sha256: "89aac72d25dd49303f71b3b1e70f8374791846729365b25bebc2a2531e5b86cd"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.2801.0"
|
version: "4.2801.1"
|
||||||
media_kit:
|
media_kit:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -1270,10 +1334,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: package_info_plus
|
name: package_info_plus
|
||||||
sha256: "70c421fe9d9cc1a9a7f3b05ae56befd469fe4f8daa3b484823141a55442d858d"
|
sha256: "739e0a5c3c4055152520fa321d0645ee98e932718b4c8efeeb51451968fe0790"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.1.2"
|
version: "8.1.3"
|
||||||
package_info_plus_platform_interface:
|
package_info_plus_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1502,10 +1566,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: pubspec_parse
|
name: pubspec_parse
|
||||||
sha256: "81876843eb50dc2e1e5b151792c9a985c5ed2536914115ed04e9c8528f6647b0"
|
sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.5.0"
|
||||||
qr:
|
qr:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1630,10 +1694,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: share_plus
|
name: share_plus
|
||||||
sha256: "6327c3f233729374d0abaafd61f6846115b2a481b4feddd8534211dc10659400"
|
sha256: fce43200aa03ea87b91ce4c3ac79f0cecd52e2a7a56c7a4185023c271fbfa6da
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.1.3"
|
version: "10.1.4"
|
||||||
share_plus_platform_interface:
|
share_plus_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1646,18 +1710,18 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: shared_preferences
|
name: shared_preferences
|
||||||
sha256: "3c7e73920c694a436afaf65ab60ce3453d91f84208d761fbd83fc21182134d93"
|
sha256: a752ce92ea7540fc35a0d19722816e04d0e72828a4200e83a98cf1a1eb524c9a
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.4"
|
version: "2.3.5"
|
||||||
shared_preferences_android:
|
shared_preferences_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: shared_preferences_android
|
name: shared_preferences_android
|
||||||
sha256: "02a7d8a9ef346c9af715811b01fbd8e27845ad2c41148eefd31321471b41863d"
|
sha256: "138b7bbbc7f59c56236e426c37afb8f78cbc57b094ac64c440e0bb90e380a4f5"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.0"
|
version: "2.4.2"
|
||||||
shared_preferences_foundation:
|
shared_preferences_foundation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1795,10 +1859,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: sqflite_darwin
|
name: sqflite_darwin
|
||||||
sha256: "96a698e2bc82bd770a4d6aab00b42396a7c63d9e33513a56945cbccb594c2474"
|
sha256: "22adfd9a2c7d634041e96d6241e6e1c8138ca6817018afc5d443fef91dcefa9c"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.1"
|
version: "2.4.1+1"
|
||||||
sqflite_platform_interface:
|
sqflite_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1863,14 +1927,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.3.0+3"
|
version: "3.3.0+3"
|
||||||
syntax_highlight:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: syntax_highlight
|
|
||||||
sha256: ee33b6aa82cc722bb9b40152a792181dee222353b486c0255fde666a3e3a4997
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.4.0"
|
|
||||||
term_glyph:
|
term_glyph:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1979,18 +2035,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_web
|
name: url_launcher_web
|
||||||
sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e"
|
sha256: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.3"
|
version: "2.4.0"
|
||||||
url_launcher_windows:
|
url_launcher_windows:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_windows
|
name: url_launcher_windows
|
||||||
sha256: "44cf3aabcedde30f2dba119a9dea3b0f2672fbe6fa96e85536251d678216b3c4"
|
sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.3"
|
version: "3.1.4"
|
||||||
uuid:
|
uuid:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -2011,10 +2067,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vector_graphics_codec
|
name: vector_graphics_codec
|
||||||
sha256: "2430b973a4ca3c4dbc9999b62b8c719a160100dcbae5c819bae0cacce32c9cdb"
|
sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.12"
|
version: "1.1.13"
|
||||||
vector_graphics_compiler:
|
vector_graphics_compiler:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -2131,10 +2187,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: win32
|
name: win32
|
||||||
sha256: "8b338d4486ab3fbc0ba0db9f9b4f5239b6697fcee427939a40e720cbb9ee0a69"
|
sha256: "154360849a56b7b67331c21f09a386562d88903f90a1099c5987afc1912e1f29"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.9.0"
|
version: "5.10.0"
|
||||||
win32_registry:
|
win32_registry:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -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: 2.2.1+40
|
version: 2.2.2+57
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.5.4
|
sdk: ^3.5.4
|
||||||
@@ -53,9 +53,7 @@ dependencies:
|
|||||||
markdown: ^7.2.2
|
markdown: ^7.2.2
|
||||||
flutter_markdown: ^0.7.4+1
|
flutter_markdown: ^0.7.4+1
|
||||||
url_launcher: ^6.3.1
|
url_launcher: ^6.3.1
|
||||||
cached_network_image: ^3.4.1
|
|
||||||
flutter_animate: ^4.5.0
|
flutter_animate: ^4.5.0
|
||||||
syntax_highlight: ^0.4.0
|
|
||||||
google_fonts: ^6.2.1
|
google_fonts: ^6.2.1
|
||||||
path: ^1.9.0
|
path: ^1.9.0
|
||||||
relative_time: ^5.0.0
|
relative_time: ^5.0.0
|
||||||
@@ -116,6 +114,9 @@ dependencies:
|
|||||||
flutter_webrtc: ^0.12.5+hotfix.1
|
flutter_webrtc: ^0.12.5+hotfix.1
|
||||||
slide_countdown: ^2.0.2
|
slide_countdown: ^2.0.2
|
||||||
video_compress: ^3.1.3
|
video_compress: ^3.1.3
|
||||||
|
cached_network_image: ^3.4.1
|
||||||
|
flutter_inappwebview: ^6.1.5
|
||||||
|
html: ^0.15.5
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
id = "solian-next"
|
id = "solian"
|
||||||
|
|
||||||
[[locations]]
|
[[locations]]
|
||||||
id = "solian-next"
|
id = "solian"
|
||||||
host = ["sn-next.solsynth.dev"]
|
hosts = ["sn.solsynth.dev"]
|
||||||
path = ["/"]
|
paths = ["/"]
|
||||||
[[locations.destinations]]
|
[[locations.destinations]]
|
||||||
id = "solian-next-web"
|
id = "solian-web"
|
||||||
uri = "files:///workdir/solian-next?fallback=index.html&index=index.html"
|
uri = "files:///workdir/solian?fallback=index.html&index=index.html"
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
-->
|
-->
|
||||||
<base href="$FLUTTER_BASE_HREF">
|
<base href="$FLUTTER_BASE_HREF">
|
||||||
|
|
||||||
|
<script type="application/javascript" src="/assets/packages/flutter_inappwebview_web/assets/web/web_support.js" defer></script>
|
||||||
|
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||||
<meta name="description" content="A new Flutter project.">
|
<meta name="description" content="A new Flutter project.">
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#include <file_saver/file_saver_plugin.h>
|
#include <file_saver/file_saver_plugin.h>
|
||||||
#include <file_selector_windows/file_selector_windows.h>
|
#include <file_selector_windows/file_selector_windows.h>
|
||||||
#include <firebase_core/firebase_core_plugin_c_api.h>
|
#include <firebase_core/firebase_core_plugin_c_api.h>
|
||||||
|
#include <flutter_inappwebview_windows/flutter_inappwebview_windows_plugin_c_api.h>
|
||||||
#include <flutter_udid/flutter_udid_plugin_c_api.h>
|
#include <flutter_udid/flutter_udid_plugin_c_api.h>
|
||||||
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
|
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
|
||||||
#include <gal/gal_plugin_c_api.h>
|
#include <gal/gal_plugin_c_api.h>
|
||||||
@@ -34,6 +35,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
|||||||
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||||
FirebaseCorePluginCApiRegisterWithRegistrar(
|
FirebaseCorePluginCApiRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("FirebaseCorePluginCApi"));
|
registry->GetRegistrarForPlugin("FirebaseCorePluginCApi"));
|
||||||
|
FlutterInappwebviewWindowsPluginCApiRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("FlutterInappwebviewWindowsPluginCApi"));
|
||||||
FlutterUdidPluginCApiRegisterWithRegistrar(
|
FlutterUdidPluginCApiRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("FlutterUdidPluginCApi"));
|
registry->GetRegistrarForPlugin("FlutterUdidPluginCApi"));
|
||||||
FlutterWebRTCPluginRegisterWithRegistrar(
|
FlutterWebRTCPluginRegisterWithRegistrar(
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||||||
file_saver
|
file_saver
|
||||||
file_selector_windows
|
file_selector_windows
|
||||||
firebase_core
|
firebase_core
|
||||||
|
flutter_inappwebview_windows
|
||||||
flutter_udid
|
flutter_udid
|
||||||
flutter_webrtc
|
flutter_webrtc
|
||||||
gal
|
gal
|
||||||
|
|||||||
Reference in New Issue
Block a user