Compare commits
92 Commits
3.3.0+146
...
f5ca6a37bf
| Author | SHA1 | Date | |
|---|---|---|---|
|
f5ca6a37bf
|
|||
|
5fc8859f3b
|
|||
|
e30e7adbe2
|
|||
|
68be4db160
|
|||
|
aa91e376ca
|
|||
|
caffb85588
|
|||
|
521b192205
|
|||
|
77ac0428ea
|
|||
|
88c8227c66
|
|||
|
b20d8350a8
|
|||
|
98b27bed0e
|
|||
|
3a7d8b1a0d
|
|||
|
b4801d6af6
|
|||
|
aab5b957af
|
|||
|
43d706a184
|
|||
|
98df275f88
|
|||
|
5663df6ef1
|
|||
|
e996a0c95f
|
|||
|
a090e93f57
|
|||
|
c69034c071
|
|||
|
369ea6cf5b
|
|||
|
2e371b5296
|
|||
|
2e9d61bcfa
|
|||
|
9c2b5b0dfa
|
|||
|
3b40f515b3
|
|||
|
5ee61dbef2
|
|||
|
b151ef6686
|
|||
|
ff934d0f08
|
|||
|
abe5ded896
|
|||
|
f1d72a5215
|
|||
|
864cbe73b7
|
|||
|
108a6da074
|
|||
|
f9a09599c9
|
|||
|
9067dadd3e
|
|||
|
09f8df1e78
|
|||
|
2c5f246c55
|
|||
|
a66c6ea654
|
|||
|
3ad4bb4518
|
|||
|
53f0dcb825
|
|||
|
557f5a2389
|
|||
|
78f14f890f
|
|||
|
77b2effb34
|
|||
|
f02b4abf65
|
|||
|
3f37c4f761
|
|||
|
5deb910fa4
|
|||
|
f50a19f573
|
|||
|
98c8a356e8
|
|||
|
d0c16ea08f
|
|||
|
f2c1b2a531
|
|||
|
3061f0c5a9
|
|||
|
98f7f33c65
|
|||
|
d9af5d32fd
|
|||
|
f2031697ec
|
|||
|
9b85b7573c
|
|||
|
4fb739b33b
|
|||
|
c03ba3bc3a
|
|||
|
fc65440420
|
|||
|
7b85533184
|
|||
|
77d9eb60c6
|
|||
|
4d8953cd22
|
|||
|
fafa460fe8
|
|||
|
faf3a677d4
|
|||
|
0f644a0234
|
|||
|
18d16fdd57
|
|||
|
18e890d63c
|
|||
|
9c5e50c16a
|
|||
|
96a2c8182e
|
|||
|
56b27c3e82
|
|||
|
ad4bf94195
|
|||
|
b77a832d8a
|
|||
|
5e61805db7
|
|||
|
35b96b0bd2
|
|||
|
c8ad791ff3
|
|||
|
1e908502dc
|
|||
|
715ce1a368
|
|||
|
548c9963ee
|
|||
|
db5199438a
|
|||
|
4409a6fb1e
|
|||
|
26a24b0e41
|
|||
|
9b948d259b
|
|||
|
1f713b5b2b
|
|||
|
f92cfafda4
|
|||
|
fa208b44d7
|
|||
|
94adecafbb
|
|||
|
0303ef4a93
|
|||
|
c2b18ce10b
|
|||
|
0767bb53ce
|
|||
|
b233f9a410
|
|||
|
256024fb46
|
|||
|
4a80aaf24d
|
|||
|
aafd160c44
|
|||
|
4a800725e3
|
@@ -48,8 +48,6 @@
|
|||||||
"deletePublisherHint": "Are you sure to delete this publisher? This will also deleted all the post and collections under this publisher.",
|
"deletePublisherHint": "Are you sure to delete this publisher? This will also deleted all the post and collections under this publisher.",
|
||||||
"deletePost": "Delete Post",
|
"deletePost": "Delete Post",
|
||||||
"deletePostHint": "Are you sure to delete this post?",
|
"deletePostHint": "Are you sure to delete this post?",
|
||||||
"deleteMessage": "Delete Message",
|
|
||||||
"deleteMessageConfirmation": "Are you sure you want to delete this message?",
|
|
||||||
"copyLink": "Copy Link",
|
"copyLink": "Copy Link",
|
||||||
"postCreateAccountTitle": "Thanks for joining!",
|
"postCreateAccountTitle": "Thanks for joining!",
|
||||||
"postCreateAccountNext": "What's next?",
|
"postCreateAccountNext": "What's next?",
|
||||||
@@ -135,12 +133,6 @@
|
|||||||
"reactionPositive": "Postive",
|
"reactionPositive": "Postive",
|
||||||
"reactionNegative": "Negative",
|
"reactionNegative": "Negative",
|
||||||
"reactionNeutral": "Neutral",
|
"reactionNeutral": "Neutral",
|
||||||
"customReaction": "Custom Reaction",
|
|
||||||
"customReactionHint": "Custom Reaction allow you to use user uploaded stickers as the symbol of the reaction for the post. Exclusive for Stellar Program members.",
|
|
||||||
"customReactions": "Custom Reactions",
|
|
||||||
"stickerPlaceholder": "Sticker Placeholder",
|
|
||||||
"reactionAttitude": "Reaction Attitude",
|
|
||||||
"addReaction": "Add Reaction",
|
|
||||||
"connectionConnected": "Connected",
|
"connectionConnected": "Connected",
|
||||||
"connectionDisconnected": "Disconnected",
|
"connectionDisconnected": "Disconnected",
|
||||||
"connectionReconnecting": "Reconnecting",
|
"connectionReconnecting": "Reconnecting",
|
||||||
@@ -163,8 +155,6 @@
|
|||||||
"accountConnectionProviderGithub": "GitHub",
|
"accountConnectionProviderGithub": "GitHub",
|
||||||
"accountConnectionProviderDiscord": "Discord",
|
"accountConnectionProviderDiscord": "Discord",
|
||||||
"accountConnectionProviderAfdian": "Afdian",
|
"accountConnectionProviderAfdian": "Afdian",
|
||||||
"accountConnectionProviderSpotify": "Spotify",
|
|
||||||
"accountConnectionProviderSteam": "Steam",
|
|
||||||
"checkIn": "Check In",
|
"checkIn": "Check In",
|
||||||
"checkInNone": "Not checked-in yet",
|
"checkInNone": "Not checked-in yet",
|
||||||
"checkInNoneHint": "Get your fortune tips and daily rewards by checking in.",
|
"checkInNoneHint": "Get your fortune tips and daily rewards by checking in.",
|
||||||
@@ -174,8 +164,6 @@
|
|||||||
"checkInResultLevel3": "Good Luck",
|
"checkInResultLevel3": "Good Luck",
|
||||||
"checkInResultLevel4": "Best Luck",
|
"checkInResultLevel4": "Best Luck",
|
||||||
"checkInActivityTitle": "{} checked in on {} and got a {}",
|
"checkInActivityTitle": "{} checked in on {} and got a {}",
|
||||||
"eventCalendar": "Event Calendar",
|
|
||||||
"eventCalendarEmpty": "No events on that day.",
|
|
||||||
"fortuneGraph": "Fortune Trend",
|
"fortuneGraph": "Fortune Trend",
|
||||||
"noFortuneData": "No fortune data available for this month.",
|
"noFortuneData": "No fortune data available for this month.",
|
||||||
"creatorHub": "Creator Hub",
|
"creatorHub": "Creator Hub",
|
||||||
@@ -261,16 +249,11 @@
|
|||||||
"translatorBadgeName": "Translator",
|
"translatorBadgeName": "Translator",
|
||||||
"translatorBadgeDescription": "Helping translate Solar Network into different languages",
|
"translatorBadgeDescription": "Helping translate Solar Network into different languages",
|
||||||
"wallet": "Wallet",
|
"wallet": "Wallet",
|
||||||
"walletStats": "Wallet Statistics",
|
|
||||||
"totalTransactions": "Total Transactions",
|
|
||||||
"totalOrders": "Total Orders",
|
|
||||||
"totalIncome": "Total Income",
|
|
||||||
"totalOutgoing": "Total Outgoing",
|
|
||||||
"netBalance": "Net Balance",
|
|
||||||
"walletCurrencyPoints": "New Solar Points",
|
"walletCurrencyPoints": "New Solar Points",
|
||||||
"walletCurrencyShortPoints": "NSP",
|
"walletCurrencyShortPoints": "NSP",
|
||||||
"walletCurrencyGolds": "The Solar Dollars",
|
"walletCurrencyGolds": "The Solar Dollars",
|
||||||
"walletCurrencyShortGolds": "NSD",
|
"walletCurrencyShortGolds": "NSD",
|
||||||
|
"retry": "Retry",
|
||||||
"creatorHubUnselectedHint": "Pick / create a publisher to get started.",
|
"creatorHubUnselectedHint": "Pick / create a publisher to get started.",
|
||||||
"relationships": "Relationships",
|
"relationships": "Relationships",
|
||||||
"addFriend": "Send a Friend Request",
|
"addFriend": "Send a Friend Request",
|
||||||
@@ -321,8 +304,6 @@
|
|||||||
"settingsBackgroundImageClear": "Clear Background Image",
|
"settingsBackgroundImageClear": "Clear Background Image",
|
||||||
"settingsBackgroundGenerateColor": "Generate color scheme from Bacground Image",
|
"settingsBackgroundGenerateColor": "Generate color scheme from Bacground Image",
|
||||||
"messageNone": "No content to display",
|
"messageNone": "No content to display",
|
||||||
"messageUpdateLinks": "Server generated links previews",
|
|
||||||
"messageUpdateEdited": "Edited a message",
|
|
||||||
"unreadMessages": {
|
"unreadMessages": {
|
||||||
"one": "{} unread message",
|
"one": "{} unread message",
|
||||||
"other": "{} unread messages"
|
"other": "{} unread messages"
|
||||||
@@ -336,7 +317,6 @@
|
|||||||
"settingsAprilFoolFeatures": "April Fool Features",
|
"settingsAprilFoolFeatures": "April Fool Features",
|
||||||
"settingsEnterToSend": "Enter to Send",
|
"settingsEnterToSend": "Enter to Send",
|
||||||
"settingsTransparentAppBar": "Transparent App Bar",
|
"settingsTransparentAppBar": "Transparent App Bar",
|
||||||
"settingsCardBackgroundOpacity": "Card Background Opacity",
|
|
||||||
"settingsCustomFonts": "Custom Fonts",
|
"settingsCustomFonts": "Custom Fonts",
|
||||||
"settingsCustomFontsHint": "Custom fonts will be used for all text in the app. Make sure it is installed on your device.",
|
"settingsCustomFontsHint": "Custom fonts will be used for all text in the app. Make sure it is installed on your device.",
|
||||||
"settingsColorScheme": "Color Scheme",
|
"settingsColorScheme": "Color Scheme",
|
||||||
@@ -384,6 +364,7 @@
|
|||||||
"authFactorSecretHint": "Create an secret for this factor.",
|
"authFactorSecretHint": "Create an secret for this factor.",
|
||||||
"authFactorQrCodeScan": "Scan this QR code with your authenticator app to set up TOTP authentication",
|
"authFactorQrCodeScan": "Scan this QR code with your authenticator app to set up TOTP authentication",
|
||||||
"authFactorNoQrCode": "No QR code available for this authentication factor",
|
"authFactorNoQrCode": "No QR code available for this authentication factor",
|
||||||
|
"cancel": "Cancel",
|
||||||
"confirm": "Confirm",
|
"confirm": "Confirm",
|
||||||
"authFactorAdditional": "One more step",
|
"authFactorAdditional": "One more step",
|
||||||
"authFactorHint": "Contact method hint",
|
"authFactorHint": "Contact method hint",
|
||||||
@@ -408,10 +389,6 @@
|
|||||||
"other": "{} are typing..."
|
"other": "{} are typing..."
|
||||||
},
|
},
|
||||||
"settingsAppearance": "Appearance",
|
"settingsAppearance": "Appearance",
|
||||||
"settingsThemeMode": "Theme Mode",
|
|
||||||
"settingsThemeModeSystem": "System",
|
|
||||||
"settingsThemeModeLight": "Light",
|
|
||||||
"settingsThemeModeDark": "Dark",
|
|
||||||
"settingsServer": "Server",
|
"settingsServer": "Server",
|
||||||
"settingsBehavior": "Behavior",
|
"settingsBehavior": "Behavior",
|
||||||
"settingsDesktop": "Desktop",
|
"settingsDesktop": "Desktop",
|
||||||
@@ -472,7 +449,6 @@
|
|||||||
"pronouns": "Pronouns",
|
"pronouns": "Pronouns",
|
||||||
"location": "Location",
|
"location": "Location",
|
||||||
"timeZone": "Time Zone",
|
"timeZone": "Time Zone",
|
||||||
"timezoneNotFound": "Time zone not found",
|
|
||||||
"birthday": "Birthday",
|
"birthday": "Birthday",
|
||||||
"selectADate": "Select a date",
|
"selectADate": "Select a date",
|
||||||
"checkInResultT0": "Worst",
|
"checkInResultT0": "Worst",
|
||||||
@@ -483,6 +459,7 @@
|
|||||||
"accountProfileView": "View Profile",
|
"accountProfileView": "View Profile",
|
||||||
"unspecified": "Unspecified",
|
"unspecified": "Unspecified",
|
||||||
"added": "Added",
|
"added": "Added",
|
||||||
|
"preview": "Preview",
|
||||||
"togglePreview": "Toggle Preview",
|
"togglePreview": "Toggle Preview",
|
||||||
"subscribe": "Subscribe",
|
"subscribe": "Subscribe",
|
||||||
"unsubscribe": "Unsubscribe",
|
"unsubscribe": "Unsubscribe",
|
||||||
@@ -493,7 +470,6 @@
|
|||||||
"pinCode": "PIN Code",
|
"pinCode": "PIN Code",
|
||||||
"biometric": "Biometric",
|
"biometric": "Biometric",
|
||||||
"enterPinToConfirm": "Enter your 6-digit PIN to confirm payment",
|
"enterPinToConfirm": "Enter your 6-digit PIN to confirm payment",
|
||||||
"enterPin": "Enter your PIN code",
|
|
||||||
"clearPin": "Clear PIN",
|
"clearPin": "Clear PIN",
|
||||||
"useBiometricToConfirm": "Use biometric authentication to confirm payment",
|
"useBiometricToConfirm": "Use biometric authentication to confirm payment",
|
||||||
"touchSensorToAuthenticate": "Touch the sensor to authenticate",
|
"touchSensorToAuthenticate": "Touch the sensor to authenticate",
|
||||||
@@ -643,10 +619,6 @@
|
|||||||
"chatNotJoined": "You have not joined this chat yet.",
|
"chatNotJoined": "You have not joined this chat yet.",
|
||||||
"chatUnableJoin": "You can't join this chat due to it's access control settings.",
|
"chatUnableJoin": "You can't join this chat due to it's access control settings.",
|
||||||
"chatJoin": "Join the Chat",
|
"chatJoin": "Join the Chat",
|
||||||
"chatReplyingTo": "Replying to {}",
|
|
||||||
"chatForwarding": "Forwarding message",
|
|
||||||
"chatEditing": "Editing message",
|
|
||||||
"chatNoContent": "No content",
|
|
||||||
"realmJoin": "Join the Realm",
|
"realmJoin": "Join the Realm",
|
||||||
"realmJoinSuccess": "Successfully joined the realm.",
|
"realmJoinSuccess": "Successfully joined the realm.",
|
||||||
"search": "Search",
|
"search": "Search",
|
||||||
@@ -778,21 +750,6 @@
|
|||||||
"rename": "Rename",
|
"rename": "Rename",
|
||||||
"markAsSensitive": "Mark as Sensitive",
|
"markAsSensitive": "Mark as Sensitive",
|
||||||
"fileName": "File name",
|
"fileName": "File name",
|
||||||
"sensitiveCategories": {
|
|
||||||
"language": "Language",
|
|
||||||
"sexualContent": "Sexual Content",
|
|
||||||
"violence": "Violence",
|
|
||||||
"profanity": "Profanity",
|
|
||||||
"hateSpeech": "Hate Speech",
|
|
||||||
"racism": "Racism",
|
|
||||||
"adultContent": "Adult Content",
|
|
||||||
"drugAbuse": "Drug Abuse",
|
|
||||||
"alcoholAbuse": "Alcohol Abuse",
|
|
||||||
"gambling": "Gambling",
|
|
||||||
"selfHarm": "Self-harm",
|
|
||||||
"childAbuse": "Child Abuse",
|
|
||||||
"other": "Other"
|
|
||||||
},
|
|
||||||
"poll": "Poll",
|
"poll": "Poll",
|
||||||
"pollsRecent": "Recent Polls",
|
"pollsRecent": "Recent Polls",
|
||||||
"pollCreateNew": "Create New",
|
"pollCreateNew": "Create New",
|
||||||
@@ -875,7 +832,6 @@
|
|||||||
"pollShortTextAnswerPreview": "Short text answer (preview)",
|
"pollShortTextAnswerPreview": "Short text answer (preview)",
|
||||||
"award": "Award",
|
"award": "Award",
|
||||||
"awardPost": "Award Post",
|
"awardPost": "Award Post",
|
||||||
"awardPoints": "Awarded {} points",
|
|
||||||
"awardMessage": "Message",
|
"awardMessage": "Message",
|
||||||
"awardMessageHint": "Enter your award message...",
|
"awardMessageHint": "Enter your award message...",
|
||||||
"awardAttitude": "Attitude",
|
"awardAttitude": "Attitude",
|
||||||
@@ -1032,10 +988,6 @@
|
|||||||
"searchLinks": "Links",
|
"searchLinks": "Links",
|
||||||
"searchAttachments": "Attachments",
|
"searchAttachments": "Attachments",
|
||||||
"noMessagesFound": "No messages found",
|
"noMessagesFound": "No messages found",
|
||||||
"Searching...": "Searching...",
|
|
||||||
"searchError": "Search failed. Please try again.",
|
|
||||||
"tryDifferentKeywords": "Try different keywords or remove search filters",
|
|
||||||
"retry": "Retry",
|
|
||||||
"openInBrowser": "Open in Browser",
|
"openInBrowser": "Open in Browser",
|
||||||
"highlightPost": "Highlight Post",
|
"highlightPost": "Highlight Post",
|
||||||
"filters": "Filters",
|
"filters": "Filters",
|
||||||
@@ -1067,14 +1019,12 @@
|
|||||||
"authDeviceChallenges": "Device Usage",
|
"authDeviceChallenges": "Device Usage",
|
||||||
"authDeviceHint": "Swipe left to edit label, swipe right to logout device.",
|
"authDeviceHint": "Swipe left to edit label, swipe right to logout device.",
|
||||||
"settingsMessageDisplayStyle": "Message Display Style",
|
"settingsMessageDisplayStyle": "Message Display Style",
|
||||||
"settingsWindowOpacity": "Window Opacity",
|
|
||||||
"auto": "Auto",
|
"auto": "Auto",
|
||||||
"manual": "Manual",
|
"manual": "Manual",
|
||||||
"iframeCode": "Iframe Code",
|
"iframeCode": "Iframe Code",
|
||||||
"iframeCodeHint": "<iframe src=\"...\" width=\"...\" height=\"...\">",
|
"iframeCodeHint": "<iframe src=\"...\" width=\"...\" height=\"...\">",
|
||||||
"parseIframe": "Parse Iframe",
|
"parseIframe": "Parse Iframe",
|
||||||
"messageActions": "Message Actions",
|
"messageActions": "Message Actions",
|
||||||
"messageContent": "Message Content",
|
|
||||||
"viewEmbedLoadHint": "Tap to load",
|
"viewEmbedLoadHint": "Tap to load",
|
||||||
"levelingStage1": "Novice",
|
"levelingStage1": "Novice",
|
||||||
"levelingStage2": "Apprentice",
|
"levelingStage2": "Apprentice",
|
||||||
@@ -1088,7 +1038,6 @@
|
|||||||
"levelingStage10": "Immortal",
|
"levelingStage10": "Immortal",
|
||||||
"levelingStage11": "Divine",
|
"levelingStage11": "Divine",
|
||||||
"levelingStage12": "Transcendent",
|
"levelingStage12": "Transcendent",
|
||||||
"uploadTasks": "Upload Tasks",
|
|
||||||
"uploadAttachment": "Upload Attachment",
|
"uploadAttachment": "Upload Attachment",
|
||||||
"attachmentPreview": "Attachment Preview",
|
"attachmentPreview": "Attachment Preview",
|
||||||
"selectPool": "Select Pool",
|
"selectPool": "Select Pool",
|
||||||
@@ -1110,6 +1059,53 @@
|
|||||||
"recycledFilesDeleted": "Recycled files deleted successfully",
|
"recycledFilesDeleted": "Recycled files deleted successfully",
|
||||||
"failedToDeleteRecycledFiles": "Failed to delete recycled files",
|
"failedToDeleteRecycledFiles": "Failed to delete recycled files",
|
||||||
"upload": "Upload",
|
"upload": "Upload",
|
||||||
|
"deleteMessage": "Delete Message",
|
||||||
|
"deleteMessageConfirmation": "Are you sure you want to delete this message?",
|
||||||
|
"customReaction": "Custom Reaction",
|
||||||
|
"customReactions": "Custom Reactions",
|
||||||
|
"stickerPlaceholder": "Sticker Placeholder",
|
||||||
|
"reactionAttitude": "Reaction Attitude",
|
||||||
|
"addReaction": "Add Reaction",
|
||||||
|
"eventCalendar": "Event Calendar",
|
||||||
|
"eventCalendarEmpty": "No events on that day.",
|
||||||
|
"walletStats": "Wallet Statistics",
|
||||||
|
"totalTransactions": "Total Transactions",
|
||||||
|
"totalOrders": "Total Orders",
|
||||||
|
"totalIncome": "Total Income",
|
||||||
|
"totalOutgoing": "Total Outgoing",
|
||||||
|
"netBalance": "Net Balance",
|
||||||
|
"messageUpdateLinks": "Server generated links previews",
|
||||||
|
"messageUpdateEdited": "Edited a message",
|
||||||
|
"settingsCardBackgroundOpacity": "Card Background Opacity",
|
||||||
|
"settingsThemeMode": "Theme Mode",
|
||||||
|
"settingsThemeModeSystem": "System",
|
||||||
|
"settingsThemeModeLight": "Light",
|
||||||
|
"settingsThemeModeDark": "Dark",
|
||||||
|
"enterPin": "Enter your PIN code",
|
||||||
|
"chatReplyingTo": "Replying to {}",
|
||||||
|
"chatForwarding": "Forwarding message",
|
||||||
|
"chatEditing": "Editing message",
|
||||||
|
"chatNoContent": "No content",
|
||||||
|
"sensitiveCategories": {
|
||||||
|
"language": "Language",
|
||||||
|
"sexualContent": "Sexual Content",
|
||||||
|
"violence": "Violence",
|
||||||
|
"profanity": "Profanity",
|
||||||
|
"hateSpeech": "Hate Speech",
|
||||||
|
"racism": "Racism",
|
||||||
|
"adultContent": "Adult Content",
|
||||||
|
"drugAbuse": "Drug Abuse",
|
||||||
|
"alcoholAbuse": "Alcohol Abuse",
|
||||||
|
"gambling": "Gambling",
|
||||||
|
"selfHarm": "Self-harm",
|
||||||
|
"childAbuse": "Child Abuse",
|
||||||
|
"other": "Other"
|
||||||
|
},
|
||||||
|
"Searching...": "Searching...",
|
||||||
|
"searchError": "Search failed. Please try again.",
|
||||||
|
"tryDifferentKeywords": "Try different keywords or remove search filters",
|
||||||
|
"settingsWindowOpacity": "Window Opacity",
|
||||||
|
"messageContent": "Message Content",
|
||||||
"updateAvailable": "Update available",
|
"updateAvailable": "Update available",
|
||||||
"noChangelogProvided": "No changelog provided.",
|
"noChangelogProvided": "No changelog provided.",
|
||||||
"useSecondarySourceForDownload": "Use secondary source for download",
|
"useSecondarySourceForDownload": "Use secondary source for download",
|
||||||
@@ -1148,7 +1144,6 @@
|
|||||||
"thisWillBeAnOpenGift": "This will be an open gift",
|
"thisWillBeAnOpenGift": "This will be an open gift",
|
||||||
"personalMessage": "Personal Message",
|
"personalMessage": "Personal Message",
|
||||||
"addPersonalMessageForRecipient": "Add a personal message for the recipient",
|
"addPersonalMessageForRecipient": "Add a personal message for the recipient",
|
||||||
"cancel": "Cancel",
|
|
||||||
"giftStatusCreated": "Created",
|
"giftStatusCreated": "Created",
|
||||||
"giftStatusSent": "Sent",
|
"giftStatusSent": "Sent",
|
||||||
"giftStatusRedeemed": "Redeemed",
|
"giftStatusRedeemed": "Redeemed",
|
||||||
@@ -1254,11 +1249,14 @@
|
|||||||
"gradientColors": "Gradient Colors",
|
"gradientColors": "Gradient Colors",
|
||||||
"color": "Color",
|
"color": "Color",
|
||||||
"addColor": "Add Color",
|
"addColor": "Add Color",
|
||||||
"preview": "Preview",
|
|
||||||
"availableWithYourPlan": "Available with your plan",
|
"availableWithYourPlan": "Available with your plan",
|
||||||
"upgradeRequired": "Upgrade required",
|
"upgradeRequired": "Upgrade required",
|
||||||
"settingsDisableAnimation": "Disable Animation",
|
"settingsDisableAnimation": "Disable Animation",
|
||||||
"addTag": "Add Tag",
|
"addTag": "Add Tag",
|
||||||
|
"accountConnectionProviderSpotify": "Spotify",
|
||||||
|
"accountConnectionProviderSteam": "Steam",
|
||||||
|
"timezoneNotFound": "Time zone not found",
|
||||||
|
"awardPoints": "Awarded {} points",
|
||||||
"postFeaturedOn": "Post featured on {}",
|
"postFeaturedOn": "Post featured on {}",
|
||||||
"messageSentAt": "Sent at {}",
|
"messageSentAt": "Sent at {}",
|
||||||
"myTickets": "My Tickets",
|
"myTickets": "My Tickets",
|
||||||
@@ -1304,8 +1302,6 @@
|
|||||||
"thoughtNewConversation": "Start New Conversation",
|
"thoughtNewConversation": "Start New Conversation",
|
||||||
"thoughtParseError": "Failed to parse AI response",
|
"thoughtParseError": "Failed to parse AI response",
|
||||||
"thoughtFunctionCall": "Use {}",
|
"thoughtFunctionCall": "Use {}",
|
||||||
"thoughtFunctionCallBegin": "Calling tool {}",
|
|
||||||
"thoughtFunctionCallFinish": "{} responded",
|
|
||||||
"aiThought": "AI Thought",
|
"aiThought": "AI Thought",
|
||||||
"aiThoughtTitle": "Let sn-chan think",
|
"aiThoughtTitle": "Let sn-chan think",
|
||||||
"postReferenceUnavailable": "Referenced post is unavailable",
|
"postReferenceUnavailable": "Referenced post is unavailable",
|
||||||
@@ -1325,6 +1321,11 @@
|
|||||||
"descendingOrder": "Descending Order",
|
"descendingOrder": "Descending Order",
|
||||||
"selectDate": "Select Date",
|
"selectDate": "Select Date",
|
||||||
"pinnedPosts": "Pinned Posts",
|
"pinnedPosts": "Pinned Posts",
|
||||||
|
"customReactionHint": "Custom Reaction allow you to use user uploaded stickers as the symbol of the reaction for the post. Exclusive for Stellar Program members.",
|
||||||
|
"publicationSites": "Publication Sites",
|
||||||
|
"uploadTasks": "Upload Tasks",
|
||||||
|
"thoughtFunctionCallBegin": "Calling tool {}",
|
||||||
|
"thoughtFunctionCallFinish": "{} responded",
|
||||||
"thoughtUnpaidHint": "Thinking unavaiable due to unpaid orders",
|
"thoughtUnpaidHint": "Thinking unavaiable due to unpaid orders",
|
||||||
"more": "More",
|
"more": "More",
|
||||||
"collapse": "Collapse",
|
"collapse": "Collapse",
|
||||||
@@ -1336,5 +1337,142 @@
|
|||||||
"fundCreateNewHint": "Create a new fund for your message. Select recipients and amount.",
|
"fundCreateNewHint": "Create a new fund for your message. Select recipients and amount.",
|
||||||
"amountOfSplits": "Amount of Splits",
|
"amountOfSplits": "Amount of Splits",
|
||||||
"enterNumberOfSplits": "Enter Splits Amount",
|
"enterNumberOfSplits": "Enter Splits Amount",
|
||||||
"orCreateWith": "Or\ncreate with"
|
"orCreateWith": "Or\ncreate with",
|
||||||
|
"unindexedFiles": "Unindexed files",
|
||||||
|
"folder": "Folder",
|
||||||
|
"clearCompleted": "Clear Completed",
|
||||||
|
"uploadSuccess": "Upload successful!",
|
||||||
|
"wouldYouLikeToViewFile": "Would you like to view the file?",
|
||||||
|
"contentCantEmpty": "Content cannot be empty",
|
||||||
|
"features": "Features",
|
||||||
|
"unnamed": "Unnamed",
|
||||||
|
"fundEnvelopeLoadFailed": "Failed to load fund envelope",
|
||||||
|
"fundEnvelope": "Fund Envelope",
|
||||||
|
"fundEnvelopeRemaining": "Remaining: {} {}",
|
||||||
|
"fundEnvelopeSplit": "Split: {}",
|
||||||
|
"fundEnvelopeSplitEvenly": "Evenly",
|
||||||
|
"fundEnvelopeSplitRandomly": "Randomly",
|
||||||
|
"fundEnvelopeClaimSuccess": "Fund claimed successfully!",
|
||||||
|
"fundEnvelopeStatusCreated": "Created",
|
||||||
|
"fundEnvelopeStatusPartial": "Partially Claimed",
|
||||||
|
"fundEnvelopeStatusCompleted": "Fully Claimed",
|
||||||
|
"fundEnvelopeStatusExpired": "Expired",
|
||||||
|
"fundEnvelopeStatusUnknown": "Unknown",
|
||||||
|
"fundEnvelopeRecipients": "Recipients ({}/{} claimed)",
|
||||||
|
"fundEnvelopeExpiredDaysAgo": {
|
||||||
|
"one": "Expired {} day ago",
|
||||||
|
"other": "Expired {} days ago"
|
||||||
|
},
|
||||||
|
"fundEnvelopeExpiresSoon": "Expires soon",
|
||||||
|
"fundEnvelopeExpiresInHours": {
|
||||||
|
"one": "Expires in {} hour",
|
||||||
|
"other": "Expires in {} hours"
|
||||||
|
},
|
||||||
|
"fundEnvelopeExpiresInDays": {
|
||||||
|
"one": "Expires in {} day",
|
||||||
|
"other": "Expires in {} days"
|
||||||
|
},
|
||||||
|
"fundEnvelopeRemainingWithSplits": "{} {} / {} splits",
|
||||||
|
"fundEnvelopeUnknownUser": "Unknown User",
|
||||||
|
"deleteSite": "Delete Site",
|
||||||
|
"deleteSiteConfirm": "Are you sure you want to delete this site?",
|
||||||
|
"siteDeletedSuccess": "Site deleted successfully",
|
||||||
|
"siteSlug": "Slug",
|
||||||
|
"siteSlugHint": "my-site",
|
||||||
|
"siteSlugRequired": "Please enter a slug",
|
||||||
|
"siteSlugInvalid": "Slug can only contain lowercase letters, numbers, and dashes",
|
||||||
|
"siteName": "Site Name",
|
||||||
|
"siteNameHint": "My Publication Site",
|
||||||
|
"siteNameRequired": "Please enter a site name",
|
||||||
|
"siteMode": "Mode",
|
||||||
|
"siteModeFullyManaged": "Fully Managed",
|
||||||
|
"siteModeSelfManaged": "Self-Managed",
|
||||||
|
"editPublicationSite": "Edit Publication Site",
|
||||||
|
"deletePublicationSite": "Delete Publication Site",
|
||||||
|
"publicationSiteSavedSuccess": "Publication site saved successfully",
|
||||||
|
"publicationSiteDeleteConfirm": "Are you sure you want to delete this publication site? This action cannot be undone.",
|
||||||
|
"publicationSiteDeletedSuccess": "Publication site deleted successfully",
|
||||||
|
"newPublicationSite": "New Publication Site",
|
||||||
|
"siteDetails": "Site Details",
|
||||||
|
"siteInformation": "Site Information",
|
||||||
|
"siteDomain": "Domain",
|
||||||
|
"siteCreated": "Created",
|
||||||
|
"siteUpdated": "Updated",
|
||||||
|
"failedToLoadSite": "Failed to load site",
|
||||||
|
"sitePages": "Pages",
|
||||||
|
"noPagesYet": "No pages yet",
|
||||||
|
"createFirstPage": "Create your first page to get started",
|
||||||
|
"failedToLoadPages": "Failed to load pages",
|
||||||
|
"fileManagement": "File Management",
|
||||||
|
"siteFiles": "Files",
|
||||||
|
"siteFolder": "Folder",
|
||||||
|
"siteRoot": "Root",
|
||||||
|
"noFilesUploadedYet": "No files uploaded yet",
|
||||||
|
"uploadFirstFile": "Upload your first file to get started",
|
||||||
|
"failedToLoadFiles": "Failed to load files",
|
||||||
|
"noFilesFoundInFolder": "No files found in the selected folder",
|
||||||
|
"fileActions": "File Actions",
|
||||||
|
"purgeFiles": "Purge Files",
|
||||||
|
"purgeFilesDescription": "Remove all uploaded files from the site",
|
||||||
|
"deploySite": "Deploy Site",
|
||||||
|
"deploySiteDescription": "Upload and deploy a new version from ZIP archive",
|
||||||
|
"confirmPurge": "Confirm Purge",
|
||||||
|
"purgeFilesConfirm": "This will permanently delete all files uploaded to this site. This action cannot be undone. Are you sure you want to continue?",
|
||||||
|
"purgeAllFiles": "Purge All Files",
|
||||||
|
"allFilesPurgedSuccess": "All files purged successfully",
|
||||||
|
"failedToPurgeFiles": "Failed to purge files: {}",
|
||||||
|
"siteDeployedSuccess": "Site deployed successfully",
|
||||||
|
"failedToDeploySite": "Failed to deploy site: {}",
|
||||||
|
"createPage": "Create Page",
|
||||||
|
"editPage": "Edit Page",
|
||||||
|
"pageType": "Page Type",
|
||||||
|
"htmlPage": "HTML Page",
|
||||||
|
"redirectPage": "Redirect Page",
|
||||||
|
"pageTypeRequired": "Please select a page type",
|
||||||
|
"pagePath": "Page Path",
|
||||||
|
"pagePathHint": "/about, /contact, etc.",
|
||||||
|
"pagePathRequired": "Please enter a page path",
|
||||||
|
"pagePathInvalid": "Page path can only contain letters, numbers, hyphens, underscores, and slashes",
|
||||||
|
"pagePathMustStartWithSlash": "Page path must start with /",
|
||||||
|
"pagePathNoConsecutiveSlashes": "Page path cannot have consecutive slashes",
|
||||||
|
"pageTitle": "Page Title",
|
||||||
|
"pageTitleHint": "About Us, Contact, etc.",
|
||||||
|
"pageTitleRequired": "Please enter a page title",
|
||||||
|
"pageContentHtml": "Page Content (HTML)",
|
||||||
|
"pageContentHint": "<h1>Hello World</h1><p>This is my page content...</p>",
|
||||||
|
"pageContentRequired": "Please enter HTML content for the page",
|
||||||
|
"redirectTarget": "Redirect Target",
|
||||||
|
"redirectTargetHint": "/new-page, https://example.com, etc.",
|
||||||
|
"redirectTargetRequired": "Please enter a redirect target",
|
||||||
|
"redirectTargetInvalid": "Target must be a relative path (/) or absolute URL (http/https)",
|
||||||
|
"deletePage": "Delete Page",
|
||||||
|
"deletePageConfirm": "Are you sure you want to delete this page?",
|
||||||
|
"savePage": "Save Page",
|
||||||
|
"pageCreatedSuccess": "Page created successfully",
|
||||||
|
"pageUpdatedSuccess": "Page updated successfully",
|
||||||
|
"pageDeletedSuccess": "Page deleted successfully",
|
||||||
|
"uploadFiles": "Upload Files",
|
||||||
|
"uploadPath": "Upload Path",
|
||||||
|
"uploadPathHint": "/ (root) or /assets/images/",
|
||||||
|
"uploadPathRequired": "Please enter an upload path",
|
||||||
|
"uploadPathMustStartWithSlash": "Path must start with /",
|
||||||
|
"uploadPathNoSpaces": "Path cannot contain spaces",
|
||||||
|
"uploadPathNoConsecutiveSlashes": "Path cannot have consecutive slashes",
|
||||||
|
"percentCompleted": "{}% completed",
|
||||||
|
"filesToUpload": "{} files to upload",
|
||||||
|
"fileSizeKb": "Size: {} KB",
|
||||||
|
"uploadingEllipsis": "Uploading...",
|
||||||
|
"uploadFilesCount": {
|
||||||
|
"one": "Upload {} File",
|
||||||
|
"other": "Upload {} Files"
|
||||||
|
},
|
||||||
|
"allUploadsCompleted": "All uploads completed",
|
||||||
|
"someUploadsFailed": "Some uploads failed",
|
||||||
|
"uploadingInProgress": "Uploading in progress",
|
||||||
|
"readyToUpload": "Ready to upload",
|
||||||
|
"allFilesUploadedSuccess": "All files uploaded successfully",
|
||||||
|
"lotteryLastNumberSpecial": "The last selected number will be your special number.",
|
||||||
|
"lotteryMultiplierRequired": "Please enter a multiplier",
|
||||||
|
"lotteryMultiplierRange": "Multiplier must be between 1 and 10",
|
||||||
|
"dropToShare": "Drop to share"
|
||||||
}
|
}
|
||||||
@@ -164,8 +164,6 @@
|
|||||||
"checkInResultLevel3": "Good Luck",
|
"checkInResultLevel3": "Good Luck",
|
||||||
"checkInResultLevel4": "Best Luck",
|
"checkInResultLevel4": "Best Luck",
|
||||||
"checkInActivityTitle": "{} checked in on {} and got a {}",
|
"checkInActivityTitle": "{} checked in on {} and got a {}",
|
||||||
"eventCalander": "Event Calander",
|
|
||||||
"eventCalanderEmpty": "No events on that day.",
|
|
||||||
"fortuneGraph": "Fortune Trend",
|
"fortuneGraph": "Fortune Trend",
|
||||||
"noFortuneData": "No fortune data available for this month.",
|
"noFortuneData": "No fortune data available for this month.",
|
||||||
"creatorHub": "Creator Hub",
|
"creatorHub": "Creator Hub",
|
||||||
@@ -752,21 +750,6 @@
|
|||||||
"rename": "Rename",
|
"rename": "Rename",
|
||||||
"markAsSensitive": "Mark as Sensitive",
|
"markAsSensitive": "Mark as Sensitive",
|
||||||
"fileName": "File name",
|
"fileName": "File name",
|
||||||
"sensitiveCategories": {
|
|
||||||
"language": "Language",
|
|
||||||
"sexualContent": "Sexual Content",
|
|
||||||
"violence": "Violence",
|
|
||||||
"profanity": "Profanity",
|
|
||||||
"hateSpeech": "Hate Speech",
|
|
||||||
"racism": "Racism",
|
|
||||||
"adultContent": "Adult Content",
|
|
||||||
"drugAbuse": "Drug Abuse",
|
|
||||||
"alcoholAbuse": "Alcohol Abuse",
|
|
||||||
"gambling": "Gambling",
|
|
||||||
"selfHarm": "Self-harm",
|
|
||||||
"childAbuse": "Child Abuse",
|
|
||||||
"other": "Other"
|
|
||||||
},
|
|
||||||
"poll": "Poll",
|
"poll": "Poll",
|
||||||
"pollsRecent": "Recent Polls",
|
"pollsRecent": "Recent Polls",
|
||||||
"pollCreateNew": "Create New",
|
"pollCreateNew": "Create New",
|
||||||
@@ -1075,5 +1058,421 @@
|
|||||||
"deleteRecycledFiles": "Delete Recycled Files",
|
"deleteRecycledFiles": "Delete Recycled Files",
|
||||||
"recycledFilesDeleted": "Recycled files deleted successfully",
|
"recycledFilesDeleted": "Recycled files deleted successfully",
|
||||||
"failedToDeleteRecycledFiles": "Failed to delete recycled files",
|
"failedToDeleteRecycledFiles": "Failed to delete recycled files",
|
||||||
"upload": "Upload"
|
"upload": "Upload",
|
||||||
|
"deleteMessage": "Delete Message",
|
||||||
|
"deleteMessageConfirmation": "Are you sure you want to delete this message?",
|
||||||
|
"customReaction": "Custom Reaction",
|
||||||
|
"customReactions": "Custom Reactions",
|
||||||
|
"stickerPlaceholder": "Sticker Placeholder",
|
||||||
|
"reactionAttitude": "Reaction Attitude",
|
||||||
|
"addReaction": "Add Reaction",
|
||||||
|
"eventCalendar": "Event Calendar",
|
||||||
|
"eventCalendarEmpty": "No events on that day.",
|
||||||
|
"walletStats": "Wallet Statistics",
|
||||||
|
"totalTransactions": "Total Transactions",
|
||||||
|
"totalOrders": "Total Orders",
|
||||||
|
"totalIncome": "Total Income",
|
||||||
|
"totalOutgoing": "Total Outgoing",
|
||||||
|
"netBalance": "Net Balance",
|
||||||
|
"messageUpdateLinks": "Server generated links previews",
|
||||||
|
"messageUpdateEdited": "Edited a message",
|
||||||
|
"settingsCardBackgroundOpacity": "Card Background Opacity",
|
||||||
|
"settingsThemeMode": "Theme Mode",
|
||||||
|
"settingsThemeModeSystem": "System",
|
||||||
|
"settingsThemeModeLight": "Light",
|
||||||
|
"settingsThemeModeDark": "Dark",
|
||||||
|
"enterPin": "Enter your PIN code",
|
||||||
|
"chatReplyingTo": "Replying to {}",
|
||||||
|
"chatForwarding": "Forwarding message",
|
||||||
|
"chatEditing": "Editing message",
|
||||||
|
"chatNoContent": "No content",
|
||||||
|
"sensitiveCategories": {
|
||||||
|
"language": "Language",
|
||||||
|
"sexualContent": "Sexual Content",
|
||||||
|
"violence": "Violence",
|
||||||
|
"profanity": "Profanity",
|
||||||
|
"hateSpeech": "Hate Speech",
|
||||||
|
"racism": "Racism",
|
||||||
|
"adultContent": "Adult Content",
|
||||||
|
"drugAbuse": "Drug Abuse",
|
||||||
|
"alcoholAbuse": "Alcohol Abuse",
|
||||||
|
"gambling": "Gambling",
|
||||||
|
"selfHarm": "Self-harm",
|
||||||
|
"childAbuse": "Child Abuse",
|
||||||
|
"other": "Other"
|
||||||
|
},
|
||||||
|
"Searching...": "Searching...",
|
||||||
|
"searchError": "Search failed. Please try again.",
|
||||||
|
"tryDifferentKeywords": "Try different keywords or remove search filters",
|
||||||
|
"settingsWindowOpacity": "Window Opacity",
|
||||||
|
"messageContent": "Message Content",
|
||||||
|
"updateAvailable": "Update available",
|
||||||
|
"noChangelogProvided": "No changelog provided.",
|
||||||
|
"useSecondarySourceForDownload": "Use secondary source for download",
|
||||||
|
"installUpdate": "Install update",
|
||||||
|
"openReleasePage": "Open release page",
|
||||||
|
"postCompose": "Compose Post",
|
||||||
|
"postPublish": "Publish Post",
|
||||||
|
"restoreDraftTitle": "Restore Draft",
|
||||||
|
"restoreDraftMessage": "A draft was found. Do you want to restore it?",
|
||||||
|
"draft": "Draft",
|
||||||
|
"purchaseGift": "Purchase Gift",
|
||||||
|
"selectRecipient": "Select Recipient",
|
||||||
|
"changeRecipient": "Change Recipient",
|
||||||
|
"addMessage": "Add Message",
|
||||||
|
"skipRecipient": "Skip Recipient",
|
||||||
|
"giftSubscriptions": "Gift Subscriptions",
|
||||||
|
"purchaseAGift": "Purchase a Gift",
|
||||||
|
"redeemAGift": "Redeem a Gift",
|
||||||
|
"giftHistory": "Gift History",
|
||||||
|
"sentGifts": "Sent Gifts",
|
||||||
|
"receivedGifts": "Received Gifts",
|
||||||
|
"noSentGifts": "No sent gifts",
|
||||||
|
"noReceivedGifts": "No received gifts",
|
||||||
|
"stellarGift": "Stellar Gift",
|
||||||
|
"novaGift": "Nova Gift",
|
||||||
|
"supernovaGift": "Supernova Gift",
|
||||||
|
"sameAsMembership": "Same as membership",
|
||||||
|
"enterGiftCodeToRedeem": "Enter gift code to redeem",
|
||||||
|
"enterGiftCode": "Enter gift code",
|
||||||
|
"giftPurchased": "Gift Purchased!",
|
||||||
|
"shareCodeWithRecipient": "Share this code with the recipient to redeem the gift.",
|
||||||
|
"openGiftAnyoneCanRedeem": "This is an open gift that anyone can redeem.",
|
||||||
|
"ok": "OK",
|
||||||
|
"selectedRecipient": "Selected recipient",
|
||||||
|
"noRecipientSelected": "No recipient selected",
|
||||||
|
"thisWillBeAnOpenGift": "This will be an open gift",
|
||||||
|
"personalMessage": "Personal Message",
|
||||||
|
"addPersonalMessageForRecipient": "Add a personal message for the recipient",
|
||||||
|
"giftStatusCreated": "Created",
|
||||||
|
"giftStatusSent": "Sent",
|
||||||
|
"giftStatusRedeemed": "Redeemed",
|
||||||
|
"giftStatusCancelled": "Cancelled",
|
||||||
|
"giftStatusExpired": "Expired",
|
||||||
|
"giftStatusUnknown": "Unknown",
|
||||||
|
"giftCodeCopiedToClipboard": "Gift code copied to clipboard",
|
||||||
|
"codeLabel": "Code: ",
|
||||||
|
"subscriptionLabel": "Subscription: ",
|
||||||
|
"toLabel": "To: ",
|
||||||
|
"fromLabel": "From: ",
|
||||||
|
"messageLabel": "Message: ",
|
||||||
|
"giftRedeemed": "Gift Redeemed!",
|
||||||
|
"giftRedeemedSuccessfully": "You have successfully redeemed the gift. Your new subscription is now active.",
|
||||||
|
"cancelGift": "Cancel Gift",
|
||||||
|
"cancelGiftConfirm": "Are you sure you want to cancel this gift? This action cannot be undone.",
|
||||||
|
"giftCancelledSuccessfully": "Gift cancelled successfully",
|
||||||
|
"createFund": "Create Fund",
|
||||||
|
"fundAmount": "Fund Amount",
|
||||||
|
"enterAmount": "Enter Amount",
|
||||||
|
"selectCurrency": "Select Currency",
|
||||||
|
"splitType": "Split Type",
|
||||||
|
"evenSplit": "Even Split",
|
||||||
|
"equalAmountEach": "Equal amount for each recipient",
|
||||||
|
"randomSplit": "Random Split",
|
||||||
|
"randomAmountEach": "Random amount for each recipient",
|
||||||
|
"recipientCount": "Recipient Count",
|
||||||
|
"numberOfRecipients": "Number of Recipients",
|
||||||
|
"addPersonalMessageForRecipients": "Add a personal message for recipients",
|
||||||
|
"invalidAmount": "Invalid amount",
|
||||||
|
"invalidRecipientCount": "Invalid recipient count",
|
||||||
|
"fundOverview": "Fund Overview",
|
||||||
|
"totalFundsSent": "Total Funds Sent",
|
||||||
|
"totalFundsReceived": "Total Funds Received",
|
||||||
|
"transactions": "Transactions",
|
||||||
|
"myFunds": "My Funds",
|
||||||
|
"availableFunds": "Available Funds",
|
||||||
|
"fundStatusCreated": "Created",
|
||||||
|
"fundStatusPartial": "Partially Claimed",
|
||||||
|
"fundStatusCompleted": "Fully Claimed",
|
||||||
|
"fundStatusExpired": "Expired",
|
||||||
|
"fundStatusUnknown": "Unknown",
|
||||||
|
"recipients": "Recipients",
|
||||||
|
"fundClaimedSuccessfully": "Fund claimed successfully!",
|
||||||
|
"claim": "Claim",
|
||||||
|
"noFundsCreated": "No funds created yet",
|
||||||
|
"createYourFirstFund": "Create your first fund to get started",
|
||||||
|
"noAvailableFunds": "No available funds",
|
||||||
|
"fundsWillAppearHere": "Funds you can claim will appear here",
|
||||||
|
"fundCreatedSuccessfully": "Fund created successfully!",
|
||||||
|
"selectRecipients": "Select Recipients",
|
||||||
|
"noRecipientsSelected": "No recipients selected",
|
||||||
|
"selectRecipientsToSendFund": "Select recipients to send the fund to",
|
||||||
|
"addRecipient": "Add Recipient",
|
||||||
|
"addMoreRecipients": "Add More Recipients",
|
||||||
|
"transactionDetails": "Transaction Details",
|
||||||
|
"remarks": "Remarks",
|
||||||
|
"payer": "Payer",
|
||||||
|
"payee": "Payee",
|
||||||
|
"transactionType": "Transaction Type",
|
||||||
|
"transfer": "Transfer",
|
||||||
|
"payment": "Payment",
|
||||||
|
"systemWallet": "System Wallet",
|
||||||
|
"date": "Date",
|
||||||
|
"createTransfer": "Create Transfer",
|
||||||
|
"transferAmount": "Transfer Amount",
|
||||||
|
"selectPayee": "Select Payee",
|
||||||
|
"selectedPayee": "Selected Payee",
|
||||||
|
"noPayeeSelected": "No payee selected",
|
||||||
|
"selectPayeeToTransfer": "Select payee to transfer to",
|
||||||
|
"addRemark": "Add Remark",
|
||||||
|
"transferRemark": "Transfer Remark",
|
||||||
|
"addRemarkForTransfer": "Add remark for transfer",
|
||||||
|
"enterPinToConfirmTransfer": "Enter your 6-digit PIN to confirm transfer",
|
||||||
|
"transferCreatedSuccessfully": "Transfer created successfully!",
|
||||||
|
"postUpdate": "Update",
|
||||||
|
"fileMetadata": "File Metadata",
|
||||||
|
"resend": "Resend",
|
||||||
|
"fileInfoTitle": "File Information",
|
||||||
|
"download": "Download",
|
||||||
|
"info": "Info",
|
||||||
|
"noStickers": "No Stickers",
|
||||||
|
"noStickersInPack": "This pack does not contains stickers",
|
||||||
|
"noStickerPacks": "No Sticker Packs",
|
||||||
|
"refresh": "Refresh",
|
||||||
|
"spoiler": "Spoiler",
|
||||||
|
"activityHeatmap": "Activity Heatmap",
|
||||||
|
"custom": "Custom",
|
||||||
|
"usernameColor": "Username Color",
|
||||||
|
"colorType": "Color Type",
|
||||||
|
"plain": "Plain",
|
||||||
|
"gradient": "Gradient",
|
||||||
|
"colorValue": "Color Value",
|
||||||
|
"gradientDirection": "Gradient Direction",
|
||||||
|
"gradientDirectionToRight": "To Right",
|
||||||
|
"gradientDirectionToLeft": "To Left",
|
||||||
|
"gradientDirectionToBottom": "To Bottom",
|
||||||
|
"gradientDirectionToTop": "To Top",
|
||||||
|
"gradientDirectionToBottomRight": "To Bottom Right",
|
||||||
|
"gradientDirectionToBottomLeft": "To Bottom Left",
|
||||||
|
"gradientDirectionToTopRight": "To Top Right",
|
||||||
|
"gradientDirectionToTopLeft": "To Top Left",
|
||||||
|
"gradientColors": "Gradient Colors",
|
||||||
|
"color": "Color",
|
||||||
|
"addColor": "Add Color",
|
||||||
|
"availableWithYourPlan": "Available with your plan",
|
||||||
|
"upgradeRequired": "Upgrade required",
|
||||||
|
"settingsDisableAnimation": "Disable Animation",
|
||||||
|
"addTag": "Add Tag",
|
||||||
|
"accountConnectionProviderSpotify": "Spotify",
|
||||||
|
"accountConnectionProviderSteam": "Steam",
|
||||||
|
"timezoneNotFound": "Time zone not found",
|
||||||
|
"awardPoints": "Awarded {} points",
|
||||||
|
"postFeaturedOn": "Post featured on {}",
|
||||||
|
"messageSentAt": "Sent at {}",
|
||||||
|
"myTickets": "My Tickets",
|
||||||
|
"drawHistory": "Draw History",
|
||||||
|
"lottery": "Lottery",
|
||||||
|
"noLotteryTickets": "No lottery tickets yet",
|
||||||
|
"buyYourFirstTicket": "Buy your first lottery ticket to get started!",
|
||||||
|
"buyTicket": "Buy Ticket",
|
||||||
|
"ticketNumbers": "Numbers: {}, Special: {}",
|
||||||
|
"cost": "Cost",
|
||||||
|
"multiplier": "Multiplier",
|
||||||
|
"prizeWon": "Prize Won",
|
||||||
|
"pending": "Pending",
|
||||||
|
"drawn": "Drawn",
|
||||||
|
"won": "Won",
|
||||||
|
"lost": "Lost",
|
||||||
|
"noDrawHistory": "No draw history yet",
|
||||||
|
"buyLotteryTicket": "Buy Lottery Ticket",
|
||||||
|
"selectNumbers": "Select Numbers",
|
||||||
|
"select5UniqueNumbers": "Select 5 unique numbers",
|
||||||
|
"selectSpecialNumber": "Select Special Number",
|
||||||
|
"selectMultiplier": "Select Multiplier",
|
||||||
|
"baseCost": "Base Cost",
|
||||||
|
"totalCost": "Total Cost",
|
||||||
|
"prizeStructure": "Prize Structure",
|
||||||
|
"enterPinToConfirmPurchase": "Enter your PIN to confirm purchase",
|
||||||
|
"ticketPurchasedSuccessfully": "Ticket purchased successfully!",
|
||||||
|
"winningNumbers": "Winning Numbers",
|
||||||
|
"specialNumber": "Special Number",
|
||||||
|
"totalTickets": "Total Tickets",
|
||||||
|
"totalWinners": "Total Winners",
|
||||||
|
"prizePool": "Prize Pool",
|
||||||
|
"enterPinToConfirmPayment": "Enter your PIN code to confirm payment",
|
||||||
|
"purchase": "Purchase",
|
||||||
|
"multiplierLabel": "Multiplier",
|
||||||
|
"specialOnly": "Special Only",
|
||||||
|
"matches": "Matches",
|
||||||
|
"thoughtDefaultTopic": "Reflection",
|
||||||
|
"thoughtAiName": "SN-chan",
|
||||||
|
"thoughtUserName": "You",
|
||||||
|
"thoughtStreamingHint": "Sn-chan is thinking...",
|
||||||
|
"thoughtInputHint": "Ask sn-chan anything...",
|
||||||
|
"thoughtNewConversation": "Start New Conversation",
|
||||||
|
"thoughtParseError": "Failed to parse AI response",
|
||||||
|
"thoughtFunctionCall": "Use {}",
|
||||||
|
"aiThought": "AI Thought",
|
||||||
|
"aiThoughtTitle": "Let sn-chan think",
|
||||||
|
"postReferenceUnavailable": "Referenced post is unavailable",
|
||||||
|
"fabLocation": "FAB Location",
|
||||||
|
"activities": "Activities",
|
||||||
|
"presenceTypeGaming": "Playing",
|
||||||
|
"presenceTypeMusic": "Listening to Music",
|
||||||
|
"presenceTypeWorkout": "Working out",
|
||||||
|
"articleCompose": "Compose Article",
|
||||||
|
"backToHub": "Back to Hub",
|
||||||
|
"advancedFilters": "Advanced Filters",
|
||||||
|
"searchPosts": "Search Posts",
|
||||||
|
"sortBy": "Sort by",
|
||||||
|
"fromDate": "From Date",
|
||||||
|
"toDate": "To Date",
|
||||||
|
"popularity": "Popularity",
|
||||||
|
"descendingOrder": "Descending Order",
|
||||||
|
"selectDate": "Select Date",
|
||||||
|
"pinnedPosts": "Pinned Posts",
|
||||||
|
"customReactionHint": "Custom Reaction allow you to use user uploaded stickers as the symbol of the reaction for the post. Exclusive for Stellar Program members.",
|
||||||
|
"publicationSites": "Publication Sites",
|
||||||
|
"uploadTasks": "Upload Tasks",
|
||||||
|
"thoughtFunctionCallBegin": "Calling tool {}",
|
||||||
|
"thoughtFunctionCallFinish": "{} responded",
|
||||||
|
"thoughtUnpaidHint": "Thinking unavaiable due to unpaid orders",
|
||||||
|
"more": "More",
|
||||||
|
"collapse": "Collapse",
|
||||||
|
"pollConfirmDiscard": "Are you sure you want to leave? All the poll data you're editing will not be saved.",
|
||||||
|
"discard": "Discard",
|
||||||
|
"fund": "Fund",
|
||||||
|
"fundsRecent": "Recent Funds",
|
||||||
|
"fundCreateNew": "Create New",
|
||||||
|
"fundCreateNewHint": "Create a new fund for your message. Select recipients and amount.",
|
||||||
|
"amountOfSplits": "Amount of Splits",
|
||||||
|
"enterNumberOfSplits": "Enter Splits Amount",
|
||||||
|
"orCreateWith": "Or\ncreate with",
|
||||||
|
"unindexedFiles": "Unindexed files",
|
||||||
|
"folder": "Folder",
|
||||||
|
"clearCompleted": "Clear Completed",
|
||||||
|
"uploadSuccess": "Upload successful!",
|
||||||
|
"wouldYouLikeToViewFile": "Would you like to view the file?",
|
||||||
|
"contentCantEmpty": "Content cannot be empty",
|
||||||
|
"features": "Features",
|
||||||
|
"unnamed": "Unnamed",
|
||||||
|
"fundEnvelopeLoadFailed": "Failed to load fund envelope",
|
||||||
|
"fundEnvelope": "Fund Envelope",
|
||||||
|
"fundEnvelopeRemaining": "Remaining: {} {}",
|
||||||
|
"fundEnvelopeSplit": "Split: {}",
|
||||||
|
"fundEnvelopeSplitEvenly": "Evenly",
|
||||||
|
"fundEnvelopeSplitRandomly": "Randomly",
|
||||||
|
"fundEnvelopeClaimSuccess": "Fund claimed successfully!",
|
||||||
|
"fundEnvelopeStatusCreated": "Created",
|
||||||
|
"fundEnvelopeStatusPartial": "Partially Claimed",
|
||||||
|
"fundEnvelopeStatusCompleted": "Fully Claimed",
|
||||||
|
"fundEnvelopeStatusExpired": "Expired",
|
||||||
|
"fundEnvelopeStatusUnknown": "Unknown",
|
||||||
|
"fundEnvelopeRecipients": "Recipients ({}/{} claimed)",
|
||||||
|
"fundEnvelopeExpiredDaysAgo": {
|
||||||
|
"one": "Expired {} day ago",
|
||||||
|
"other": "Expired {} days ago"
|
||||||
|
},
|
||||||
|
"fundEnvelopeExpiresSoon": "Expires soon",
|
||||||
|
"fundEnvelopeExpiresInHours": {
|
||||||
|
"one": "Expires in {} hour",
|
||||||
|
"other": "Expires in {} hours"
|
||||||
|
},
|
||||||
|
"fundEnvelopeExpiresInDays": {
|
||||||
|
"one": "Expires in {} day",
|
||||||
|
"other": "Expires in {} days"
|
||||||
|
},
|
||||||
|
"fundEnvelopeRemainingWithSplits": "{} {} / {} splits",
|
||||||
|
"fundEnvelopeUnknownUser": "Unknown User",
|
||||||
|
"deleteSite": "Delete Site",
|
||||||
|
"deleteSiteConfirm": "Are you sure you want to delete this site?",
|
||||||
|
"siteDeletedSuccess": "Site deleted successfully",
|
||||||
|
"siteSlug": "Slug",
|
||||||
|
"siteSlugHint": "my-site",
|
||||||
|
"siteSlugRequired": "Please enter a slug",
|
||||||
|
"siteSlugInvalid": "Slug can only contain lowercase letters, numbers, and dashes",
|
||||||
|
"siteName": "Site Name",
|
||||||
|
"siteNameHint": "My Publication Site",
|
||||||
|
"siteNameRequired": "Please enter a site name",
|
||||||
|
"siteMode": "Mode",
|
||||||
|
"siteModeFullyManaged": "Fully Managed",
|
||||||
|
"siteModeSelfManaged": "Self-Managed",
|
||||||
|
"editPublicationSite": "Edit Publication Site",
|
||||||
|
"deletePublicationSite": "Delete Publication Site",
|
||||||
|
"publicationSiteSavedSuccess": "Publication site saved successfully",
|
||||||
|
"publicationSiteDeleteConfirm": "Are you sure you want to delete this publication site? This action cannot be undone.",
|
||||||
|
"publicationSiteDeletedSuccess": "Publication site deleted successfully",
|
||||||
|
"newPublicationSite": "New Publication Site",
|
||||||
|
"siteDetails": "Site Details",
|
||||||
|
"siteInformation": "Site Information",
|
||||||
|
"siteDomain": "Domain",
|
||||||
|
"siteCreated": "Created",
|
||||||
|
"siteUpdated": "Updated",
|
||||||
|
"failedToLoadSite": "Failed to load site",
|
||||||
|
"sitePages": "Pages",
|
||||||
|
"noPagesYet": "No pages yet",
|
||||||
|
"createFirstPage": "Create your first page to get started",
|
||||||
|
"failedToLoadPages": "Failed to load pages",
|
||||||
|
"fileManagement": "File Management",
|
||||||
|
"siteFiles": "Files",
|
||||||
|
"siteFolder": "Folder",
|
||||||
|
"siteRoot": "Root",
|
||||||
|
"noFilesUploadedYet": "No files uploaded yet",
|
||||||
|
"uploadFirstFile": "Upload your first file to get started",
|
||||||
|
"failedToLoadFiles": "Failed to load files",
|
||||||
|
"noFilesFoundInFolder": "No files found in the selected folder",
|
||||||
|
"fileActions": "File Actions",
|
||||||
|
"purgeFiles": "Purge Files",
|
||||||
|
"purgeFilesDescription": "Remove all uploaded files from the site",
|
||||||
|
"deploySite": "Deploy Site",
|
||||||
|
"deploySiteDescription": "Upload and deploy a new version from ZIP archive",
|
||||||
|
"confirmPurge": "Confirm Purge",
|
||||||
|
"purgeFilesConfirm": "This will permanently delete all files uploaded to this site. This action cannot be undone. Are you sure you want to continue?",
|
||||||
|
"purgeAllFiles": "Purge All Files",
|
||||||
|
"allFilesPurgedSuccess": "All files purged successfully",
|
||||||
|
"failedToPurgeFiles": "Failed to purge files: {}",
|
||||||
|
"siteDeployedSuccess": "Site deployed successfully",
|
||||||
|
"failedToDeploySite": "Failed to deploy site: {}",
|
||||||
|
"createPage": "Create Page",
|
||||||
|
"editPage": "Edit Page",
|
||||||
|
"pageType": "Page Type",
|
||||||
|
"htmlPage": "HTML Page",
|
||||||
|
"redirectPage": "Redirect Page",
|
||||||
|
"pageTypeRequired": "Please select a page type",
|
||||||
|
"pagePath": "Page Path",
|
||||||
|
"pagePathHint": "/about, /contact, etc.",
|
||||||
|
"pagePathRequired": "Please enter a page path",
|
||||||
|
"pagePathInvalid": "Page path can only contain letters, numbers, hyphens, underscores, and slashes",
|
||||||
|
"pagePathMustStartWithSlash": "Page path must start with /",
|
||||||
|
"pagePathNoConsecutiveSlashes": "Page path cannot have consecutive slashes",
|
||||||
|
"pageTitle": "Page Title",
|
||||||
|
"pageTitleHint": "About Us, Contact, etc.",
|
||||||
|
"pageTitleRequired": "Please enter a page title",
|
||||||
|
"pageContentHtml": "Page Content (HTML)",
|
||||||
|
"pageContentHint": "<h1>Hello World</h1><p>This is my page content...</p>",
|
||||||
|
"pageContentRequired": "Please enter HTML content for the page",
|
||||||
|
"redirectTarget": "Redirect Target",
|
||||||
|
"redirectTargetHint": "/new-page, https://example.com, etc.",
|
||||||
|
"redirectTargetRequired": "Please enter a redirect target",
|
||||||
|
"redirectTargetInvalid": "Target must be a relative path (/) or absolute URL (http/https)",
|
||||||
|
"deletePage": "Delete Page",
|
||||||
|
"deletePageConfirm": "Are you sure you want to delete this page?",
|
||||||
|
"savePage": "Save Page",
|
||||||
|
"pageCreatedSuccess": "Page created successfully",
|
||||||
|
"pageUpdatedSuccess": "Page updated successfully",
|
||||||
|
"pageDeletedSuccess": "Page deleted successfully",
|
||||||
|
"uploadFiles": "Upload Files",
|
||||||
|
"uploadPath": "Upload Path",
|
||||||
|
"uploadPathHint": "/ (root) or /assets/images/",
|
||||||
|
"uploadPathRequired": "Please enter an upload path",
|
||||||
|
"uploadPathMustStartWithSlash": "Path must start with /",
|
||||||
|
"uploadPathNoSpaces": "Path cannot contain spaces",
|
||||||
|
"uploadPathNoConsecutiveSlashes": "Path cannot have consecutive slashes",
|
||||||
|
"percentCompleted": "{}% completed",
|
||||||
|
"filesToUpload": "{} files to upload",
|
||||||
|
"fileSizeKb": "Size: {} KB",
|
||||||
|
"uploadingEllipsis": "Uploading...",
|
||||||
|
"uploadFilesCount": {
|
||||||
|
"one": "Upload {} File",
|
||||||
|
"other": "Upload {} Files"
|
||||||
|
},
|
||||||
|
"allUploadsCompleted": "All uploads completed",
|
||||||
|
"someUploadsFailed": "Some uploads failed",
|
||||||
|
"uploadingInProgress": "Uploading in progress",
|
||||||
|
"readyToUpload": "Ready to upload",
|
||||||
|
"allFilesUploadedSuccess": "All files uploaded successfully",
|
||||||
|
"lotteryLastNumberSpecial": "The last selected number will be your special number.",
|
||||||
|
"lotteryMultiplierRequired": "Please enter a multiplier",
|
||||||
|
"lotteryMultiplierRange": "Multiplier must be between 1 and 10",
|
||||||
|
"dropToShare": "Drop to share"
|
||||||
}
|
}
|
||||||
@@ -164,8 +164,6 @@
|
|||||||
"checkInResultLevel3": "Good Luck",
|
"checkInResultLevel3": "Good Luck",
|
||||||
"checkInResultLevel4": "Best Luck",
|
"checkInResultLevel4": "Best Luck",
|
||||||
"checkInActivityTitle": "{} checked in on {} and got a {}",
|
"checkInActivityTitle": "{} checked in on {} and got a {}",
|
||||||
"eventCalander": "Event Calander",
|
|
||||||
"eventCalanderEmpty": "No events on that day.",
|
|
||||||
"fortuneGraph": "Fortune Trend",
|
"fortuneGraph": "Fortune Trend",
|
||||||
"noFortuneData": "No fortune data available for this month.",
|
"noFortuneData": "No fortune data available for this month.",
|
||||||
"creatorHub": "Creator Hub",
|
"creatorHub": "Creator Hub",
|
||||||
@@ -752,21 +750,6 @@
|
|||||||
"rename": "Rename",
|
"rename": "Rename",
|
||||||
"markAsSensitive": "Mark as Sensitive",
|
"markAsSensitive": "Mark as Sensitive",
|
||||||
"fileName": "File name",
|
"fileName": "File name",
|
||||||
"sensitiveCategories": {
|
|
||||||
"language": "Language",
|
|
||||||
"sexualContent": "Sexual Content",
|
|
||||||
"violence": "Violence",
|
|
||||||
"profanity": "Profanity",
|
|
||||||
"hateSpeech": "Hate Speech",
|
|
||||||
"racism": "Racism",
|
|
||||||
"adultContent": "Adult Content",
|
|
||||||
"drugAbuse": "Drug Abuse",
|
|
||||||
"alcoholAbuse": "Alcohol Abuse",
|
|
||||||
"gambling": "Gambling",
|
|
||||||
"selfHarm": "Self-harm",
|
|
||||||
"childAbuse": "Child Abuse",
|
|
||||||
"other": "Other"
|
|
||||||
},
|
|
||||||
"poll": "Poll",
|
"poll": "Poll",
|
||||||
"pollsRecent": "Recent Polls",
|
"pollsRecent": "Recent Polls",
|
||||||
"pollCreateNew": "Create New",
|
"pollCreateNew": "Create New",
|
||||||
@@ -1075,5 +1058,421 @@
|
|||||||
"deleteRecycledFiles": "Delete Recycled Files",
|
"deleteRecycledFiles": "Delete Recycled Files",
|
||||||
"recycledFilesDeleted": "Recycled files deleted successfully",
|
"recycledFilesDeleted": "Recycled files deleted successfully",
|
||||||
"failedToDeleteRecycledFiles": "Failed to delete recycled files",
|
"failedToDeleteRecycledFiles": "Failed to delete recycled files",
|
||||||
"upload": "Upload"
|
"upload": "Upload",
|
||||||
|
"deleteMessage": "Delete Message",
|
||||||
|
"deleteMessageConfirmation": "Are you sure you want to delete this message?",
|
||||||
|
"customReaction": "Custom Reaction",
|
||||||
|
"customReactions": "Custom Reactions",
|
||||||
|
"stickerPlaceholder": "Sticker Placeholder",
|
||||||
|
"reactionAttitude": "Reaction Attitude",
|
||||||
|
"addReaction": "Add Reaction",
|
||||||
|
"eventCalendar": "Event Calendar",
|
||||||
|
"eventCalendarEmpty": "No events on that day.",
|
||||||
|
"walletStats": "Wallet Statistics",
|
||||||
|
"totalTransactions": "Total Transactions",
|
||||||
|
"totalOrders": "Total Orders",
|
||||||
|
"totalIncome": "Total Income",
|
||||||
|
"totalOutgoing": "Total Outgoing",
|
||||||
|
"netBalance": "Net Balance",
|
||||||
|
"messageUpdateLinks": "Server generated links previews",
|
||||||
|
"messageUpdateEdited": "Edited a message",
|
||||||
|
"settingsCardBackgroundOpacity": "Card Background Opacity",
|
||||||
|
"settingsThemeMode": "Theme Mode",
|
||||||
|
"settingsThemeModeSystem": "System",
|
||||||
|
"settingsThemeModeLight": "Light",
|
||||||
|
"settingsThemeModeDark": "Dark",
|
||||||
|
"enterPin": "Enter your PIN code",
|
||||||
|
"chatReplyingTo": "Replying to {}",
|
||||||
|
"chatForwarding": "Forwarding message",
|
||||||
|
"chatEditing": "Editing message",
|
||||||
|
"chatNoContent": "No content",
|
||||||
|
"sensitiveCategories": {
|
||||||
|
"language": "Language",
|
||||||
|
"sexualContent": "Sexual Content",
|
||||||
|
"violence": "Violence",
|
||||||
|
"profanity": "Profanity",
|
||||||
|
"hateSpeech": "Hate Speech",
|
||||||
|
"racism": "Racism",
|
||||||
|
"adultContent": "Adult Content",
|
||||||
|
"drugAbuse": "Drug Abuse",
|
||||||
|
"alcoholAbuse": "Alcohol Abuse",
|
||||||
|
"gambling": "Gambling",
|
||||||
|
"selfHarm": "Self-harm",
|
||||||
|
"childAbuse": "Child Abuse",
|
||||||
|
"other": "Other"
|
||||||
|
},
|
||||||
|
"Searching...": "Searching...",
|
||||||
|
"searchError": "Search failed. Please try again.",
|
||||||
|
"tryDifferentKeywords": "Try different keywords or remove search filters",
|
||||||
|
"settingsWindowOpacity": "Window Opacity",
|
||||||
|
"messageContent": "Message Content",
|
||||||
|
"updateAvailable": "Update available",
|
||||||
|
"noChangelogProvided": "No changelog provided.",
|
||||||
|
"useSecondarySourceForDownload": "Use secondary source for download",
|
||||||
|
"installUpdate": "Install update",
|
||||||
|
"openReleasePage": "Open release page",
|
||||||
|
"postCompose": "Compose Post",
|
||||||
|
"postPublish": "Publish Post",
|
||||||
|
"restoreDraftTitle": "Restore Draft",
|
||||||
|
"restoreDraftMessage": "A draft was found. Do you want to restore it?",
|
||||||
|
"draft": "Draft",
|
||||||
|
"purchaseGift": "Purchase Gift",
|
||||||
|
"selectRecipient": "Select Recipient",
|
||||||
|
"changeRecipient": "Change Recipient",
|
||||||
|
"addMessage": "Add Message",
|
||||||
|
"skipRecipient": "Skip Recipient",
|
||||||
|
"giftSubscriptions": "Gift Subscriptions",
|
||||||
|
"purchaseAGift": "Purchase a Gift",
|
||||||
|
"redeemAGift": "Redeem a Gift",
|
||||||
|
"giftHistory": "Gift History",
|
||||||
|
"sentGifts": "Sent Gifts",
|
||||||
|
"receivedGifts": "Received Gifts",
|
||||||
|
"noSentGifts": "No sent gifts",
|
||||||
|
"noReceivedGifts": "No received gifts",
|
||||||
|
"stellarGift": "Stellar Gift",
|
||||||
|
"novaGift": "Nova Gift",
|
||||||
|
"supernovaGift": "Supernova Gift",
|
||||||
|
"sameAsMembership": "Same as membership",
|
||||||
|
"enterGiftCodeToRedeem": "Enter gift code to redeem",
|
||||||
|
"enterGiftCode": "Enter gift code",
|
||||||
|
"giftPurchased": "Gift Purchased!",
|
||||||
|
"shareCodeWithRecipient": "Share this code with the recipient to redeem the gift.",
|
||||||
|
"openGiftAnyoneCanRedeem": "This is an open gift that anyone can redeem.",
|
||||||
|
"ok": "OK",
|
||||||
|
"selectedRecipient": "Selected recipient",
|
||||||
|
"noRecipientSelected": "No recipient selected",
|
||||||
|
"thisWillBeAnOpenGift": "This will be an open gift",
|
||||||
|
"personalMessage": "Personal Message",
|
||||||
|
"addPersonalMessageForRecipient": "Add a personal message for the recipient",
|
||||||
|
"giftStatusCreated": "Created",
|
||||||
|
"giftStatusSent": "Sent",
|
||||||
|
"giftStatusRedeemed": "Redeemed",
|
||||||
|
"giftStatusCancelled": "Cancelled",
|
||||||
|
"giftStatusExpired": "Expired",
|
||||||
|
"giftStatusUnknown": "Unknown",
|
||||||
|
"giftCodeCopiedToClipboard": "Gift code copied to clipboard",
|
||||||
|
"codeLabel": "Code: ",
|
||||||
|
"subscriptionLabel": "Subscription: ",
|
||||||
|
"toLabel": "To: ",
|
||||||
|
"fromLabel": "From: ",
|
||||||
|
"messageLabel": "Message: ",
|
||||||
|
"giftRedeemed": "Gift Redeemed!",
|
||||||
|
"giftRedeemedSuccessfully": "You have successfully redeemed the gift. Your new subscription is now active.",
|
||||||
|
"cancelGift": "Cancel Gift",
|
||||||
|
"cancelGiftConfirm": "Are you sure you want to cancel this gift? This action cannot be undone.",
|
||||||
|
"giftCancelledSuccessfully": "Gift cancelled successfully",
|
||||||
|
"createFund": "Create Fund",
|
||||||
|
"fundAmount": "Fund Amount",
|
||||||
|
"enterAmount": "Enter Amount",
|
||||||
|
"selectCurrency": "Select Currency",
|
||||||
|
"splitType": "Split Type",
|
||||||
|
"evenSplit": "Even Split",
|
||||||
|
"equalAmountEach": "Equal amount for each recipient",
|
||||||
|
"randomSplit": "Random Split",
|
||||||
|
"randomAmountEach": "Random amount for each recipient",
|
||||||
|
"recipientCount": "Recipient Count",
|
||||||
|
"numberOfRecipients": "Number of Recipients",
|
||||||
|
"addPersonalMessageForRecipients": "Add a personal message for recipients",
|
||||||
|
"invalidAmount": "Invalid amount",
|
||||||
|
"invalidRecipientCount": "Invalid recipient count",
|
||||||
|
"fundOverview": "Fund Overview",
|
||||||
|
"totalFundsSent": "Total Funds Sent",
|
||||||
|
"totalFundsReceived": "Total Funds Received",
|
||||||
|
"transactions": "Transactions",
|
||||||
|
"myFunds": "My Funds",
|
||||||
|
"availableFunds": "Available Funds",
|
||||||
|
"fundStatusCreated": "Created",
|
||||||
|
"fundStatusPartial": "Partially Claimed",
|
||||||
|
"fundStatusCompleted": "Fully Claimed",
|
||||||
|
"fundStatusExpired": "Expired",
|
||||||
|
"fundStatusUnknown": "Unknown",
|
||||||
|
"recipients": "Recipients",
|
||||||
|
"fundClaimedSuccessfully": "Fund claimed successfully!",
|
||||||
|
"claim": "Claim",
|
||||||
|
"noFundsCreated": "No funds created yet",
|
||||||
|
"createYourFirstFund": "Create your first fund to get started",
|
||||||
|
"noAvailableFunds": "No available funds",
|
||||||
|
"fundsWillAppearHere": "Funds you can claim will appear here",
|
||||||
|
"fundCreatedSuccessfully": "Fund created successfully!",
|
||||||
|
"selectRecipients": "Select Recipients",
|
||||||
|
"noRecipientsSelected": "No recipients selected",
|
||||||
|
"selectRecipientsToSendFund": "Select recipients to send the fund to",
|
||||||
|
"addRecipient": "Add Recipient",
|
||||||
|
"addMoreRecipients": "Add More Recipients",
|
||||||
|
"transactionDetails": "Transaction Details",
|
||||||
|
"remarks": "Remarks",
|
||||||
|
"payer": "Payer",
|
||||||
|
"payee": "Payee",
|
||||||
|
"transactionType": "Transaction Type",
|
||||||
|
"transfer": "Transfer",
|
||||||
|
"payment": "Payment",
|
||||||
|
"systemWallet": "System Wallet",
|
||||||
|
"date": "Date",
|
||||||
|
"createTransfer": "Create Transfer",
|
||||||
|
"transferAmount": "Transfer Amount",
|
||||||
|
"selectPayee": "Select Payee",
|
||||||
|
"selectedPayee": "Selected Payee",
|
||||||
|
"noPayeeSelected": "No payee selected",
|
||||||
|
"selectPayeeToTransfer": "Select payee to transfer to",
|
||||||
|
"addRemark": "Add Remark",
|
||||||
|
"transferRemark": "Transfer Remark",
|
||||||
|
"addRemarkForTransfer": "Add remark for transfer",
|
||||||
|
"enterPinToConfirmTransfer": "Enter your 6-digit PIN to confirm transfer",
|
||||||
|
"transferCreatedSuccessfully": "Transfer created successfully!",
|
||||||
|
"postUpdate": "Update",
|
||||||
|
"fileMetadata": "File Metadata",
|
||||||
|
"resend": "Resend",
|
||||||
|
"fileInfoTitle": "File Information",
|
||||||
|
"download": "Download",
|
||||||
|
"info": "Info",
|
||||||
|
"noStickers": "No Stickers",
|
||||||
|
"noStickersInPack": "This pack does not contains stickers",
|
||||||
|
"noStickerPacks": "No Sticker Packs",
|
||||||
|
"refresh": "Refresh",
|
||||||
|
"spoiler": "Spoiler",
|
||||||
|
"activityHeatmap": "Activity Heatmap",
|
||||||
|
"custom": "Custom",
|
||||||
|
"usernameColor": "Username Color",
|
||||||
|
"colorType": "Color Type",
|
||||||
|
"plain": "Plain",
|
||||||
|
"gradient": "Gradient",
|
||||||
|
"colorValue": "Color Value",
|
||||||
|
"gradientDirection": "Gradient Direction",
|
||||||
|
"gradientDirectionToRight": "To Right",
|
||||||
|
"gradientDirectionToLeft": "To Left",
|
||||||
|
"gradientDirectionToBottom": "To Bottom",
|
||||||
|
"gradientDirectionToTop": "To Top",
|
||||||
|
"gradientDirectionToBottomRight": "To Bottom Right",
|
||||||
|
"gradientDirectionToBottomLeft": "To Bottom Left",
|
||||||
|
"gradientDirectionToTopRight": "To Top Right",
|
||||||
|
"gradientDirectionToTopLeft": "To Top Left",
|
||||||
|
"gradientColors": "Gradient Colors",
|
||||||
|
"color": "Color",
|
||||||
|
"addColor": "Add Color",
|
||||||
|
"availableWithYourPlan": "Available with your plan",
|
||||||
|
"upgradeRequired": "Upgrade required",
|
||||||
|
"settingsDisableAnimation": "Disable Animation",
|
||||||
|
"addTag": "Add Tag",
|
||||||
|
"accountConnectionProviderSpotify": "Spotify",
|
||||||
|
"accountConnectionProviderSteam": "Steam",
|
||||||
|
"timezoneNotFound": "Time zone not found",
|
||||||
|
"awardPoints": "Awarded {} points",
|
||||||
|
"postFeaturedOn": "Post featured on {}",
|
||||||
|
"messageSentAt": "Sent at {}",
|
||||||
|
"myTickets": "My Tickets",
|
||||||
|
"drawHistory": "Draw History",
|
||||||
|
"lottery": "Lottery",
|
||||||
|
"noLotteryTickets": "No lottery tickets yet",
|
||||||
|
"buyYourFirstTicket": "Buy your first lottery ticket to get started!",
|
||||||
|
"buyTicket": "Buy Ticket",
|
||||||
|
"ticketNumbers": "Numbers: {}, Special: {}",
|
||||||
|
"cost": "Cost",
|
||||||
|
"multiplier": "Multiplier",
|
||||||
|
"prizeWon": "Prize Won",
|
||||||
|
"pending": "Pending",
|
||||||
|
"drawn": "Drawn",
|
||||||
|
"won": "Won",
|
||||||
|
"lost": "Lost",
|
||||||
|
"noDrawHistory": "No draw history yet",
|
||||||
|
"buyLotteryTicket": "Buy Lottery Ticket",
|
||||||
|
"selectNumbers": "Select Numbers",
|
||||||
|
"select5UniqueNumbers": "Select 5 unique numbers",
|
||||||
|
"selectSpecialNumber": "Select Special Number",
|
||||||
|
"selectMultiplier": "Select Multiplier",
|
||||||
|
"baseCost": "Base Cost",
|
||||||
|
"totalCost": "Total Cost",
|
||||||
|
"prizeStructure": "Prize Structure",
|
||||||
|
"enterPinToConfirmPurchase": "Enter your PIN to confirm purchase",
|
||||||
|
"ticketPurchasedSuccessfully": "Ticket purchased successfully!",
|
||||||
|
"winningNumbers": "Winning Numbers",
|
||||||
|
"specialNumber": "Special Number",
|
||||||
|
"totalTickets": "Total Tickets",
|
||||||
|
"totalWinners": "Total Winners",
|
||||||
|
"prizePool": "Prize Pool",
|
||||||
|
"enterPinToConfirmPayment": "Enter your PIN code to confirm payment",
|
||||||
|
"purchase": "Purchase",
|
||||||
|
"multiplierLabel": "Multiplier",
|
||||||
|
"specialOnly": "Special Only",
|
||||||
|
"matches": "Matches",
|
||||||
|
"thoughtDefaultTopic": "Reflection",
|
||||||
|
"thoughtAiName": "SN-chan",
|
||||||
|
"thoughtUserName": "You",
|
||||||
|
"thoughtStreamingHint": "Sn-chan is thinking...",
|
||||||
|
"thoughtInputHint": "Ask sn-chan anything...",
|
||||||
|
"thoughtNewConversation": "Start New Conversation",
|
||||||
|
"thoughtParseError": "Failed to parse AI response",
|
||||||
|
"thoughtFunctionCall": "Use {}",
|
||||||
|
"aiThought": "AI Thought",
|
||||||
|
"aiThoughtTitle": "Let sn-chan think",
|
||||||
|
"postReferenceUnavailable": "Referenced post is unavailable",
|
||||||
|
"fabLocation": "FAB Location",
|
||||||
|
"activities": "Activities",
|
||||||
|
"presenceTypeGaming": "Playing",
|
||||||
|
"presenceTypeMusic": "Listening to Music",
|
||||||
|
"presenceTypeWorkout": "Working out",
|
||||||
|
"articleCompose": "Compose Article",
|
||||||
|
"backToHub": "Back to Hub",
|
||||||
|
"advancedFilters": "Advanced Filters",
|
||||||
|
"searchPosts": "Search Posts",
|
||||||
|
"sortBy": "Sort by",
|
||||||
|
"fromDate": "From Date",
|
||||||
|
"toDate": "To Date",
|
||||||
|
"popularity": "Popularity",
|
||||||
|
"descendingOrder": "Descending Order",
|
||||||
|
"selectDate": "Select Date",
|
||||||
|
"pinnedPosts": "Pinned Posts",
|
||||||
|
"customReactionHint": "Custom Reaction allow you to use user uploaded stickers as the symbol of the reaction for the post. Exclusive for Stellar Program members.",
|
||||||
|
"publicationSites": "Publication Sites",
|
||||||
|
"uploadTasks": "Upload Tasks",
|
||||||
|
"thoughtFunctionCallBegin": "Calling tool {}",
|
||||||
|
"thoughtFunctionCallFinish": "{} responded",
|
||||||
|
"thoughtUnpaidHint": "Thinking unavaiable due to unpaid orders",
|
||||||
|
"more": "More",
|
||||||
|
"collapse": "Collapse",
|
||||||
|
"pollConfirmDiscard": "Are you sure you want to leave? All the poll data you're editing will not be saved.",
|
||||||
|
"discard": "Discard",
|
||||||
|
"fund": "Fund",
|
||||||
|
"fundsRecent": "Recent Funds",
|
||||||
|
"fundCreateNew": "Create New",
|
||||||
|
"fundCreateNewHint": "Create a new fund for your message. Select recipients and amount.",
|
||||||
|
"amountOfSplits": "Amount of Splits",
|
||||||
|
"enterNumberOfSplits": "Enter Splits Amount",
|
||||||
|
"orCreateWith": "Or\ncreate with",
|
||||||
|
"unindexedFiles": "Unindexed files",
|
||||||
|
"folder": "Folder",
|
||||||
|
"clearCompleted": "Clear Completed",
|
||||||
|
"uploadSuccess": "Upload successful!",
|
||||||
|
"wouldYouLikeToViewFile": "Would you like to view the file?",
|
||||||
|
"contentCantEmpty": "Content cannot be empty",
|
||||||
|
"features": "Features",
|
||||||
|
"unnamed": "Unnamed",
|
||||||
|
"fundEnvelopeLoadFailed": "Failed to load fund envelope",
|
||||||
|
"fundEnvelope": "Fund Envelope",
|
||||||
|
"fundEnvelopeRemaining": "Remaining: {} {}",
|
||||||
|
"fundEnvelopeSplit": "Split: {}",
|
||||||
|
"fundEnvelopeSplitEvenly": "Evenly",
|
||||||
|
"fundEnvelopeSplitRandomly": "Randomly",
|
||||||
|
"fundEnvelopeClaimSuccess": "Fund claimed successfully!",
|
||||||
|
"fundEnvelopeStatusCreated": "Created",
|
||||||
|
"fundEnvelopeStatusPartial": "Partially Claimed",
|
||||||
|
"fundEnvelopeStatusCompleted": "Fully Claimed",
|
||||||
|
"fundEnvelopeStatusExpired": "Expired",
|
||||||
|
"fundEnvelopeStatusUnknown": "Unknown",
|
||||||
|
"fundEnvelopeRecipients": "Recipients ({}/{} claimed)",
|
||||||
|
"fundEnvelopeExpiredDaysAgo": {
|
||||||
|
"one": "Expired {} day ago",
|
||||||
|
"other": "Expired {} days ago"
|
||||||
|
},
|
||||||
|
"fundEnvelopeExpiresSoon": "Expires soon",
|
||||||
|
"fundEnvelopeExpiresInHours": {
|
||||||
|
"one": "Expires in {} hour",
|
||||||
|
"other": "Expires in {} hours"
|
||||||
|
},
|
||||||
|
"fundEnvelopeExpiresInDays": {
|
||||||
|
"one": "Expires in {} day",
|
||||||
|
"other": "Expires in {} days"
|
||||||
|
},
|
||||||
|
"fundEnvelopeRemainingWithSplits": "{} {} / {} splits",
|
||||||
|
"fundEnvelopeUnknownUser": "Unknown User",
|
||||||
|
"deleteSite": "Delete Site",
|
||||||
|
"deleteSiteConfirm": "Are you sure you want to delete this site?",
|
||||||
|
"siteDeletedSuccess": "Site deleted successfully",
|
||||||
|
"siteSlug": "Slug",
|
||||||
|
"siteSlugHint": "my-site",
|
||||||
|
"siteSlugRequired": "Please enter a slug",
|
||||||
|
"siteSlugInvalid": "Slug can only contain lowercase letters, numbers, and dashes",
|
||||||
|
"siteName": "Site Name",
|
||||||
|
"siteNameHint": "My Publication Site",
|
||||||
|
"siteNameRequired": "Please enter a site name",
|
||||||
|
"siteMode": "Mode",
|
||||||
|
"siteModeFullyManaged": "Fully Managed",
|
||||||
|
"siteModeSelfManaged": "Self-Managed",
|
||||||
|
"editPublicationSite": "Edit Publication Site",
|
||||||
|
"deletePublicationSite": "Delete Publication Site",
|
||||||
|
"publicationSiteSavedSuccess": "Publication site saved successfully",
|
||||||
|
"publicationSiteDeleteConfirm": "Are you sure you want to delete this publication site? This action cannot be undone.",
|
||||||
|
"publicationSiteDeletedSuccess": "Publication site deleted successfully",
|
||||||
|
"newPublicationSite": "New Publication Site",
|
||||||
|
"siteDetails": "Site Details",
|
||||||
|
"siteInformation": "Site Information",
|
||||||
|
"siteDomain": "Domain",
|
||||||
|
"siteCreated": "Created",
|
||||||
|
"siteUpdated": "Updated",
|
||||||
|
"failedToLoadSite": "Failed to load site",
|
||||||
|
"sitePages": "Pages",
|
||||||
|
"noPagesYet": "No pages yet",
|
||||||
|
"createFirstPage": "Create your first page to get started",
|
||||||
|
"failedToLoadPages": "Failed to load pages",
|
||||||
|
"fileManagement": "File Management",
|
||||||
|
"siteFiles": "Files",
|
||||||
|
"siteFolder": "Folder",
|
||||||
|
"siteRoot": "Root",
|
||||||
|
"noFilesUploadedYet": "No files uploaded yet",
|
||||||
|
"uploadFirstFile": "Upload your first file to get started",
|
||||||
|
"failedToLoadFiles": "Failed to load files",
|
||||||
|
"noFilesFoundInFolder": "No files found in the selected folder",
|
||||||
|
"fileActions": "File Actions",
|
||||||
|
"purgeFiles": "Purge Files",
|
||||||
|
"purgeFilesDescription": "Remove all uploaded files from the site",
|
||||||
|
"deploySite": "Deploy Site",
|
||||||
|
"deploySiteDescription": "Upload and deploy a new version from ZIP archive",
|
||||||
|
"confirmPurge": "Confirm Purge",
|
||||||
|
"purgeFilesConfirm": "This will permanently delete all files uploaded to this site. This action cannot be undone. Are you sure you want to continue?",
|
||||||
|
"purgeAllFiles": "Purge All Files",
|
||||||
|
"allFilesPurgedSuccess": "All files purged successfully",
|
||||||
|
"failedToPurgeFiles": "Failed to purge files: {}",
|
||||||
|
"siteDeployedSuccess": "Site deployed successfully",
|
||||||
|
"failedToDeploySite": "Failed to deploy site: {}",
|
||||||
|
"createPage": "Create Page",
|
||||||
|
"editPage": "Edit Page",
|
||||||
|
"pageType": "Page Type",
|
||||||
|
"htmlPage": "HTML Page",
|
||||||
|
"redirectPage": "Redirect Page",
|
||||||
|
"pageTypeRequired": "Please select a page type",
|
||||||
|
"pagePath": "Page Path",
|
||||||
|
"pagePathHint": "/about, /contact, etc.",
|
||||||
|
"pagePathRequired": "Please enter a page path",
|
||||||
|
"pagePathInvalid": "Page path can only contain letters, numbers, hyphens, underscores, and slashes",
|
||||||
|
"pagePathMustStartWithSlash": "Page path must start with /",
|
||||||
|
"pagePathNoConsecutiveSlashes": "Page path cannot have consecutive slashes",
|
||||||
|
"pageTitle": "Page Title",
|
||||||
|
"pageTitleHint": "About Us, Contact, etc.",
|
||||||
|
"pageTitleRequired": "Please enter a page title",
|
||||||
|
"pageContentHtml": "Page Content (HTML)",
|
||||||
|
"pageContentHint": "<h1>Hello World</h1><p>This is my page content...</p>",
|
||||||
|
"pageContentRequired": "Please enter HTML content for the page",
|
||||||
|
"redirectTarget": "Redirect Target",
|
||||||
|
"redirectTargetHint": "/new-page, https://example.com, etc.",
|
||||||
|
"redirectTargetRequired": "Please enter a redirect target",
|
||||||
|
"redirectTargetInvalid": "Target must be a relative path (/) or absolute URL (http/https)",
|
||||||
|
"deletePage": "Delete Page",
|
||||||
|
"deletePageConfirm": "Are you sure you want to delete this page?",
|
||||||
|
"savePage": "Save Page",
|
||||||
|
"pageCreatedSuccess": "Page created successfully",
|
||||||
|
"pageUpdatedSuccess": "Page updated successfully",
|
||||||
|
"pageDeletedSuccess": "Page deleted successfully",
|
||||||
|
"uploadFiles": "Upload Files",
|
||||||
|
"uploadPath": "Upload Path",
|
||||||
|
"uploadPathHint": "/ (root) or /assets/images/",
|
||||||
|
"uploadPathRequired": "Please enter an upload path",
|
||||||
|
"uploadPathMustStartWithSlash": "Path must start with /",
|
||||||
|
"uploadPathNoSpaces": "Path cannot contain spaces",
|
||||||
|
"uploadPathNoConsecutiveSlashes": "Path cannot have consecutive slashes",
|
||||||
|
"percentCompleted": "{}% completed",
|
||||||
|
"filesToUpload": "{} files to upload",
|
||||||
|
"fileSizeKb": "Size: {} KB",
|
||||||
|
"uploadingEllipsis": "Uploading...",
|
||||||
|
"uploadFilesCount": {
|
||||||
|
"one": "Upload {} File",
|
||||||
|
"other": "Upload {} Files"
|
||||||
|
},
|
||||||
|
"allUploadsCompleted": "All uploads completed",
|
||||||
|
"someUploadsFailed": "Some uploads failed",
|
||||||
|
"uploadingInProgress": "Uploading in progress",
|
||||||
|
"readyToUpload": "Ready to upload",
|
||||||
|
"allFilesUploadedSuccess": "All files uploaded successfully",
|
||||||
|
"lotteryLastNumberSpecial": "The last selected number will be your special number.",
|
||||||
|
"lotteryMultiplierRequired": "Please enter a multiplier",
|
||||||
|
"lotteryMultiplierRange": "Multiplier must be between 1 and 10",
|
||||||
|
"dropToShare": "Drop to share"
|
||||||
}
|
}
|
||||||
@@ -164,8 +164,6 @@
|
|||||||
"checkInResultLevel3": "Good Luck",
|
"checkInResultLevel3": "Good Luck",
|
||||||
"checkInResultLevel4": "Best Luck",
|
"checkInResultLevel4": "Best Luck",
|
||||||
"checkInActivityTitle": "{} checked in on {} and got a {}",
|
"checkInActivityTitle": "{} checked in on {} and got a {}",
|
||||||
"eventCalander": "Event Calander",
|
|
||||||
"eventCalanderEmpty": "No events on that day.",
|
|
||||||
"fortuneGraph": "Fortune Trend",
|
"fortuneGraph": "Fortune Trend",
|
||||||
"noFortuneData": "No fortune data available for this month.",
|
"noFortuneData": "No fortune data available for this month.",
|
||||||
"creatorHub": "Creator Hub",
|
"creatorHub": "Creator Hub",
|
||||||
@@ -752,21 +750,6 @@
|
|||||||
"rename": "Rename",
|
"rename": "Rename",
|
||||||
"markAsSensitive": "Mark as Sensitive",
|
"markAsSensitive": "Mark as Sensitive",
|
||||||
"fileName": "File name",
|
"fileName": "File name",
|
||||||
"sensitiveCategories": {
|
|
||||||
"language": "Language",
|
|
||||||
"sexualContent": "Sexual Content",
|
|
||||||
"violence": "Violence",
|
|
||||||
"profanity": "Profanity",
|
|
||||||
"hateSpeech": "Hate Speech",
|
|
||||||
"racism": "Racism",
|
|
||||||
"adultContent": "Adult Content",
|
|
||||||
"drugAbuse": "Drug Abuse",
|
|
||||||
"alcoholAbuse": "Alcohol Abuse",
|
|
||||||
"gambling": "Gambling",
|
|
||||||
"selfHarm": "Self-harm",
|
|
||||||
"childAbuse": "Child Abuse",
|
|
||||||
"other": "Other"
|
|
||||||
},
|
|
||||||
"poll": "Poll",
|
"poll": "Poll",
|
||||||
"pollsRecent": "Recent Polls",
|
"pollsRecent": "Recent Polls",
|
||||||
"pollCreateNew": "Create New",
|
"pollCreateNew": "Create New",
|
||||||
@@ -1075,5 +1058,421 @@
|
|||||||
"deleteRecycledFiles": "Delete Recycled Files",
|
"deleteRecycledFiles": "Delete Recycled Files",
|
||||||
"recycledFilesDeleted": "Recycled files deleted successfully",
|
"recycledFilesDeleted": "Recycled files deleted successfully",
|
||||||
"failedToDeleteRecycledFiles": "Failed to delete recycled files",
|
"failedToDeleteRecycledFiles": "Failed to delete recycled files",
|
||||||
"upload": "Upload"
|
"upload": "Upload",
|
||||||
|
"deleteMessage": "Delete Message",
|
||||||
|
"deleteMessageConfirmation": "Are you sure you want to delete this message?",
|
||||||
|
"customReaction": "Custom Reaction",
|
||||||
|
"customReactions": "Custom Reactions",
|
||||||
|
"stickerPlaceholder": "Sticker Placeholder",
|
||||||
|
"reactionAttitude": "Reaction Attitude",
|
||||||
|
"addReaction": "Add Reaction",
|
||||||
|
"eventCalendar": "Event Calendar",
|
||||||
|
"eventCalendarEmpty": "No events on that day.",
|
||||||
|
"walletStats": "Wallet Statistics",
|
||||||
|
"totalTransactions": "Total Transactions",
|
||||||
|
"totalOrders": "Total Orders",
|
||||||
|
"totalIncome": "Total Income",
|
||||||
|
"totalOutgoing": "Total Outgoing",
|
||||||
|
"netBalance": "Net Balance",
|
||||||
|
"messageUpdateLinks": "Server generated links previews",
|
||||||
|
"messageUpdateEdited": "Edited a message",
|
||||||
|
"settingsCardBackgroundOpacity": "Card Background Opacity",
|
||||||
|
"settingsThemeMode": "Theme Mode",
|
||||||
|
"settingsThemeModeSystem": "System",
|
||||||
|
"settingsThemeModeLight": "Light",
|
||||||
|
"settingsThemeModeDark": "Dark",
|
||||||
|
"enterPin": "Enter your PIN code",
|
||||||
|
"chatReplyingTo": "Replying to {}",
|
||||||
|
"chatForwarding": "Forwarding message",
|
||||||
|
"chatEditing": "Editing message",
|
||||||
|
"chatNoContent": "No content",
|
||||||
|
"sensitiveCategories": {
|
||||||
|
"language": "Language",
|
||||||
|
"sexualContent": "Sexual Content",
|
||||||
|
"violence": "Violence",
|
||||||
|
"profanity": "Profanity",
|
||||||
|
"hateSpeech": "Hate Speech",
|
||||||
|
"racism": "Racism",
|
||||||
|
"adultContent": "Adult Content",
|
||||||
|
"drugAbuse": "Drug Abuse",
|
||||||
|
"alcoholAbuse": "Alcohol Abuse",
|
||||||
|
"gambling": "Gambling",
|
||||||
|
"selfHarm": "Self-harm",
|
||||||
|
"childAbuse": "Child Abuse",
|
||||||
|
"other": "Other"
|
||||||
|
},
|
||||||
|
"Searching...": "Searching...",
|
||||||
|
"searchError": "Search failed. Please try again.",
|
||||||
|
"tryDifferentKeywords": "Try different keywords or remove search filters",
|
||||||
|
"settingsWindowOpacity": "Window Opacity",
|
||||||
|
"messageContent": "Message Content",
|
||||||
|
"updateAvailable": "Update available",
|
||||||
|
"noChangelogProvided": "No changelog provided.",
|
||||||
|
"useSecondarySourceForDownload": "Use secondary source for download",
|
||||||
|
"installUpdate": "Install update",
|
||||||
|
"openReleasePage": "Open release page",
|
||||||
|
"postCompose": "Compose Post",
|
||||||
|
"postPublish": "Publish Post",
|
||||||
|
"restoreDraftTitle": "Restore Draft",
|
||||||
|
"restoreDraftMessage": "A draft was found. Do you want to restore it?",
|
||||||
|
"draft": "Draft",
|
||||||
|
"purchaseGift": "Purchase Gift",
|
||||||
|
"selectRecipient": "Select Recipient",
|
||||||
|
"changeRecipient": "Change Recipient",
|
||||||
|
"addMessage": "Add Message",
|
||||||
|
"skipRecipient": "Skip Recipient",
|
||||||
|
"giftSubscriptions": "Gift Subscriptions",
|
||||||
|
"purchaseAGift": "Purchase a Gift",
|
||||||
|
"redeemAGift": "Redeem a Gift",
|
||||||
|
"giftHistory": "Gift History",
|
||||||
|
"sentGifts": "Sent Gifts",
|
||||||
|
"receivedGifts": "Received Gifts",
|
||||||
|
"noSentGifts": "No sent gifts",
|
||||||
|
"noReceivedGifts": "No received gifts",
|
||||||
|
"stellarGift": "Stellar Gift",
|
||||||
|
"novaGift": "Nova Gift",
|
||||||
|
"supernovaGift": "Supernova Gift",
|
||||||
|
"sameAsMembership": "Same as membership",
|
||||||
|
"enterGiftCodeToRedeem": "Enter gift code to redeem",
|
||||||
|
"enterGiftCode": "Enter gift code",
|
||||||
|
"giftPurchased": "Gift Purchased!",
|
||||||
|
"shareCodeWithRecipient": "Share this code with the recipient to redeem the gift.",
|
||||||
|
"openGiftAnyoneCanRedeem": "This is an open gift that anyone can redeem.",
|
||||||
|
"ok": "OK",
|
||||||
|
"selectedRecipient": "Selected recipient",
|
||||||
|
"noRecipientSelected": "No recipient selected",
|
||||||
|
"thisWillBeAnOpenGift": "This will be an open gift",
|
||||||
|
"personalMessage": "Personal Message",
|
||||||
|
"addPersonalMessageForRecipient": "Add a personal message for the recipient",
|
||||||
|
"giftStatusCreated": "Created",
|
||||||
|
"giftStatusSent": "Sent",
|
||||||
|
"giftStatusRedeemed": "Redeemed",
|
||||||
|
"giftStatusCancelled": "Cancelled",
|
||||||
|
"giftStatusExpired": "Expired",
|
||||||
|
"giftStatusUnknown": "Unknown",
|
||||||
|
"giftCodeCopiedToClipboard": "Gift code copied to clipboard",
|
||||||
|
"codeLabel": "Code: ",
|
||||||
|
"subscriptionLabel": "Subscription: ",
|
||||||
|
"toLabel": "To: ",
|
||||||
|
"fromLabel": "From: ",
|
||||||
|
"messageLabel": "Message: ",
|
||||||
|
"giftRedeemed": "Gift Redeemed!",
|
||||||
|
"giftRedeemedSuccessfully": "You have successfully redeemed the gift. Your new subscription is now active.",
|
||||||
|
"cancelGift": "Cancel Gift",
|
||||||
|
"cancelGiftConfirm": "Are you sure you want to cancel this gift? This action cannot be undone.",
|
||||||
|
"giftCancelledSuccessfully": "Gift cancelled successfully",
|
||||||
|
"createFund": "Create Fund",
|
||||||
|
"fundAmount": "Fund Amount",
|
||||||
|
"enterAmount": "Enter Amount",
|
||||||
|
"selectCurrency": "Select Currency",
|
||||||
|
"splitType": "Split Type",
|
||||||
|
"evenSplit": "Even Split",
|
||||||
|
"equalAmountEach": "Equal amount for each recipient",
|
||||||
|
"randomSplit": "Random Split",
|
||||||
|
"randomAmountEach": "Random amount for each recipient",
|
||||||
|
"recipientCount": "Recipient Count",
|
||||||
|
"numberOfRecipients": "Number of Recipients",
|
||||||
|
"addPersonalMessageForRecipients": "Add a personal message for recipients",
|
||||||
|
"invalidAmount": "Invalid amount",
|
||||||
|
"invalidRecipientCount": "Invalid recipient count",
|
||||||
|
"fundOverview": "Fund Overview",
|
||||||
|
"totalFundsSent": "Total Funds Sent",
|
||||||
|
"totalFundsReceived": "Total Funds Received",
|
||||||
|
"transactions": "Transactions",
|
||||||
|
"myFunds": "My Funds",
|
||||||
|
"availableFunds": "Available Funds",
|
||||||
|
"fundStatusCreated": "Created",
|
||||||
|
"fundStatusPartial": "Partially Claimed",
|
||||||
|
"fundStatusCompleted": "Fully Claimed",
|
||||||
|
"fundStatusExpired": "Expired",
|
||||||
|
"fundStatusUnknown": "Unknown",
|
||||||
|
"recipients": "Recipients",
|
||||||
|
"fundClaimedSuccessfully": "Fund claimed successfully!",
|
||||||
|
"claim": "Claim",
|
||||||
|
"noFundsCreated": "No funds created yet",
|
||||||
|
"createYourFirstFund": "Create your first fund to get started",
|
||||||
|
"noAvailableFunds": "No available funds",
|
||||||
|
"fundsWillAppearHere": "Funds you can claim will appear here",
|
||||||
|
"fundCreatedSuccessfully": "Fund created successfully!",
|
||||||
|
"selectRecipients": "Select Recipients",
|
||||||
|
"noRecipientsSelected": "No recipients selected",
|
||||||
|
"selectRecipientsToSendFund": "Select recipients to send the fund to",
|
||||||
|
"addRecipient": "Add Recipient",
|
||||||
|
"addMoreRecipients": "Add More Recipients",
|
||||||
|
"transactionDetails": "Transaction Details",
|
||||||
|
"remarks": "Remarks",
|
||||||
|
"payer": "Payer",
|
||||||
|
"payee": "Payee",
|
||||||
|
"transactionType": "Transaction Type",
|
||||||
|
"transfer": "Transfer",
|
||||||
|
"payment": "Payment",
|
||||||
|
"systemWallet": "System Wallet",
|
||||||
|
"date": "Date",
|
||||||
|
"createTransfer": "Create Transfer",
|
||||||
|
"transferAmount": "Transfer Amount",
|
||||||
|
"selectPayee": "Select Payee",
|
||||||
|
"selectedPayee": "Selected Payee",
|
||||||
|
"noPayeeSelected": "No payee selected",
|
||||||
|
"selectPayeeToTransfer": "Select payee to transfer to",
|
||||||
|
"addRemark": "Add Remark",
|
||||||
|
"transferRemark": "Transfer Remark",
|
||||||
|
"addRemarkForTransfer": "Add remark for transfer",
|
||||||
|
"enterPinToConfirmTransfer": "Enter your 6-digit PIN to confirm transfer",
|
||||||
|
"transferCreatedSuccessfully": "Transfer created successfully!",
|
||||||
|
"postUpdate": "Update",
|
||||||
|
"fileMetadata": "File Metadata",
|
||||||
|
"resend": "Resend",
|
||||||
|
"fileInfoTitle": "File Information",
|
||||||
|
"download": "Download",
|
||||||
|
"info": "Info",
|
||||||
|
"noStickers": "No Stickers",
|
||||||
|
"noStickersInPack": "This pack does not contains stickers",
|
||||||
|
"noStickerPacks": "No Sticker Packs",
|
||||||
|
"refresh": "Refresh",
|
||||||
|
"spoiler": "Spoiler",
|
||||||
|
"activityHeatmap": "Activity Heatmap",
|
||||||
|
"custom": "Custom",
|
||||||
|
"usernameColor": "Username Color",
|
||||||
|
"colorType": "Color Type",
|
||||||
|
"plain": "Plain",
|
||||||
|
"gradient": "Gradient",
|
||||||
|
"colorValue": "Color Value",
|
||||||
|
"gradientDirection": "Gradient Direction",
|
||||||
|
"gradientDirectionToRight": "To Right",
|
||||||
|
"gradientDirectionToLeft": "To Left",
|
||||||
|
"gradientDirectionToBottom": "To Bottom",
|
||||||
|
"gradientDirectionToTop": "To Top",
|
||||||
|
"gradientDirectionToBottomRight": "To Bottom Right",
|
||||||
|
"gradientDirectionToBottomLeft": "To Bottom Left",
|
||||||
|
"gradientDirectionToTopRight": "To Top Right",
|
||||||
|
"gradientDirectionToTopLeft": "To Top Left",
|
||||||
|
"gradientColors": "Gradient Colors",
|
||||||
|
"color": "Color",
|
||||||
|
"addColor": "Add Color",
|
||||||
|
"availableWithYourPlan": "Available with your plan",
|
||||||
|
"upgradeRequired": "Upgrade required",
|
||||||
|
"settingsDisableAnimation": "Disable Animation",
|
||||||
|
"addTag": "Add Tag",
|
||||||
|
"accountConnectionProviderSpotify": "Spotify",
|
||||||
|
"accountConnectionProviderSteam": "Steam",
|
||||||
|
"timezoneNotFound": "Time zone not found",
|
||||||
|
"awardPoints": "Awarded {} points",
|
||||||
|
"postFeaturedOn": "Post featured on {}",
|
||||||
|
"messageSentAt": "Sent at {}",
|
||||||
|
"myTickets": "My Tickets",
|
||||||
|
"drawHistory": "Draw History",
|
||||||
|
"lottery": "Lottery",
|
||||||
|
"noLotteryTickets": "No lottery tickets yet",
|
||||||
|
"buyYourFirstTicket": "Buy your first lottery ticket to get started!",
|
||||||
|
"buyTicket": "Buy Ticket",
|
||||||
|
"ticketNumbers": "Numbers: {}, Special: {}",
|
||||||
|
"cost": "Cost",
|
||||||
|
"multiplier": "Multiplier",
|
||||||
|
"prizeWon": "Prize Won",
|
||||||
|
"pending": "Pending",
|
||||||
|
"drawn": "Drawn",
|
||||||
|
"won": "Won",
|
||||||
|
"lost": "Lost",
|
||||||
|
"noDrawHistory": "No draw history yet",
|
||||||
|
"buyLotteryTicket": "Buy Lottery Ticket",
|
||||||
|
"selectNumbers": "Select Numbers",
|
||||||
|
"select5UniqueNumbers": "Select 5 unique numbers",
|
||||||
|
"selectSpecialNumber": "Select Special Number",
|
||||||
|
"selectMultiplier": "Select Multiplier",
|
||||||
|
"baseCost": "Base Cost",
|
||||||
|
"totalCost": "Total Cost",
|
||||||
|
"prizeStructure": "Prize Structure",
|
||||||
|
"enterPinToConfirmPurchase": "Enter your PIN to confirm purchase",
|
||||||
|
"ticketPurchasedSuccessfully": "Ticket purchased successfully!",
|
||||||
|
"winningNumbers": "Winning Numbers",
|
||||||
|
"specialNumber": "Special Number",
|
||||||
|
"totalTickets": "Total Tickets",
|
||||||
|
"totalWinners": "Total Winners",
|
||||||
|
"prizePool": "Prize Pool",
|
||||||
|
"enterPinToConfirmPayment": "Enter your PIN code to confirm payment",
|
||||||
|
"purchase": "Purchase",
|
||||||
|
"multiplierLabel": "Multiplier",
|
||||||
|
"specialOnly": "Special Only",
|
||||||
|
"matches": "Matches",
|
||||||
|
"thoughtDefaultTopic": "Reflection",
|
||||||
|
"thoughtAiName": "SN-chan",
|
||||||
|
"thoughtUserName": "You",
|
||||||
|
"thoughtStreamingHint": "Sn-chan is thinking...",
|
||||||
|
"thoughtInputHint": "Ask sn-chan anything...",
|
||||||
|
"thoughtNewConversation": "Start New Conversation",
|
||||||
|
"thoughtParseError": "Failed to parse AI response",
|
||||||
|
"thoughtFunctionCall": "Use {}",
|
||||||
|
"aiThought": "AI Thought",
|
||||||
|
"aiThoughtTitle": "Let sn-chan think",
|
||||||
|
"postReferenceUnavailable": "Referenced post is unavailable",
|
||||||
|
"fabLocation": "FAB Location",
|
||||||
|
"activities": "Activities",
|
||||||
|
"presenceTypeGaming": "Playing",
|
||||||
|
"presenceTypeMusic": "Listening to Music",
|
||||||
|
"presenceTypeWorkout": "Working out",
|
||||||
|
"articleCompose": "Compose Article",
|
||||||
|
"backToHub": "Back to Hub",
|
||||||
|
"advancedFilters": "Advanced Filters",
|
||||||
|
"searchPosts": "Search Posts",
|
||||||
|
"sortBy": "Sort by",
|
||||||
|
"fromDate": "From Date",
|
||||||
|
"toDate": "To Date",
|
||||||
|
"popularity": "Popularity",
|
||||||
|
"descendingOrder": "Descending Order",
|
||||||
|
"selectDate": "Select Date",
|
||||||
|
"pinnedPosts": "Pinned Posts",
|
||||||
|
"customReactionHint": "Custom Reaction allow you to use user uploaded stickers as the symbol of the reaction for the post. Exclusive for Stellar Program members.",
|
||||||
|
"publicationSites": "Publication Sites",
|
||||||
|
"uploadTasks": "Upload Tasks",
|
||||||
|
"thoughtFunctionCallBegin": "Calling tool {}",
|
||||||
|
"thoughtFunctionCallFinish": "{} responded",
|
||||||
|
"thoughtUnpaidHint": "Thinking unavaiable due to unpaid orders",
|
||||||
|
"more": "More",
|
||||||
|
"collapse": "Collapse",
|
||||||
|
"pollConfirmDiscard": "Are you sure you want to leave? All the poll data you're editing will not be saved.",
|
||||||
|
"discard": "Discard",
|
||||||
|
"fund": "Fund",
|
||||||
|
"fundsRecent": "Recent Funds",
|
||||||
|
"fundCreateNew": "Create New",
|
||||||
|
"fundCreateNewHint": "Create a new fund for your message. Select recipients and amount.",
|
||||||
|
"amountOfSplits": "Amount of Splits",
|
||||||
|
"enterNumberOfSplits": "Enter Splits Amount",
|
||||||
|
"orCreateWith": "Or\ncreate with",
|
||||||
|
"unindexedFiles": "Unindexed files",
|
||||||
|
"folder": "Folder",
|
||||||
|
"clearCompleted": "Clear Completed",
|
||||||
|
"uploadSuccess": "Upload successful!",
|
||||||
|
"wouldYouLikeToViewFile": "Would you like to view the file?",
|
||||||
|
"contentCantEmpty": "Content cannot be empty",
|
||||||
|
"features": "Features",
|
||||||
|
"unnamed": "Unnamed",
|
||||||
|
"fundEnvelopeLoadFailed": "Failed to load fund envelope",
|
||||||
|
"fundEnvelope": "Fund Envelope",
|
||||||
|
"fundEnvelopeRemaining": "Remaining: {} {}",
|
||||||
|
"fundEnvelopeSplit": "Split: {}",
|
||||||
|
"fundEnvelopeSplitEvenly": "Evenly",
|
||||||
|
"fundEnvelopeSplitRandomly": "Randomly",
|
||||||
|
"fundEnvelopeClaimSuccess": "Fund claimed successfully!",
|
||||||
|
"fundEnvelopeStatusCreated": "Created",
|
||||||
|
"fundEnvelopeStatusPartial": "Partially Claimed",
|
||||||
|
"fundEnvelopeStatusCompleted": "Fully Claimed",
|
||||||
|
"fundEnvelopeStatusExpired": "Expired",
|
||||||
|
"fundEnvelopeStatusUnknown": "Unknown",
|
||||||
|
"fundEnvelopeRecipients": "Recipients ({}/{} claimed)",
|
||||||
|
"fundEnvelopeExpiredDaysAgo": {
|
||||||
|
"one": "Expired {} day ago",
|
||||||
|
"other": "Expired {} days ago"
|
||||||
|
},
|
||||||
|
"fundEnvelopeExpiresSoon": "Expires soon",
|
||||||
|
"fundEnvelopeExpiresInHours": {
|
||||||
|
"one": "Expires in {} hour",
|
||||||
|
"other": "Expires in {} hours"
|
||||||
|
},
|
||||||
|
"fundEnvelopeExpiresInDays": {
|
||||||
|
"one": "Expires in {} day",
|
||||||
|
"other": "Expires in {} days"
|
||||||
|
},
|
||||||
|
"fundEnvelopeRemainingWithSplits": "{} {} / {} splits",
|
||||||
|
"fundEnvelopeUnknownUser": "Unknown User",
|
||||||
|
"deleteSite": "Delete Site",
|
||||||
|
"deleteSiteConfirm": "Are you sure you want to delete this site?",
|
||||||
|
"siteDeletedSuccess": "Site deleted successfully",
|
||||||
|
"siteSlug": "Slug",
|
||||||
|
"siteSlugHint": "my-site",
|
||||||
|
"siteSlugRequired": "Please enter a slug",
|
||||||
|
"siteSlugInvalid": "Slug can only contain lowercase letters, numbers, and dashes",
|
||||||
|
"siteName": "Site Name",
|
||||||
|
"siteNameHint": "My Publication Site",
|
||||||
|
"siteNameRequired": "Please enter a site name",
|
||||||
|
"siteMode": "Mode",
|
||||||
|
"siteModeFullyManaged": "Fully Managed",
|
||||||
|
"siteModeSelfManaged": "Self-Managed",
|
||||||
|
"editPublicationSite": "Edit Publication Site",
|
||||||
|
"deletePublicationSite": "Delete Publication Site",
|
||||||
|
"publicationSiteSavedSuccess": "Publication site saved successfully",
|
||||||
|
"publicationSiteDeleteConfirm": "Are you sure you want to delete this publication site? This action cannot be undone.",
|
||||||
|
"publicationSiteDeletedSuccess": "Publication site deleted successfully",
|
||||||
|
"newPublicationSite": "New Publication Site",
|
||||||
|
"siteDetails": "Site Details",
|
||||||
|
"siteInformation": "Site Information",
|
||||||
|
"siteDomain": "Domain",
|
||||||
|
"siteCreated": "Created",
|
||||||
|
"siteUpdated": "Updated",
|
||||||
|
"failedToLoadSite": "Failed to load site",
|
||||||
|
"sitePages": "Pages",
|
||||||
|
"noPagesYet": "No pages yet",
|
||||||
|
"createFirstPage": "Create your first page to get started",
|
||||||
|
"failedToLoadPages": "Failed to load pages",
|
||||||
|
"fileManagement": "File Management",
|
||||||
|
"siteFiles": "Files",
|
||||||
|
"siteFolder": "Folder",
|
||||||
|
"siteRoot": "Root",
|
||||||
|
"noFilesUploadedYet": "No files uploaded yet",
|
||||||
|
"uploadFirstFile": "Upload your first file to get started",
|
||||||
|
"failedToLoadFiles": "Failed to load files",
|
||||||
|
"noFilesFoundInFolder": "No files found in the selected folder",
|
||||||
|
"fileActions": "File Actions",
|
||||||
|
"purgeFiles": "Purge Files",
|
||||||
|
"purgeFilesDescription": "Remove all uploaded files from the site",
|
||||||
|
"deploySite": "Deploy Site",
|
||||||
|
"deploySiteDescription": "Upload and deploy a new version from ZIP archive",
|
||||||
|
"confirmPurge": "Confirm Purge",
|
||||||
|
"purgeFilesConfirm": "This will permanently delete all files uploaded to this site. This action cannot be undone. Are you sure you want to continue?",
|
||||||
|
"purgeAllFiles": "Purge All Files",
|
||||||
|
"allFilesPurgedSuccess": "All files purged successfully",
|
||||||
|
"failedToPurgeFiles": "Failed to purge files: {}",
|
||||||
|
"siteDeployedSuccess": "Site deployed successfully",
|
||||||
|
"failedToDeploySite": "Failed to deploy site: {}",
|
||||||
|
"createPage": "Create Page",
|
||||||
|
"editPage": "Edit Page",
|
||||||
|
"pageType": "Page Type",
|
||||||
|
"htmlPage": "HTML Page",
|
||||||
|
"redirectPage": "Redirect Page",
|
||||||
|
"pageTypeRequired": "Please select a page type",
|
||||||
|
"pagePath": "Page Path",
|
||||||
|
"pagePathHint": "/about, /contact, etc.",
|
||||||
|
"pagePathRequired": "Please enter a page path",
|
||||||
|
"pagePathInvalid": "Page path can only contain letters, numbers, hyphens, underscores, and slashes",
|
||||||
|
"pagePathMustStartWithSlash": "Page path must start with /",
|
||||||
|
"pagePathNoConsecutiveSlashes": "Page path cannot have consecutive slashes",
|
||||||
|
"pageTitle": "Page Title",
|
||||||
|
"pageTitleHint": "About Us, Contact, etc.",
|
||||||
|
"pageTitleRequired": "Please enter a page title",
|
||||||
|
"pageContentHtml": "Page Content (HTML)",
|
||||||
|
"pageContentHint": "<h1>Hello World</h1><p>This is my page content...</p>",
|
||||||
|
"pageContentRequired": "Please enter HTML content for the page",
|
||||||
|
"redirectTarget": "Redirect Target",
|
||||||
|
"redirectTargetHint": "/new-page, https://example.com, etc.",
|
||||||
|
"redirectTargetRequired": "Please enter a redirect target",
|
||||||
|
"redirectTargetInvalid": "Target must be a relative path (/) or absolute URL (http/https)",
|
||||||
|
"deletePage": "Delete Page",
|
||||||
|
"deletePageConfirm": "Are you sure you want to delete this page?",
|
||||||
|
"savePage": "Save Page",
|
||||||
|
"pageCreatedSuccess": "Page created successfully",
|
||||||
|
"pageUpdatedSuccess": "Page updated successfully",
|
||||||
|
"pageDeletedSuccess": "Page deleted successfully",
|
||||||
|
"uploadFiles": "Upload Files",
|
||||||
|
"uploadPath": "Upload Path",
|
||||||
|
"uploadPathHint": "/ (root) or /assets/images/",
|
||||||
|
"uploadPathRequired": "Please enter an upload path",
|
||||||
|
"uploadPathMustStartWithSlash": "Path must start with /",
|
||||||
|
"uploadPathNoSpaces": "Path cannot contain spaces",
|
||||||
|
"uploadPathNoConsecutiveSlashes": "Path cannot have consecutive slashes",
|
||||||
|
"percentCompleted": "{}% completed",
|
||||||
|
"filesToUpload": "{} files to upload",
|
||||||
|
"fileSizeKb": "Size: {} KB",
|
||||||
|
"uploadingEllipsis": "Uploading...",
|
||||||
|
"uploadFilesCount": {
|
||||||
|
"one": "Upload {} File",
|
||||||
|
"other": "Upload {} Files"
|
||||||
|
},
|
||||||
|
"allUploadsCompleted": "All uploads completed",
|
||||||
|
"someUploadsFailed": "Some uploads failed",
|
||||||
|
"uploadingInProgress": "Uploading in progress",
|
||||||
|
"readyToUpload": "Ready to upload",
|
||||||
|
"allFilesUploadedSuccess": "All files uploaded successfully",
|
||||||
|
"lotteryLastNumberSpecial": "The last selected number will be your special number.",
|
||||||
|
"lotteryMultiplierRequired": "Please enter a multiplier",
|
||||||
|
"lotteryMultiplierRange": "Multiplier must be between 1 and 10",
|
||||||
|
"dropToShare": "Drop to share"
|
||||||
}
|
}
|
||||||
@@ -164,8 +164,6 @@
|
|||||||
"checkInResultLevel3": "吉",
|
"checkInResultLevel3": "吉",
|
||||||
"checkInResultLevel4": "大吉",
|
"checkInResultLevel4": "大吉",
|
||||||
"checkInActivityTitle": "{} 在 {} 签到并获得了 {}",
|
"checkInActivityTitle": "{} 在 {} 签到并获得了 {}",
|
||||||
"eventCalander": "活动日历",
|
|
||||||
"eventCalanderEmpty": "该日无活动。",
|
|
||||||
"fortuneGraph": "时运趋势",
|
"fortuneGraph": "时运趋势",
|
||||||
"noFortuneData": "本月沒有时运數據。",
|
"noFortuneData": "本月沒有时运數據。",
|
||||||
"creatorHub": "创作者中心",
|
"creatorHub": "创作者中心",
|
||||||
@@ -251,10 +249,10 @@
|
|||||||
"translatorBadgeName": "翻译者",
|
"translatorBadgeName": "翻译者",
|
||||||
"translatorBadgeDescription": "协助将 Solar Network 翻译成不同语言",
|
"translatorBadgeDescription": "协助将 Solar Network 翻译成不同语言",
|
||||||
"wallet": "钱包",
|
"wallet": "钱包",
|
||||||
"walletCurrencyPoints": "源能点",
|
"walletCurrencyPoints": "新太阳点",
|
||||||
"walletCurrencyShortPoints": "NSP",
|
"walletCurrencyShortPoints": "NSP",
|
||||||
"walletCurrencyGolds": "星辰碎片",
|
"walletCurrencyGolds": "太阳币",
|
||||||
"walletCurrencyShortGolds": "SHD",
|
"walletCurrencyShortGolds": "TSD",
|
||||||
"retry": "重试",
|
"retry": "重试",
|
||||||
"creatorHubUnselectedHint": "选择/创建一个发布者以开始使用。",
|
"creatorHubUnselectedHint": "选择/创建一个发布者以开始使用。",
|
||||||
"relationships": "关系",
|
"relationships": "关系",
|
||||||
@@ -752,21 +750,6 @@
|
|||||||
"rename": "重命名",
|
"rename": "重命名",
|
||||||
"markAsSensitive": "标记为敏感",
|
"markAsSensitive": "标记为敏感",
|
||||||
"fileName": "文件名",
|
"fileName": "文件名",
|
||||||
"sensitiveCategories": {
|
|
||||||
"language": "语言",
|
|
||||||
"sexualContent": "色情内容",
|
|
||||||
"violence": "暴力",
|
|
||||||
"profanity": "亵渎",
|
|
||||||
"hateSpeech": "仇恨言论",
|
|
||||||
"racism": "种族主义",
|
|
||||||
"adultContent": "成人内容",
|
|
||||||
"drugAbuse": "药物滥用",
|
|
||||||
"alcoholAbuse": "酗酒",
|
|
||||||
"gambling": "赌博",
|
|
||||||
"selfHarm": "自残",
|
|
||||||
"childAbuse": "虐待儿童",
|
|
||||||
"other": "其他"
|
|
||||||
},
|
|
||||||
"poll": "投票",
|
"poll": "投票",
|
||||||
"pollsRecent": "最近投票",
|
"pollsRecent": "最近投票",
|
||||||
"pollCreateNew": "创建新投票",
|
"pollCreateNew": "创建新投票",
|
||||||
@@ -1076,12 +1059,241 @@
|
|||||||
"recycledFilesDeleted": "被回收文件成功删除",
|
"recycledFilesDeleted": "被回收文件成功删除",
|
||||||
"failedToDeleteRecycledFiles": "删除被回收文件失败",
|
"failedToDeleteRecycledFiles": "删除被回收文件失败",
|
||||||
"upload": "上传",
|
"upload": "上传",
|
||||||
"systemWallet": "中央统筹",
|
"deleteMessage": "删除消息",
|
||||||
|
"deleteMessageConfirmation": "您确定要删除这条消息吗?",
|
||||||
|
"customReaction": "自定义反应",
|
||||||
|
"customReactions": "自定义反应",
|
||||||
|
"stickerPlaceholder": "贴图占位符",
|
||||||
|
"reactionAttitude": "反应属性",
|
||||||
|
"addReaction": "添加反应",
|
||||||
|
"eventCalendar": "活动日历",
|
||||||
|
"eventCalendarEmpty": "该日无活动。",
|
||||||
|
"walletStats": "钱包统计",
|
||||||
|
"totalTransactions": "交易总额",
|
||||||
|
"totalOrders": "总订单",
|
||||||
|
"totalIncome": "总收入",
|
||||||
|
"totalOutgoing": "总支出",
|
||||||
|
"netBalance": "净余额",
|
||||||
|
"messageUpdateLinks": "服务器生成的链接预览",
|
||||||
|
"messageUpdateEdited": "编辑一条消息",
|
||||||
|
"settingsCardBackgroundOpacity": "卡片背景不透明度",
|
||||||
|
"settingsThemeMode": "主题模式",
|
||||||
|
"settingsThemeModeSystem": "跟随系统",
|
||||||
|
"settingsThemeModeLight": "亮色",
|
||||||
|
"settingsThemeModeDark": "暗色",
|
||||||
|
"enterPin": "请输入您的PIN码",
|
||||||
|
"chatReplyingTo": "回复给 {}",
|
||||||
|
"chatForwarding": "正在转发消息",
|
||||||
|
"chatEditing": "编辑消息",
|
||||||
|
"chatNoContent": "没有内容",
|
||||||
|
"sensitiveCategories": {
|
||||||
|
"language": "语言",
|
||||||
|
"sexualContent": "色情内容",
|
||||||
|
"violence": "暴力",
|
||||||
|
"profanity": "亵渎",
|
||||||
|
"hateSpeech": "仇恨言论",
|
||||||
|
"racism": "种族主义",
|
||||||
|
"adultContent": "成人内容",
|
||||||
|
"drugAbuse": "药物滥用",
|
||||||
|
"alcoholAbuse": "酗酒",
|
||||||
|
"gambling": "赌博",
|
||||||
|
"selfHarm": "自残",
|
||||||
|
"childAbuse": "虐待儿童",
|
||||||
|
"other": "其他"
|
||||||
|
},
|
||||||
|
"Searching...": "搜索中……",
|
||||||
|
"searchError": "搜索失败,请重试。",
|
||||||
|
"tryDifferentKeywords": "尝试不同的关键字或删除搜索过滤器",
|
||||||
|
"settingsWindowOpacity": "窗口不透明度",
|
||||||
|
"messageContent": "消息内容",
|
||||||
|
"updateAvailable": "更新可用",
|
||||||
|
"noChangelogProvided": "没有提供更新日志。",
|
||||||
|
"useSecondarySourceForDownload": "使用次要源下载",
|
||||||
|
"installUpdate": "安装更新",
|
||||||
|
"openReleasePage": "打开发行页面",
|
||||||
"postCompose": "撰写帖子",
|
"postCompose": "撰写帖子",
|
||||||
"postPublish": "发布帖子",
|
"postPublish": "发布帖子",
|
||||||
"restoreDraftTitle": "恢复草稿",
|
"restoreDraftTitle": "恢复草稿",
|
||||||
"restoreDraftMessage": "发现了一个草稿。你想要恢复它吗?",
|
"restoreDraftMessage": "发现了一个草稿。你想要恢复它吗?",
|
||||||
"draft": "草稿",
|
"draft": "草稿",
|
||||||
|
"purchaseGift": "购买礼物",
|
||||||
|
"selectRecipient": "选择款件人",
|
||||||
|
"changeRecipient": "修改款件人",
|
||||||
|
"addMessage": "添加信息",
|
||||||
|
"skipRecipient": "跳过款件人",
|
||||||
|
"giftSubscriptions": "礼物订阅",
|
||||||
|
"purchaseAGift": "购买礼物",
|
||||||
|
"redeemAGift": "兑换礼物",
|
||||||
|
"giftHistory": "礼物历史",
|
||||||
|
"sentGifts": "发送礼物",
|
||||||
|
"receivedGifts": "接收礼物",
|
||||||
|
"noSentGifts": "没有发送过礼物",
|
||||||
|
"noReceivedGifts": "没有收到过礼物",
|
||||||
|
"stellarGift": "恒星订阅",
|
||||||
|
"novaGift": "新星订阅",
|
||||||
|
"supernovaGift": "超新星订阅",
|
||||||
|
"sameAsMembership": "与成员相同",
|
||||||
|
"enterGiftCodeToRedeem": "输入礼品代码以兑换",
|
||||||
|
"enterGiftCode": "输入礼物代码",
|
||||||
|
"giftPurchased": "已购买礼物!",
|
||||||
|
"shareCodeWithRecipient": "与收件人分享此代码来兑换礼物。",
|
||||||
|
"openGiftAnyoneCanRedeem": "这是一份任何人都可以兑换的公开礼物。",
|
||||||
|
"ok": "确定",
|
||||||
|
"selectedRecipient": "选定收件人",
|
||||||
|
"noRecipientSelected": "未选择收件人",
|
||||||
|
"thisWillBeAnOpenGift": "这将是一份公开的礼物",
|
||||||
|
"personalMessage": "个人消息",
|
||||||
|
"addPersonalMessageForRecipient": "为收件人添加个人消息",
|
||||||
|
"giftStatusCreated": "已创建",
|
||||||
|
"giftStatusSent": "发送",
|
||||||
|
"giftStatusRedeemed": "已兑换",
|
||||||
|
"giftStatusCancelled": "已取消",
|
||||||
|
"giftStatusExpired": "已过期",
|
||||||
|
"giftStatusUnknown": "未知",
|
||||||
|
"giftCodeCopiedToClipboard": "礼物代码已复制到剪贴板",
|
||||||
|
"codeLabel": "代码: ",
|
||||||
|
"subscriptionLabel": "订阅: ",
|
||||||
|
"toLabel": "发送至: ",
|
||||||
|
"fromLabel": "来自: ",
|
||||||
|
"messageLabel": "消息: ",
|
||||||
|
"giftRedeemed": "礼物兑换成功!",
|
||||||
|
"giftRedeemedSuccessfully": "您已成功兑换了礼物。您的新订阅现在已经生效。",
|
||||||
|
"cancelGift": "取消礼物",
|
||||||
|
"cancelGiftConfirm": "您确定要取消此礼物?此操作不能撤消。",
|
||||||
|
"giftCancelledSuccessfully": "已成功取消礼物",
|
||||||
|
"createFund": "创建红包",
|
||||||
|
"fundAmount": "红包金额",
|
||||||
|
"enterAmount": "输入金额",
|
||||||
|
"selectCurrency": "选择币种",
|
||||||
|
"splitType": "拆分类型",
|
||||||
|
"evenSplit": "平均分配",
|
||||||
|
"equalAmountEach": "每个收款人的金额相同",
|
||||||
|
"randomSplit": "随机分配",
|
||||||
|
"randomAmountEach": "每个收款人的金额随机",
|
||||||
|
"recipientCount": "收款人总计",
|
||||||
|
"numberOfRecipients": "收款人数量",
|
||||||
|
"addPersonalMessageForRecipients": "为收款人添加个人信息",
|
||||||
|
"invalidAmount": "无效的金额",
|
||||||
|
"invalidRecipientCount": "收款人数量无效",
|
||||||
|
"fundOverview": "红包概述",
|
||||||
|
"totalFundsSent": "已发送的红包总额",
|
||||||
|
"totalFundsReceived": "收到的红包总额",
|
||||||
|
"transactions": "交易",
|
||||||
|
"myFunds": "我的支票",
|
||||||
|
"availableFunds": "可用支票",
|
||||||
|
"fundStatusCreated": "已创建",
|
||||||
|
"fundStatusPartial": "部分领取",
|
||||||
|
"fundStatusCompleted": "已领完",
|
||||||
|
"fundStatusExpired": "已过期",
|
||||||
|
"fundStatusUnknown": "未知",
|
||||||
|
"recipients": "收款人",
|
||||||
|
"fundClaimedSuccessfully": "支票领取成功!",
|
||||||
|
"claim": "申请",
|
||||||
|
"noFundsCreated": "尚未创建任何支票",
|
||||||
|
"createYourFirstFund": "创建您的第一个支票来开始",
|
||||||
|
"noAvailableFunds": "暂无可用支票",
|
||||||
|
"fundsWillAppearHere": "您可以领取的支票将出现在这里",
|
||||||
|
"fundCreatedSuccessfully": "支票创建成功!",
|
||||||
|
"selectRecipients": "选择收款人",
|
||||||
|
"noRecipientsSelected": "尚未选择收款人",
|
||||||
|
"selectRecipientsToSendFund": "选择收款人将支票发送到",
|
||||||
|
"addRecipient": "添加收款人",
|
||||||
|
"addMoreRecipients": "添加更多收款人",
|
||||||
|
"transactionDetails": "交易详情",
|
||||||
|
"remarks": "备注",
|
||||||
|
"payer": "付款方",
|
||||||
|
"payee": "交易方",
|
||||||
|
"transactionType": "交易类型",
|
||||||
|
"transfer": "转账",
|
||||||
|
"payment": "支付",
|
||||||
|
"systemWallet": "中央统筹",
|
||||||
|
"date": "日期",
|
||||||
|
"createTransfer": "创建转账",
|
||||||
|
"transferAmount": "转账金额",
|
||||||
|
"selectPayee": "请选择收款人",
|
||||||
|
"selectedPayee": "选定的收款人",
|
||||||
|
"noPayeeSelected": "没有选择收款人",
|
||||||
|
"selectPayeeToTransfer": "选择要转账的收款人",
|
||||||
|
"addRemark": "添加备注",
|
||||||
|
"transferRemark": "转账备注",
|
||||||
|
"addRemarkForTransfer": "为转账添加备注",
|
||||||
|
"enterPinToConfirmTransfer": "输入您的 6 位PIN码以确认转账",
|
||||||
|
"transferCreatedSuccessfully": "转账成功创建!",
|
||||||
|
"postUpdate": "更新",
|
||||||
|
"fileMetadata": "文件元数据",
|
||||||
|
"resend": "重新发送",
|
||||||
|
"fileInfoTitle": "文件信息",
|
||||||
|
"download": "下载",
|
||||||
|
"info": "信息",
|
||||||
|
"noStickers": "无贴图",
|
||||||
|
"noStickersInPack": "这个包没有贴纸",
|
||||||
|
"noStickerPacks": "无贴图包",
|
||||||
|
"refresh": "刷新",
|
||||||
|
"spoiler": "已隐藏",
|
||||||
|
"activityHeatmap": "活动热力图",
|
||||||
|
"custom": "自定义",
|
||||||
|
"usernameColor": "用户名颜色",
|
||||||
|
"colorType": "颜色类型",
|
||||||
|
"plain": "纯色",
|
||||||
|
"gradient": "渐变",
|
||||||
|
"colorValue": "色值",
|
||||||
|
"gradientDirection": "渐变方向",
|
||||||
|
"gradientDirectionToRight": "向右",
|
||||||
|
"gradientDirectionToLeft": "向左",
|
||||||
|
"gradientDirectionToBottom": "向底部",
|
||||||
|
"gradientDirectionToTop": "向上",
|
||||||
|
"gradientDirectionToBottomRight": "向右下角",
|
||||||
|
"gradientDirectionToBottomLeft": "向左下角",
|
||||||
|
"gradientDirectionToTopRight": "向右上角",
|
||||||
|
"gradientDirectionToTopLeft": "向左下角",
|
||||||
|
"gradientColors": "渐变颜色",
|
||||||
|
"color": "颜色",
|
||||||
|
"addColor": "添加颜色",
|
||||||
|
"availableWithYourPlan": "适用于您的计划",
|
||||||
|
"upgradeRequired": "需要升级恒星计划等级",
|
||||||
|
"settingsDisableAnimation": "禁用动画",
|
||||||
|
"addTag": "添加标签",
|
||||||
|
"accountConnectionProviderSpotify": "Spotify",
|
||||||
|
"accountConnectionProviderSteam": "Steam",
|
||||||
|
"timezoneNotFound": "未找到时区",
|
||||||
|
"awardPoints": "收到 {} 点奖励",
|
||||||
|
"postFeaturedOn": "帖子在 {} 被精选",
|
||||||
|
"messageSentAt": "在 {} 发送",
|
||||||
|
"myTickets": "我的彩票",
|
||||||
|
"drawHistory": "抽奖历史",
|
||||||
|
"lottery": "彩票",
|
||||||
|
"noLotteryTickets": "暂无彩票",
|
||||||
|
"buyYourFirstTicket": "购买您的第一张彩票开始!",
|
||||||
|
"buyTicket": "购买彩票",
|
||||||
|
"ticketNumbers": "数字: {}, 特殊数字: {}",
|
||||||
|
"cost": "花费",
|
||||||
|
"multiplier": "倍率",
|
||||||
|
"prizeWon": "获奖者",
|
||||||
|
"pending": "待开奖",
|
||||||
|
"drawn": "已开奖",
|
||||||
|
"won": "赢",
|
||||||
|
"lost": "输",
|
||||||
|
"noDrawHistory": "暂无开奖历史",
|
||||||
|
"buyLotteryTicket": "购买彩票",
|
||||||
|
"selectNumbers": "选择数字",
|
||||||
|
"select5UniqueNumbers": "选择 5 个唯一数字",
|
||||||
|
"selectSpecialNumber": "选择特殊数字",
|
||||||
|
"selectMultiplier": "选择倍数",
|
||||||
|
"baseCost": "基础花费",
|
||||||
|
"totalCost": "总费用",
|
||||||
|
"prizeStructure": "奖金分级",
|
||||||
|
"enterPinToConfirmPurchase": "输入您的 PIN 码以确认购买",
|
||||||
|
"ticketPurchasedSuccessfully": "彩票购买成功!",
|
||||||
|
"winningNumbers": "获胜数字",
|
||||||
|
"specialNumber": "特殊数字",
|
||||||
|
"totalTickets": "总售出票数",
|
||||||
|
"totalWinners": "中奖者总人数",
|
||||||
|
"prizePool": "奖金池",
|
||||||
|
"enterPinToConfirmPayment": "输入您的 PIN 码以确认付款",
|
||||||
|
"purchase": "购买",
|
||||||
|
"multiplierLabel": "倍率",
|
||||||
|
"specialOnly": "仅特殊的",
|
||||||
|
"matches": "场次",
|
||||||
"thoughtDefaultTopic": "寻思",
|
"thoughtDefaultTopic": "寻思",
|
||||||
"thoughtAiName": "SN 酱",
|
"thoughtAiName": "SN 酱",
|
||||||
"thoughtUserName": "您",
|
"thoughtUserName": "您",
|
||||||
@@ -1089,7 +1301,178 @@
|
|||||||
"thoughtInputHint": "问 SN 酱任何问题...",
|
"thoughtInputHint": "问 SN 酱任何问题...",
|
||||||
"thoughtNewConversation": "开始新对话",
|
"thoughtNewConversation": "开始新对话",
|
||||||
"thoughtParseError": "解析 AI 响应失败",
|
"thoughtParseError": "解析 AI 响应失败",
|
||||||
|
"thoughtFunctionCall": "调用 {} 函数",
|
||||||
"aiThought": "寻思",
|
"aiThought": "寻思",
|
||||||
"aiThoughtTitle": "让 SN 酱寻思寻思",
|
"aiThoughtTitle": "让 SN 酱寻思寻思",
|
||||||
"thoughtUnpaidHint": "寻思因为有未支付的订单而被禁用"
|
"postReferenceUnavailable": "引用的帖子不可用",
|
||||||
|
"fabLocation": "底部导航按钮位置",
|
||||||
|
"activities": "活动",
|
||||||
|
"presenceTypeGaming": "正在玩",
|
||||||
|
"presenceTypeMusic": "正在听音乐",
|
||||||
|
"presenceTypeWorkout": "锻炼中",
|
||||||
|
"articleCompose": "撰写文章",
|
||||||
|
"backToHub": "返回至主页",
|
||||||
|
"advancedFilters": "高级筛选",
|
||||||
|
"searchPosts": "搜索帖子",
|
||||||
|
"sortBy": "排序方式",
|
||||||
|
"fromDate": "起始日期",
|
||||||
|
"toDate": "截止日期",
|
||||||
|
"popularity": "按热度",
|
||||||
|
"descendingOrder": "降序排序",
|
||||||
|
"selectDate": "选择日期",
|
||||||
|
"pinnedPosts": "已置顶的帖子",
|
||||||
|
"customReactionHint": "自定义反应允许你使用用户上传贴纸作为帖子反应的符号,需要恒星计划订阅。",
|
||||||
|
"publicationSites": "发布者站点",
|
||||||
|
"uploadTasks": "上传任务",
|
||||||
|
"thoughtFunctionCallBegin": "调用工具 {}",
|
||||||
|
"thoughtFunctionCallFinish": "工具 {} 响应",
|
||||||
|
"thoughtUnpaidHint": "寻思因为有未支付的订单而被禁用",
|
||||||
|
"more": "更多",
|
||||||
|
"collapse": "折叠",
|
||||||
|
"pollConfirmDiscard": "您确定要离开吗?您编辑的所有数据都不会被保存。",
|
||||||
|
"discard": "放弃",
|
||||||
|
"fund": "支票",
|
||||||
|
"fundsRecent": "最近支票",
|
||||||
|
"fundCreateNew": "创建新支票",
|
||||||
|
"fundCreateNewHint": "为您的消息创建一个新的红包。选择接收者和金额。",
|
||||||
|
"amountOfSplits": "份数",
|
||||||
|
"enterNumberOfSplits": "单份金额",
|
||||||
|
"orCreateWith": "或\n使用第三方帐户注册",
|
||||||
|
"unindexedFiles": "未索引的文件",
|
||||||
|
"folder": "文件夹",
|
||||||
|
"clearCompleted": "清除已完成的",
|
||||||
|
"uploadSuccess": "Upload successful!",
|
||||||
|
"wouldYouLikeToViewFile": "Would you like to view the file?",
|
||||||
|
"contentCantEmpty": "Content cannot be empty",
|
||||||
|
"features": "Features",
|
||||||
|
"unnamed": "Unnamed",
|
||||||
|
"fundEnvelopeLoadFailed": "Failed to load fund envelope",
|
||||||
|
"fundEnvelope": "Fund Envelope",
|
||||||
|
"fundEnvelopeRemaining": "Remaining: {} {}",
|
||||||
|
"fundEnvelopeSplit": "Split: {}",
|
||||||
|
"fundEnvelopeSplitEvenly": "Evenly",
|
||||||
|
"fundEnvelopeSplitRandomly": "Randomly",
|
||||||
|
"fundEnvelopeClaimSuccess": "Fund claimed successfully!",
|
||||||
|
"fundEnvelopeStatusCreated": "Created",
|
||||||
|
"fundEnvelopeStatusPartial": "Partially Claimed",
|
||||||
|
"fundEnvelopeStatusCompleted": "Fully Claimed",
|
||||||
|
"fundEnvelopeStatusExpired": "Expired",
|
||||||
|
"fundEnvelopeStatusUnknown": "Unknown",
|
||||||
|
"fundEnvelopeRecipients": "Recipients ({}/{} claimed)",
|
||||||
|
"fundEnvelopeExpiredDaysAgo": {
|
||||||
|
"one": "Expired {} day ago",
|
||||||
|
"other": "Expired {} days ago"
|
||||||
|
},
|
||||||
|
"fundEnvelopeExpiresSoon": "Expires soon",
|
||||||
|
"fundEnvelopeExpiresInHours": {
|
||||||
|
"one": "Expires in {} hour",
|
||||||
|
"other": "Expires in {} hours"
|
||||||
|
},
|
||||||
|
"fundEnvelopeExpiresInDays": {
|
||||||
|
"one": "Expires in {} day",
|
||||||
|
"other": "Expires in {} days"
|
||||||
|
},
|
||||||
|
"fundEnvelopeRemainingWithSplits": "{} {} / {} splits",
|
||||||
|
"fundEnvelopeUnknownUser": "Unknown User",
|
||||||
|
"deleteSite": "Delete Site",
|
||||||
|
"deleteSiteConfirm": "Are you sure you want to delete this site?",
|
||||||
|
"siteDeletedSuccess": "Site deleted successfully",
|
||||||
|
"siteSlug": "Slug",
|
||||||
|
"siteSlugHint": "my-site",
|
||||||
|
"siteSlugRequired": "Please enter a slug",
|
||||||
|
"siteSlugInvalid": "Slug can only contain lowercase letters, numbers, and dashes",
|
||||||
|
"siteName": "Site Name",
|
||||||
|
"siteNameHint": "My Publication Site",
|
||||||
|
"siteNameRequired": "Please enter a site name",
|
||||||
|
"siteMode": "Mode",
|
||||||
|
"siteModeFullyManaged": "Fully Managed",
|
||||||
|
"siteModeSelfManaged": "Self-Managed",
|
||||||
|
"editPublicationSite": "Edit Publication Site",
|
||||||
|
"deletePublicationSite": "Delete Publication Site",
|
||||||
|
"publicationSiteSavedSuccess": "Publication site saved successfully",
|
||||||
|
"publicationSiteDeleteConfirm": "Are you sure you want to delete this publication site? This action cannot be undone.",
|
||||||
|
"publicationSiteDeletedSuccess": "Publication site deleted successfully",
|
||||||
|
"newPublicationSite": "New Publication Site",
|
||||||
|
"siteDetails": "Site Details",
|
||||||
|
"siteInformation": "Site Information",
|
||||||
|
"siteDomain": "Domain",
|
||||||
|
"siteCreated": "Created",
|
||||||
|
"siteUpdated": "Updated",
|
||||||
|
"failedToLoadSite": "Failed to load site",
|
||||||
|
"sitePages": "Pages",
|
||||||
|
"noPagesYet": "No pages yet",
|
||||||
|
"createFirstPage": "Create your first page to get started",
|
||||||
|
"failedToLoadPages": "Failed to load pages",
|
||||||
|
"fileManagement": "File Management",
|
||||||
|
"siteFiles": "Files",
|
||||||
|
"siteFolder": "Folder",
|
||||||
|
"siteRoot": "Root",
|
||||||
|
"noFilesUploadedYet": "No files uploaded yet",
|
||||||
|
"uploadFirstFile": "Upload your first file to get started",
|
||||||
|
"failedToLoadFiles": "Failed to load files",
|
||||||
|
"noFilesFoundInFolder": "No files found in the selected folder",
|
||||||
|
"fileActions": "File Actions",
|
||||||
|
"purgeFiles": "Purge Files",
|
||||||
|
"purgeFilesDescription": "Remove all uploaded files from the site",
|
||||||
|
"deploySite": "Deploy Site",
|
||||||
|
"deploySiteDescription": "Upload and deploy a new version from ZIP archive",
|
||||||
|
"confirmPurge": "Confirm Purge",
|
||||||
|
"purgeFilesConfirm": "This will permanently delete all files uploaded to this site. This action cannot be undone. Are you sure you want to continue?",
|
||||||
|
"purgeAllFiles": "Purge All Files",
|
||||||
|
"allFilesPurgedSuccess": "All files purged successfully",
|
||||||
|
"failedToPurgeFiles": "Failed to purge files: {}",
|
||||||
|
"siteDeployedSuccess": "Site deployed successfully",
|
||||||
|
"failedToDeploySite": "Failed to deploy site: {}",
|
||||||
|
"createPage": "Create Page",
|
||||||
|
"editPage": "Edit Page",
|
||||||
|
"pageType": "Page Type",
|
||||||
|
"htmlPage": "HTML Page",
|
||||||
|
"redirectPage": "Redirect Page",
|
||||||
|
"pageTypeRequired": "Please select a page type",
|
||||||
|
"pagePath": "Page Path",
|
||||||
|
"pagePathHint": "/about, /contact, etc.",
|
||||||
|
"pagePathRequired": "Please enter a page path",
|
||||||
|
"pagePathInvalid": "Page path can only contain letters, numbers, hyphens, underscores, and slashes",
|
||||||
|
"pagePathMustStartWithSlash": "Page path must start with /",
|
||||||
|
"pagePathNoConsecutiveSlashes": "Page path cannot have consecutive slashes",
|
||||||
|
"pageTitle": "Page Title",
|
||||||
|
"pageTitleHint": "About Us, Contact, etc.",
|
||||||
|
"pageTitleRequired": "Please enter a page title",
|
||||||
|
"pageContentHtml": "Page Content (HTML)",
|
||||||
|
"pageContentHint": "<h1>Hello World</h1><p>This is my page content...</p>",
|
||||||
|
"pageContentRequired": "Please enter HTML content for the page",
|
||||||
|
"redirectTarget": "Redirect Target",
|
||||||
|
"redirectTargetHint": "/new-page, https://example.com, etc.",
|
||||||
|
"redirectTargetRequired": "Please enter a redirect target",
|
||||||
|
"redirectTargetInvalid": "Target must be a relative path (/) or absolute URL (http/https)",
|
||||||
|
"deletePage": "Delete Page",
|
||||||
|
"deletePageConfirm": "Are you sure you want to delete this page?",
|
||||||
|
"savePage": "Save Page",
|
||||||
|
"pageCreatedSuccess": "Page created successfully",
|
||||||
|
"pageUpdatedSuccess": "Page updated successfully",
|
||||||
|
"pageDeletedSuccess": "Page deleted successfully",
|
||||||
|
"uploadFiles": "Upload Files",
|
||||||
|
"uploadPath": "Upload Path",
|
||||||
|
"uploadPathHint": "/ (root) or /assets/images/",
|
||||||
|
"uploadPathRequired": "Please enter an upload path",
|
||||||
|
"uploadPathMustStartWithSlash": "Path must start with /",
|
||||||
|
"uploadPathNoSpaces": "Path cannot contain spaces",
|
||||||
|
"uploadPathNoConsecutiveSlashes": "Path cannot have consecutive slashes",
|
||||||
|
"percentCompleted": "{}% completed",
|
||||||
|
"filesToUpload": "{} files to upload",
|
||||||
|
"fileSizeKb": "Size: {} KB",
|
||||||
|
"uploadingEllipsis": "Uploading...",
|
||||||
|
"uploadFilesCount": {
|
||||||
|
"one": "Upload {} File",
|
||||||
|
"other": "Upload {} Files"
|
||||||
|
},
|
||||||
|
"allUploadsCompleted": "All uploads completed",
|
||||||
|
"someUploadsFailed": "Some uploads failed",
|
||||||
|
"uploadingInProgress": "Uploading in progress",
|
||||||
|
"readyToUpload": "Ready to upload",
|
||||||
|
"allFilesUploadedSuccess": "All files uploaded successfully",
|
||||||
|
"lotteryLastNumberSpecial": "The last selected number will be your special number.",
|
||||||
|
"lotteryMultiplierRequired": "Please enter a multiplier",
|
||||||
|
"lotteryMultiplierRange": "Multiplier must be between 1 and 10",
|
||||||
|
"dropToShare": "Drop to share"
|
||||||
}
|
}
|
||||||
@@ -164,177 +164,175 @@
|
|||||||
"checkInResultLevel3": "吉",
|
"checkInResultLevel3": "吉",
|
||||||
"checkInResultLevel4": "大吉",
|
"checkInResultLevel4": "大吉",
|
||||||
"checkInActivityTitle": "{} 於 {} 簽到,獲 {} 籤",
|
"checkInActivityTitle": "{} 於 {} 簽到,獲 {} 籤",
|
||||||
"eventCalander": "Event Calander",
|
"fortuneGraph": "运程谶",
|
||||||
"eventCalanderEmpty": "No events on that day.",
|
"noFortuneData": "本月气数未显。",
|
||||||
"fortuneGraph": "Fortune Trend",
|
"creatorHub": "著書閣",
|
||||||
"noFortuneData": "No fortune data available for this month.",
|
"creatorHubDescription": "司理帖文、析策諸務。",
|
||||||
"creatorHub": "Creator Hub",
|
"developerPortal": "匠造司",
|
||||||
"creatorHubDescription": "Manage posts, analytics, and more.",
|
"developerPortalDescription": "築基於 Solar Network™。",
|
||||||
"developerPortal": "Developer Portal",
|
"statusCreateHint": "何所思?可添近況。",
|
||||||
"developerPortalDescription": "Build with Solar Network™.",
|
"statusCreate": "增境況",
|
||||||
"statusCreateHint": "What's on your mind? Add a status.",
|
"statusUpdate": "更境況",
|
||||||
"statusCreate": "Add a Status",
|
"statusLabel": "心境",
|
||||||
"statusUpdate": "Update Status",
|
"statusAttitude": "意態",
|
||||||
"statusLabel": "Status",
|
"attitudePositive": "昂揚",
|
||||||
"statusAttitude": "Attitude",
|
"attitudeNeutral": "平瀾",
|
||||||
"attitudePositive": "Positive",
|
"attitudeNegative": "消沉",
|
||||||
"attitudeNeutral": "Neutral",
|
"statusInvisible": "隱跡",
|
||||||
"attitudeNegative": "Negative",
|
"statusInvisibleDescription": "汝之形跡不現於人前。",
|
||||||
"statusInvisible": "Invisible",
|
"statusNotDisturb": "勿擾",
|
||||||
"statusInvisibleDescription": "Your will be showing as offline to others.",
|
"statusNotDisturbDescription": "急報皆止。",
|
||||||
"statusNotDisturb": "Do Not Disturb",
|
"statusClearTime": "滌除於",
|
||||||
"statusNotDisturbDescription": "Push notification will be disabled.",
|
"statusNoAutoClear": "不自滌",
|
||||||
"statusClearTime": "Cleared At",
|
"online": "在朝",
|
||||||
"statusNoAutoClear": "Do not auto clear",
|
"offline": "在野",
|
||||||
"online": "Online",
|
"status": "心境",
|
||||||
"offline": "Offline",
|
"statusActivityTitle": "{} 正於 {} {}",
|
||||||
"status": "Status",
|
"statusActivityEndedTitle": "{} 正於 {} {} 迄於 {}",
|
||||||
"statusActivityTitle": "{} is {} {}",
|
"appSettings": "應用設置",
|
||||||
"statusActivityEndedTitle": "{} is {} {} until {}",
|
"accountSettings": "賬戶設置",
|
||||||
"appSettings": "App Settings",
|
"settings": "設置",
|
||||||
"accountSettings": "Account Settings",
|
"language": "言語",
|
||||||
"settings": "Settings",
|
"accountLanguageHint": "此言語將行於電郵並推送告示。",
|
||||||
"language": "Language",
|
"settingsDisplayLanguage": "顯示言語",
|
||||||
"accountLanguageHint": "This language will be used for email and push notifications.",
|
"languageFollowSystem": "從系統之制",
|
||||||
"settingsDisplayLanguage": "Display Language",
|
"postsCreatedCount": "貼文",
|
||||||
"languageFollowSystem": "Follow System",
|
"stickerPacksCreatedCount": "貼札集",
|
||||||
"postsCreatedCount": "Posts",
|
"stickersCreatedCount": "貼札",
|
||||||
"stickerPacksCreatedCount": "Sticker Packs",
|
"upvoteReceived": "收擢",
|
||||||
"stickersCreatedCount": "Stickers",
|
"downvoteReceived": "收黜",
|
||||||
"upvoteReceived": "Upvotes Recieved",
|
"stickerPacks": "貼札集",
|
||||||
"downvoteReceived": "Downvotes Recieved",
|
"createStickerPack": "始創一貼札集",
|
||||||
"stickerPacks": "Sticker Packs",
|
"editStickerPack": "修訂貼札集",
|
||||||
"createStickerPack": "Create a Sticker Pack",
|
"deleteStickerPack": "革去貼札集",
|
||||||
"editStickerPack": "Edit Sticker Pack",
|
"deleteStickerPackHint": "決革去此貼札譜否?此令無可更赦。",
|
||||||
"deleteStickerPack": "Delete Sticker Pack",
|
"stickerPackPrefix": "首綴",
|
||||||
"deleteStickerPackHint": "Are you sure to delete this sticker pack? This action cannot be undone.",
|
"stickerPackPrefixHint": "此首綴將加於本帙諸貼札縮略名之前。",
|
||||||
"stickerPackPrefix": "Prefix",
|
"stickers": "貼札",
|
||||||
"stickerPackPrefixHint": "The prefix will be added before each stickers' slug in this pack.",
|
"createSticker": "始創一貼札",
|
||||||
"stickers": "Stickers",
|
"editSticker": "修訂貼札",
|
||||||
"createSticker": "Create a Sticker",
|
"deleteSticker": "革去貼札",
|
||||||
"editSticker": "Edit Sticker",
|
"deleteStickerHint": "確欲革去此貼札乎?此勢無可逆轉。",
|
||||||
"deleteSticker": "Delete Sticker",
|
"stickerImage": "圖像",
|
||||||
"deleteStickerHint": "Are you sure to delete this sticker? This action cannot be undone.",
|
"stickerSlug": "籤碼",
|
||||||
"stickerImage": "Image",
|
"stickerSlugHint": "此籤碼將聯題端以構貼札獨契。",
|
||||||
"stickerSlug": "Slug",
|
"dataEmpty": "此間尚虛。",
|
||||||
"stickerSlugHint": "The slug will be combined with the prefix to form the sticker's unique identifier.",
|
"pickFile": "擇一卷宗",
|
||||||
"dataEmpty": "Nothing's here yet.",
|
"uploading": "上呈中",
|
||||||
"pickFile": "Pick a file",
|
"uploadingProgress": "正呈 {} 於 {}",
|
||||||
"uploading": "Uploading",
|
"uploadAll": "全盤呈上",
|
||||||
"uploadingProgress": "Uploading {} of {}",
|
"stickerCopyPlaceholder": "摹寫虛位",
|
||||||
"uploadAll": "Upload All",
|
"realmSelection": "擇一界域",
|
||||||
"stickerCopyPlaceholder": "Copy Placeholder",
|
"individual": "獨詣",
|
||||||
"realmSelection": "Select a Realm",
|
"firstPostBadgeName": "處女作",
|
||||||
"individual": "Individual",
|
"firstPostBadgeDescription": "始發帖於 Solar Network",
|
||||||
"firstPostBadgeName": "First Post",
|
"popularPostBadgeName": "熱帖",
|
||||||
"firstPostBadgeDescription": "Created your first post on Solar Network",
|
"popularPostBadgeDescription": "君作得眾譽",
|
||||||
"popularPostBadgeName": "Popular Post",
|
"viralPostBadgeName": "洛陽紙",
|
||||||
"popularPostBadgeDescription": "Your post received significant engagement from the community",
|
"viralPostBadgeDescription": "君文傳抄殆遍",
|
||||||
"viralPostBadgeName": "Viral Post",
|
"helpfulCommentBadgeName": "裨益論",
|
||||||
"viralPostBadgeDescription": "Your post went viral and reached a wide audience",
|
"helpfulCommentBadgeDescription": "諸君以為斯論足以啟迪來者",
|
||||||
"helpfulCommentBadgeName": "Helpful Comment",
|
"newcomerBadgeName": "新客",
|
||||||
"helpfulCommentBadgeDescription": "Your comment was marked as helpful by others",
|
"newcomerBadgeDescription": "欣迎加入 Solar Network!請開始遊覽結緣",
|
||||||
"newcomerBadgeName": "Newcomer",
|
"contributorBadgeName": "資士",
|
||||||
"newcomerBadgeDescription": "Welcome to Solar Network! Start exploring and connecting",
|
"contributorBadgeDescription": "勤力貢獻於 Solar Network 社群",
|
||||||
"contributorBadgeName": "Contributor",
|
"expertBadgeName": "達人",
|
||||||
"contributorBadgeDescription": "Actively contributing to the Solar Network community",
|
"expertBadgeDescription": "以專精與厚獻見稱於世",
|
||||||
"expertBadgeName": "Expert",
|
"founderBadgeName": "肇基",
|
||||||
"expertBadgeDescription": "Recognized for your expertise and valuable contributions",
|
"founderBadgeDescription": "屬 Solar Network 草創先驅",
|
||||||
"founderBadgeName": "Founder",
|
"betaTesterBadgeName": "預勘師",
|
||||||
"founderBadgeDescription": "One of the earliest members of Solar Network",
|
"betaTesterBadgeDescription": "嘗輔試 Solar Network 於初創之境",
|
||||||
"betaTesterBadgeName": "Beta Tester",
|
"moderatorBadgeName": "持衡官",
|
||||||
"betaTesterBadgeDescription": "Helped test and improve Solar Network during beta",
|
"moderatorBadgeDescription": "協理談筵規矩與秩序",
|
||||||
"moderatorBadgeName": "Moderator",
|
"developerBadgeName": "開物者",
|
||||||
"moderatorBadgeDescription": "Helping maintain and moderate the community",
|
"developerBadgeDescription": "獻力於 Solar Network 之功業",
|
||||||
"developerBadgeName": "Developer",
|
"translatorBadgeName": "譯介士",
|
||||||
"developerBadgeDescription": "Contributing to Solar Network's development",
|
"translatorBadgeDescription": "助譯 Solar Network 為諸邦之言",
|
||||||
"translatorBadgeName": "Translator",
|
"wallet": "荷包",
|
||||||
"translatorBadgeDescription": "Helping translate Solar Network into different languages",
|
"walletCurrencyPoints": "新太陽點",
|
||||||
"wallet": "Wallet",
|
|
||||||
"walletCurrencyPoints": "New Solar Points",
|
|
||||||
"walletCurrencyShortPoints": "NSP",
|
"walletCurrencyShortPoints": "NSP",
|
||||||
"walletCurrencyGolds": "The Solar Dollars",
|
"walletCurrencyGolds": "太陽幣",
|
||||||
"walletCurrencyShortGolds": "NSD",
|
"walletCurrencyShortGolds": "NSD",
|
||||||
"retry": "Retry",
|
"retry": "復行",
|
||||||
"creatorHubUnselectedHint": "Pick / create a publisher to get started.",
|
"creatorHubUnselectedHint": "擇/創發布者以啟程。",
|
||||||
"relationships": "Relationships",
|
"relationships": "交誼錄",
|
||||||
"addFriend": "Send a Friend Request",
|
"addFriend": "送締交書",
|
||||||
"addFriendShort": "Add as Friend",
|
"addFriendShort": "結為知交",
|
||||||
"addFriendHint": "Add a friend to your relationship list.",
|
"addFriendHint": "添友於交誼錄中。",
|
||||||
"pendingRequest": "Pending",
|
"pendingRequest": "待決",
|
||||||
"waitingRequest": "Waiting",
|
"waitingRequest": "候音",
|
||||||
"relationshipStatusFriend": "Friend",
|
"relationshipStatusFriend": "知交",
|
||||||
"relationshipStatusBlocked": "Blocked",
|
"relationshipStatusBlocked": "已阻",
|
||||||
"blockUser": "Block User",
|
"blockUser": "禁此戶",
|
||||||
"unblockUser": "Unblock User",
|
"unblockUser": "解禁",
|
||||||
"friendRequestAccepted": "Accepted friend request from {}",
|
"friendRequestAccepted": "已納 {} 締交書",
|
||||||
"friendRequestDeclined": "Declined friend request from {}",
|
"friendRequestDeclined": "已謝 {} 締交書",
|
||||||
"requestExpiredIn": "Expired in {}",
|
"requestExpiredIn": "效期 {}",
|
||||||
"friendSentRequest": "Sent Friend Requests",
|
"friendSentRequest": "已送締交書",
|
||||||
"friendSentRequestEmpty": "No sent friend requests",
|
"friendSentRequestEmpty": "未送締交書",
|
||||||
"friendSentRequestHint": {
|
"friendSentRequestHint": {
|
||||||
"one": "{} friend request sent",
|
"one": "已送{}締交書",
|
||||||
"other": "{} friend requests sent"
|
"other": "已送{}締交書"
|
||||||
},
|
},
|
||||||
"levelingProgress": "Leveling Progress",
|
"levelingProgress": "修為進境",
|
||||||
"levelingProgressExperience": "{} EXP",
|
"levelingProgressExperience": "{} 修為",
|
||||||
"levelingProgressLevel": "Level {}",
|
"levelingProgressLevel": "第 {} 重",
|
||||||
"fileUploadingProgress": "Uploading file #{}: {}%",
|
"fileUploadingProgress": "傳輸第 {} 宗卷:{}%",
|
||||||
"removeChatMember": "Remove Chat Room Member",
|
"removeChatMember": "逐出談席",
|
||||||
"removeChatMemberHint": "Are you sure to remove this member from the room?",
|
"removeChatMemberHint": "確欲逐此客出談筵乎?",
|
||||||
"removeRealmMember": "Remove Realm Member",
|
"removeRealmMember": "革出界域",
|
||||||
"removeRealmMemberHint": "Are you sure to remove this member from the realm?",
|
"removeRealmMemberHint": "確欲革此員出界域乎?",
|
||||||
"memberRole": "Member Role",
|
"memberRole": "成員權階",
|
||||||
"memberRoleHint": "Greater number has higher permission.",
|
"memberRoleHint": "數高則權重。",
|
||||||
"memberRoleEdit": "Edit role for @{}",
|
"memberRoleEdit": "修訂 @{} 之職",
|
||||||
"openLinkConfirm": "Leaving the Solar Network",
|
"openLinkConfirm": "行將離 Solar Network",
|
||||||
"openLinkConfirmDescription": "You're going to leave the Solar Network and open the link ({}) in your browser. It is not related to Solar Network. Beware of phishing and scams.",
|
"openLinkConfirmDescription": "君將辭 Solar Network 而啟 {} 之徑於瀏覽器,此徑與 Solar Network 無干,當心詐偽之害。",
|
||||||
"brokenLink": "Unable open link {}... It might be broken or missing uri parts...",
|
"brokenLink": "未能開啟 {} 之鏈……或為殘鏈佚址……",
|
||||||
"copyToClipboard": "Copy to clipboard",
|
"copyToClipboard": "抄入剪貼板",
|
||||||
"leaveChatRoom": "Leave Chat Room",
|
"leaveChatRoom": "辭離談席",
|
||||||
"leaveChatRoomHint": "Are you sure to leave this chat room?",
|
"leaveChatRoomHint": "君確要辭此談席否?",
|
||||||
"leaveRealm": "Leave Realm",
|
"leaveRealm": "辭離界域",
|
||||||
"leaveRealmHint": "Are you sure to leave this realm?",
|
"leaveRealmHint": "君確要辭此界域否?",
|
||||||
"walletNotFound": "Wallet not found",
|
"walletNotFound": "未見荷包",
|
||||||
"walletCreateHint": "You don't have a wallet yet. Create one to start using the Solar Network eWallet.",
|
"walletCreateHint": "君尚未備荷包,請創之以啟用 Solar Network 電子荷包。",
|
||||||
"walletCreate": "Create a Wallet",
|
"walletCreate": "始創一荷包",
|
||||||
"settingsServerUrl": "Server URL",
|
"settingsServerUrl": "伺服器網址",
|
||||||
"settingsApplied": "The settings has been applied.",
|
"settingsApplied": "諸設定已施畢。",
|
||||||
"notifications": "Notifications",
|
"notifications": "報聞",
|
||||||
"posts": "Posts",
|
"posts": "貼文",
|
||||||
"settingsBackgroundImage": "Background Image",
|
"settingsBackgroundImage": "底圖",
|
||||||
"settingsBackgroundImageClear": "Clear Background Image",
|
"settingsBackgroundImageClear": "清底圖",
|
||||||
"settingsBackgroundGenerateColor": "Generate color scheme from Bacground Image",
|
"settingsBackgroundGenerateColor": "取背景畫配色",
|
||||||
"messageNone": "No content to display",
|
"messageNone": "無文可示",
|
||||||
"unreadMessages": {
|
"unreadMessages": {
|
||||||
"one": "{} unread message",
|
"one": "{} 條未讀訊",
|
||||||
"other": "{} unread messages"
|
"other": "{} 條未讀訊"
|
||||||
},
|
},
|
||||||
"chatBreakNone": "None",
|
"chatBreakNone": "無",
|
||||||
"settingsRealmCompactView": "Compact Realm View",
|
"settingsRealmCompactView": "緊湊界域視圖",
|
||||||
"settingsMixedFeed": "Mixed Feed",
|
"settingsMixedFeed": "混流饋訊",
|
||||||
"settingsAutoTranslate": "Auto Translate",
|
"settingsAutoTranslate": "自轉譯",
|
||||||
"settingsHideBottomNav": "Hide Bottom Navigation",
|
"settingsHideBottomNav": "隱底航",
|
||||||
"settingsSoundEffects": "Sound Effects",
|
"settingsSoundEffects": "音效",
|
||||||
"settingsAprilFoolFeatures": "April Fool Features",
|
"settingsAprilFoolFeatures": "謔辰妙能",
|
||||||
"settingsEnterToSend": "Enter to Send",
|
"settingsEnterToSend": "叩回車鍵即送",
|
||||||
"settingsTransparentAppBar": "Transparent App Bar",
|
"settingsTransparentAppBar": "透光應用欄",
|
||||||
"settingsCustomFonts": "Custom Fonts",
|
"settingsCustomFonts": "自定字體",
|
||||||
"settingsCustomFontsHint": "Custom fonts will be used for all text in the app. Make sure it is installed on your device.",
|
"settingsCustomFontsHint": "此字體將通施於應用內文,請先確安於裝置。",
|
||||||
"settingsColorScheme": "Color Scheme",
|
"settingsColorScheme": "設置色彩方案",
|
||||||
"postTitle": "Title",
|
"postTitle": "標題",
|
||||||
"postDescription": "Description",
|
"postDescription": "状",
|
||||||
"call": "Call",
|
"call": "传信",
|
||||||
"done": "Done",
|
"done": "已成矣",
|
||||||
"loginResetPasswordSent": "Password reset link sent, please check your email inbox.",
|
"loginResetPasswordSent": "密码重置之链已送至,恳愿于汝之邮址查阅。",
|
||||||
"accountDeletion": "Delete Account",
|
"accountDeletion": "除去此账户",
|
||||||
"accountDeletionHint": "Are you sure to delete your account? If you confirmed, we will send an confirmation email to your primary email address, you can continue the deletion process by follow the insturctions in the email.",
|
"accountDeletionHint": "汝已存心除此账户?如已一意,信将至汝邮址,随信中之言续此行。",
|
||||||
"accountDeletionSent": "Account deletion confirmation email sent, please check your email inbox.",
|
"accountDeletionSent": "除此账户之信已送至,请于汝之邮址查阅。",
|
||||||
"accountSecurityTitle": "Security",
|
"accountSecurityTitle": "Security",
|
||||||
"accountDangerZoneTitle": "Danger Zone",
|
"accountDangerZoneTitle": "Danger Zone",
|
||||||
"accountPassword": "Password",
|
"accountPassword": "密碼",
|
||||||
"accountPasswordDescription": "Change your account password",
|
"accountPasswordDescription": "變更账户密碼",
|
||||||
"accountPasswordChange": "Change Password",
|
"accountPasswordChange": "變更密碼",
|
||||||
"accountPasswordChangeSent": "Password reset link sent, please check your email inbox.",
|
"accountPasswordChangeSent": "Password reset link sent, please check your email inbox.",
|
||||||
"accountPasswordChangeDescription": "We will send an email to your primary email address to reset your password.",
|
"accountPasswordChangeDescription": "We will send an email to your primary email address to reset your password.",
|
||||||
"accountAuthFactor": "Auth factors",
|
"accountAuthFactor": "Auth factors",
|
||||||
@@ -346,14 +344,14 @@
|
|||||||
"unauthorizedHint": "You're not signed in or session expired, please sign in again.",
|
"unauthorizedHint": "You're not signed in or session expired, please sign in again.",
|
||||||
"publisherBelongsTo": "Belongs to {}",
|
"publisherBelongsTo": "Belongs to {}",
|
||||||
"postContent": "Content",
|
"postContent": "Content",
|
||||||
"postSettings": "Settings",
|
"postSettings": "設定",
|
||||||
"postPublisherUnselected": "Publisher Unspecified",
|
"postPublisherUnselected": "发信者未定",
|
||||||
"postVisibilityPublic": "Public",
|
"postVisibilityPublic": "众以见",
|
||||||
"postVisibilityFriends": "Friends Only",
|
"postVisibilityFriends": "唯友以见",
|
||||||
"postVisibilityUnlisted": "Unlisted",
|
"postVisibilityUnlisted": "非指之人以见",
|
||||||
"postVisibilityPrivate": "Private",
|
"postVisibilityPrivate": "唯己可见",
|
||||||
"postTruncated": "Content truncated, tap to view full post",
|
"postTruncated": "Content truncated, tap to view full post",
|
||||||
"copyMessage": "Copy Message",
|
"copyMessage": "抄文",
|
||||||
"authFactor": "Authentication Factor",
|
"authFactor": "Authentication Factor",
|
||||||
"authFactorDelete": "Delete the Factor",
|
"authFactorDelete": "Delete the Factor",
|
||||||
"authFactorDeleteHint": "Are you sure you want to delete this authentication factor? This action cannot be undone.",
|
"authFactorDeleteHint": "Are you sure you want to delete this authentication factor? This action cannot be undone.",
|
||||||
@@ -366,8 +364,8 @@
|
|||||||
"authFactorSecretHint": "Create an secret for this factor.",
|
"authFactorSecretHint": "Create an secret for this factor.",
|
||||||
"authFactorQrCodeScan": "Scan this QR code with your authenticator app to set up TOTP authentication",
|
"authFactorQrCodeScan": "Scan this QR code with your authenticator app to set up TOTP authentication",
|
||||||
"authFactorNoQrCode": "No QR code available for this authentication factor",
|
"authFactorNoQrCode": "No QR code available for this authentication factor",
|
||||||
"cancel": "Cancel",
|
"cancel": "止",
|
||||||
"confirm": "Confirm",
|
"confirm": "认",
|
||||||
"authFactorAdditional": "One more step",
|
"authFactorAdditional": "One more step",
|
||||||
"authFactorHint": "Contact method hint",
|
"authFactorHint": "Contact method hint",
|
||||||
"authFactorHintHelper": "You need provide a part of your contact method and we will send the verification code to that contact method if it matched our records",
|
"authFactorHintHelper": "You need provide a part of your contact method and we will send the verification code to that contact method if it matched our records",
|
||||||
@@ -387,12 +385,12 @@
|
|||||||
"authDeviceSwipeEditHint": "Swipe left to edit label",
|
"authDeviceSwipeEditHint": "Swipe left to edit label",
|
||||||
"authDeviceSwipeLogoutHint": "Swipe right to logout device",
|
"authDeviceSwipeLogoutHint": "Swipe right to logout device",
|
||||||
"typingHint": {
|
"typingHint": {
|
||||||
"one": "{} is typing...",
|
"one": "{} 正在执笔……",
|
||||||
"other": "{} are typing..."
|
"other": "{} 正在执笔……"
|
||||||
},
|
},
|
||||||
"settingsAppearance": "Appearance",
|
"settingsAppearance": "Appearance",
|
||||||
"settingsServer": "Server",
|
"settingsServer": "伺服器",
|
||||||
"settingsBehavior": "Behavior",
|
"settingsBehavior": "为",
|
||||||
"settingsDesktop": "Desktop",
|
"settingsDesktop": "Desktop",
|
||||||
"settingsKeyboardShortcuts": "Keyboard Shortcuts",
|
"settingsKeyboardShortcuts": "Keyboard Shortcuts",
|
||||||
"settingsEnterToSendDesktopHint": "Press Enter to send messages, use Shift+Enter for new line.",
|
"settingsEnterToSendDesktopHint": "Press Enter to send messages, use Shift+Enter for new line.",
|
||||||
@@ -752,21 +750,6 @@
|
|||||||
"rename": "Rename",
|
"rename": "Rename",
|
||||||
"markAsSensitive": "Mark as Sensitive",
|
"markAsSensitive": "Mark as Sensitive",
|
||||||
"fileName": "File name",
|
"fileName": "File name",
|
||||||
"sensitiveCategories": {
|
|
||||||
"language": "Language",
|
|
||||||
"sexualContent": "Sexual Content",
|
|
||||||
"violence": "Violence",
|
|
||||||
"profanity": "Profanity",
|
|
||||||
"hateSpeech": "Hate Speech",
|
|
||||||
"racism": "Racism",
|
|
||||||
"adultContent": "Adult Content",
|
|
||||||
"drugAbuse": "Drug Abuse",
|
|
||||||
"alcoholAbuse": "Alcohol Abuse",
|
|
||||||
"gambling": "Gambling",
|
|
||||||
"selfHarm": "Self-harm",
|
|
||||||
"childAbuse": "Child Abuse",
|
|
||||||
"other": "Other"
|
|
||||||
},
|
|
||||||
"poll": "Poll",
|
"poll": "Poll",
|
||||||
"pollsRecent": "Recent Polls",
|
"pollsRecent": "Recent Polls",
|
||||||
"pollCreateNew": "Create New",
|
"pollCreateNew": "Create New",
|
||||||
@@ -1075,5 +1058,421 @@
|
|||||||
"deleteRecycledFiles": "Delete Recycled Files",
|
"deleteRecycledFiles": "Delete Recycled Files",
|
||||||
"recycledFilesDeleted": "Recycled files deleted successfully",
|
"recycledFilesDeleted": "Recycled files deleted successfully",
|
||||||
"failedToDeleteRecycledFiles": "Failed to delete recycled files",
|
"failedToDeleteRecycledFiles": "Failed to delete recycled files",
|
||||||
"upload": "Upload"
|
"upload": "Upload",
|
||||||
|
"deleteMessage": "Delete Message",
|
||||||
|
"deleteMessageConfirmation": "Are you sure you want to delete this message?",
|
||||||
|
"customReaction": "Custom Reaction",
|
||||||
|
"customReactions": "Custom Reactions",
|
||||||
|
"stickerPlaceholder": "Sticker Placeholder",
|
||||||
|
"reactionAttitude": "Reaction Attitude",
|
||||||
|
"addReaction": "Add Reaction",
|
||||||
|
"eventCalendar": "Event Calendar",
|
||||||
|
"eventCalendarEmpty": "No events on that day.",
|
||||||
|
"walletStats": "Wallet Statistics",
|
||||||
|
"totalTransactions": "Total Transactions",
|
||||||
|
"totalOrders": "Total Orders",
|
||||||
|
"totalIncome": "Total Income",
|
||||||
|
"totalOutgoing": "Total Outgoing",
|
||||||
|
"netBalance": "Net Balance",
|
||||||
|
"messageUpdateLinks": "Server generated links previews",
|
||||||
|
"messageUpdateEdited": "Edited a message",
|
||||||
|
"settingsCardBackgroundOpacity": "Card Background Opacity",
|
||||||
|
"settingsThemeMode": "Theme Mode",
|
||||||
|
"settingsThemeModeSystem": "System",
|
||||||
|
"settingsThemeModeLight": "Light",
|
||||||
|
"settingsThemeModeDark": "Dark",
|
||||||
|
"enterPin": "Enter your PIN code",
|
||||||
|
"chatReplyingTo": "Replying to {}",
|
||||||
|
"chatForwarding": "Forwarding message",
|
||||||
|
"chatEditing": "Editing message",
|
||||||
|
"chatNoContent": "No content",
|
||||||
|
"sensitiveCategories": {
|
||||||
|
"language": "Language",
|
||||||
|
"sexualContent": "Sexual Content",
|
||||||
|
"violence": "Violence",
|
||||||
|
"profanity": "Profanity",
|
||||||
|
"hateSpeech": "Hate Speech",
|
||||||
|
"racism": "Racism",
|
||||||
|
"adultContent": "Adult Content",
|
||||||
|
"drugAbuse": "Drug Abuse",
|
||||||
|
"alcoholAbuse": "Alcohol Abuse",
|
||||||
|
"gambling": "Gambling",
|
||||||
|
"selfHarm": "Self-harm",
|
||||||
|
"childAbuse": "Child Abuse",
|
||||||
|
"other": "Other"
|
||||||
|
},
|
||||||
|
"Searching...": "Searching...",
|
||||||
|
"searchError": "Search failed. Please try again.",
|
||||||
|
"tryDifferentKeywords": "Try different keywords or remove search filters",
|
||||||
|
"settingsWindowOpacity": "Window Opacity",
|
||||||
|
"messageContent": "Message Content",
|
||||||
|
"updateAvailable": "Update available",
|
||||||
|
"noChangelogProvided": "No changelog provided.",
|
||||||
|
"useSecondarySourceForDownload": "Use secondary source for download",
|
||||||
|
"installUpdate": "Install update",
|
||||||
|
"openReleasePage": "Open release page",
|
||||||
|
"postCompose": "Compose Post",
|
||||||
|
"postPublish": "Publish Post",
|
||||||
|
"restoreDraftTitle": "Restore Draft",
|
||||||
|
"restoreDraftMessage": "A draft was found. Do you want to restore it?",
|
||||||
|
"draft": "Draft",
|
||||||
|
"purchaseGift": "Purchase Gift",
|
||||||
|
"selectRecipient": "Select Recipient",
|
||||||
|
"changeRecipient": "Change Recipient",
|
||||||
|
"addMessage": "Add Message",
|
||||||
|
"skipRecipient": "Skip Recipient",
|
||||||
|
"giftSubscriptions": "Gift Subscriptions",
|
||||||
|
"purchaseAGift": "Purchase a Gift",
|
||||||
|
"redeemAGift": "Redeem a Gift",
|
||||||
|
"giftHistory": "Gift History",
|
||||||
|
"sentGifts": "Sent Gifts",
|
||||||
|
"receivedGifts": "Received Gifts",
|
||||||
|
"noSentGifts": "No sent gifts",
|
||||||
|
"noReceivedGifts": "No received gifts",
|
||||||
|
"stellarGift": "Stellar Gift",
|
||||||
|
"novaGift": "Nova Gift",
|
||||||
|
"supernovaGift": "Supernova Gift",
|
||||||
|
"sameAsMembership": "Same as membership",
|
||||||
|
"enterGiftCodeToRedeem": "Enter gift code to redeem",
|
||||||
|
"enterGiftCode": "Enter gift code",
|
||||||
|
"giftPurchased": "Gift Purchased!",
|
||||||
|
"shareCodeWithRecipient": "Share this code with the recipient to redeem the gift.",
|
||||||
|
"openGiftAnyoneCanRedeem": "This is an open gift that anyone can redeem.",
|
||||||
|
"ok": "OK",
|
||||||
|
"selectedRecipient": "Selected recipient",
|
||||||
|
"noRecipientSelected": "No recipient selected",
|
||||||
|
"thisWillBeAnOpenGift": "This will be an open gift",
|
||||||
|
"personalMessage": "Personal Message",
|
||||||
|
"addPersonalMessageForRecipient": "Add a personal message for the recipient",
|
||||||
|
"giftStatusCreated": "Created",
|
||||||
|
"giftStatusSent": "Sent",
|
||||||
|
"giftStatusRedeemed": "Redeemed",
|
||||||
|
"giftStatusCancelled": "Cancelled",
|
||||||
|
"giftStatusExpired": "Expired",
|
||||||
|
"giftStatusUnknown": "Unknown",
|
||||||
|
"giftCodeCopiedToClipboard": "Gift code copied to clipboard",
|
||||||
|
"codeLabel": "Code: ",
|
||||||
|
"subscriptionLabel": "Subscription: ",
|
||||||
|
"toLabel": "To: ",
|
||||||
|
"fromLabel": "From: ",
|
||||||
|
"messageLabel": "Message: ",
|
||||||
|
"giftRedeemed": "Gift Redeemed!",
|
||||||
|
"giftRedeemedSuccessfully": "You have successfully redeemed the gift. Your new subscription is now active.",
|
||||||
|
"cancelGift": "Cancel Gift",
|
||||||
|
"cancelGiftConfirm": "Are you sure you want to cancel this gift? This action cannot be undone.",
|
||||||
|
"giftCancelledSuccessfully": "Gift cancelled successfully",
|
||||||
|
"createFund": "Create Fund",
|
||||||
|
"fundAmount": "Fund Amount",
|
||||||
|
"enterAmount": "Enter Amount",
|
||||||
|
"selectCurrency": "Select Currency",
|
||||||
|
"splitType": "Split Type",
|
||||||
|
"evenSplit": "Even Split",
|
||||||
|
"equalAmountEach": "Equal amount for each recipient",
|
||||||
|
"randomSplit": "Random Split",
|
||||||
|
"randomAmountEach": "Random amount for each recipient",
|
||||||
|
"recipientCount": "Recipient Count",
|
||||||
|
"numberOfRecipients": "Number of Recipients",
|
||||||
|
"addPersonalMessageForRecipients": "Add a personal message for recipients",
|
||||||
|
"invalidAmount": "Invalid amount",
|
||||||
|
"invalidRecipientCount": "Invalid recipient count",
|
||||||
|
"fundOverview": "Fund Overview",
|
||||||
|
"totalFundsSent": "Total Funds Sent",
|
||||||
|
"totalFundsReceived": "Total Funds Received",
|
||||||
|
"transactions": "Transactions",
|
||||||
|
"myFunds": "My Funds",
|
||||||
|
"availableFunds": "Available Funds",
|
||||||
|
"fundStatusCreated": "Created",
|
||||||
|
"fundStatusPartial": "Partially Claimed",
|
||||||
|
"fundStatusCompleted": "Fully Claimed",
|
||||||
|
"fundStatusExpired": "Expired",
|
||||||
|
"fundStatusUnknown": "Unknown",
|
||||||
|
"recipients": "Recipients",
|
||||||
|
"fundClaimedSuccessfully": "Fund claimed successfully!",
|
||||||
|
"claim": "Claim",
|
||||||
|
"noFundsCreated": "No funds created yet",
|
||||||
|
"createYourFirstFund": "Create your first fund to get started",
|
||||||
|
"noAvailableFunds": "No available funds",
|
||||||
|
"fundsWillAppearHere": "Funds you can claim will appear here",
|
||||||
|
"fundCreatedSuccessfully": "Fund created successfully!",
|
||||||
|
"selectRecipients": "Select Recipients",
|
||||||
|
"noRecipientsSelected": "No recipients selected",
|
||||||
|
"selectRecipientsToSendFund": "Select recipients to send the fund to",
|
||||||
|
"addRecipient": "Add Recipient",
|
||||||
|
"addMoreRecipients": "Add More Recipients",
|
||||||
|
"transactionDetails": "Transaction Details",
|
||||||
|
"remarks": "Remarks",
|
||||||
|
"payer": "Payer",
|
||||||
|
"payee": "Payee",
|
||||||
|
"transactionType": "Transaction Type",
|
||||||
|
"transfer": "Transfer",
|
||||||
|
"payment": "Payment",
|
||||||
|
"systemWallet": "System Wallet",
|
||||||
|
"date": "Date",
|
||||||
|
"createTransfer": "Create Transfer",
|
||||||
|
"transferAmount": "Transfer Amount",
|
||||||
|
"selectPayee": "Select Payee",
|
||||||
|
"selectedPayee": "Selected Payee",
|
||||||
|
"noPayeeSelected": "No payee selected",
|
||||||
|
"selectPayeeToTransfer": "Select payee to transfer to",
|
||||||
|
"addRemark": "Add Remark",
|
||||||
|
"transferRemark": "Transfer Remark",
|
||||||
|
"addRemarkForTransfer": "Add remark for transfer",
|
||||||
|
"enterPinToConfirmTransfer": "Enter your 6-digit PIN to confirm transfer",
|
||||||
|
"transferCreatedSuccessfully": "Transfer created successfully!",
|
||||||
|
"postUpdate": "Update",
|
||||||
|
"fileMetadata": "File Metadata",
|
||||||
|
"resend": "Resend",
|
||||||
|
"fileInfoTitle": "File Information",
|
||||||
|
"download": "Download",
|
||||||
|
"info": "Info",
|
||||||
|
"noStickers": "No Stickers",
|
||||||
|
"noStickersInPack": "This pack does not contains stickers",
|
||||||
|
"noStickerPacks": "No Sticker Packs",
|
||||||
|
"refresh": "Refresh",
|
||||||
|
"spoiler": "Spoiler",
|
||||||
|
"activityHeatmap": "Activity Heatmap",
|
||||||
|
"custom": "Custom",
|
||||||
|
"usernameColor": "Username Color",
|
||||||
|
"colorType": "Color Type",
|
||||||
|
"plain": "Plain",
|
||||||
|
"gradient": "Gradient",
|
||||||
|
"colorValue": "Color Value",
|
||||||
|
"gradientDirection": "Gradient Direction",
|
||||||
|
"gradientDirectionToRight": "To Right",
|
||||||
|
"gradientDirectionToLeft": "To Left",
|
||||||
|
"gradientDirectionToBottom": "To Bottom",
|
||||||
|
"gradientDirectionToTop": "To Top",
|
||||||
|
"gradientDirectionToBottomRight": "To Bottom Right",
|
||||||
|
"gradientDirectionToBottomLeft": "To Bottom Left",
|
||||||
|
"gradientDirectionToTopRight": "To Top Right",
|
||||||
|
"gradientDirectionToTopLeft": "To Top Left",
|
||||||
|
"gradientColors": "Gradient Colors",
|
||||||
|
"color": "Color",
|
||||||
|
"addColor": "Add Color",
|
||||||
|
"availableWithYourPlan": "Available with your plan",
|
||||||
|
"upgradeRequired": "Upgrade required",
|
||||||
|
"settingsDisableAnimation": "Disable Animation",
|
||||||
|
"addTag": "Add Tag",
|
||||||
|
"accountConnectionProviderSpotify": "Spotify",
|
||||||
|
"accountConnectionProviderSteam": "Steam",
|
||||||
|
"timezoneNotFound": "Time zone not found",
|
||||||
|
"awardPoints": "Awarded {} points",
|
||||||
|
"postFeaturedOn": "Post featured on {}",
|
||||||
|
"messageSentAt": "Sent at {}",
|
||||||
|
"myTickets": "My Tickets",
|
||||||
|
"drawHistory": "Draw History",
|
||||||
|
"lottery": "Lottery",
|
||||||
|
"noLotteryTickets": "No lottery tickets yet",
|
||||||
|
"buyYourFirstTicket": "Buy your first lottery ticket to get started!",
|
||||||
|
"buyTicket": "Buy Ticket",
|
||||||
|
"ticketNumbers": "Numbers: {}, Special: {}",
|
||||||
|
"cost": "Cost",
|
||||||
|
"multiplier": "Multiplier",
|
||||||
|
"prizeWon": "Prize Won",
|
||||||
|
"pending": "Pending",
|
||||||
|
"drawn": "Drawn",
|
||||||
|
"won": "Won",
|
||||||
|
"lost": "Lost",
|
||||||
|
"noDrawHistory": "No draw history yet",
|
||||||
|
"buyLotteryTicket": "Buy Lottery Ticket",
|
||||||
|
"selectNumbers": "Select Numbers",
|
||||||
|
"select5UniqueNumbers": "Select 5 unique numbers",
|
||||||
|
"selectSpecialNumber": "Select Special Number",
|
||||||
|
"selectMultiplier": "Select Multiplier",
|
||||||
|
"baseCost": "Base Cost",
|
||||||
|
"totalCost": "Total Cost",
|
||||||
|
"prizeStructure": "Prize Structure",
|
||||||
|
"enterPinToConfirmPurchase": "Enter your PIN to confirm purchase",
|
||||||
|
"ticketPurchasedSuccessfully": "Ticket purchased successfully!",
|
||||||
|
"winningNumbers": "Winning Numbers",
|
||||||
|
"specialNumber": "Special Number",
|
||||||
|
"totalTickets": "Total Tickets",
|
||||||
|
"totalWinners": "Total Winners",
|
||||||
|
"prizePool": "Prize Pool",
|
||||||
|
"enterPinToConfirmPayment": "Enter your PIN code to confirm payment",
|
||||||
|
"purchase": "Purchase",
|
||||||
|
"multiplierLabel": "Multiplier",
|
||||||
|
"specialOnly": "Special Only",
|
||||||
|
"matches": "Matches",
|
||||||
|
"thoughtDefaultTopic": "Reflection",
|
||||||
|
"thoughtAiName": "SN-chan",
|
||||||
|
"thoughtUserName": "You",
|
||||||
|
"thoughtStreamingHint": "Sn-chan is thinking...",
|
||||||
|
"thoughtInputHint": "Ask sn-chan anything...",
|
||||||
|
"thoughtNewConversation": "Start New Conversation",
|
||||||
|
"thoughtParseError": "Failed to parse AI response",
|
||||||
|
"thoughtFunctionCall": "Use {}",
|
||||||
|
"aiThought": "AI Thought",
|
||||||
|
"aiThoughtTitle": "Let sn-chan think",
|
||||||
|
"postReferenceUnavailable": "Referenced post is unavailable",
|
||||||
|
"fabLocation": "FAB Location",
|
||||||
|
"activities": "Activities",
|
||||||
|
"presenceTypeGaming": "Playing",
|
||||||
|
"presenceTypeMusic": "Listening to Music",
|
||||||
|
"presenceTypeWorkout": "Working out",
|
||||||
|
"articleCompose": "Compose Article",
|
||||||
|
"backToHub": "Back to Hub",
|
||||||
|
"advancedFilters": "Advanced Filters",
|
||||||
|
"searchPosts": "Search Posts",
|
||||||
|
"sortBy": "Sort by",
|
||||||
|
"fromDate": "From Date",
|
||||||
|
"toDate": "To Date",
|
||||||
|
"popularity": "Popularity",
|
||||||
|
"descendingOrder": "Descending Order",
|
||||||
|
"selectDate": "Select Date",
|
||||||
|
"pinnedPosts": "Pinned Posts",
|
||||||
|
"customReactionHint": "Custom Reaction allow you to use user uploaded stickers as the symbol of the reaction for the post. Exclusive for Stellar Program members.",
|
||||||
|
"publicationSites": "Publication Sites",
|
||||||
|
"uploadTasks": "Upload Tasks",
|
||||||
|
"thoughtFunctionCallBegin": "Calling tool {}",
|
||||||
|
"thoughtFunctionCallFinish": "{} responded",
|
||||||
|
"thoughtUnpaidHint": "Thinking unavaiable due to unpaid orders",
|
||||||
|
"more": "More",
|
||||||
|
"collapse": "Collapse",
|
||||||
|
"pollConfirmDiscard": "Are you sure you want to leave? All the poll data you're editing will not be saved.",
|
||||||
|
"discard": "Discard",
|
||||||
|
"fund": "Fund",
|
||||||
|
"fundsRecent": "Recent Funds",
|
||||||
|
"fundCreateNew": "Create New",
|
||||||
|
"fundCreateNewHint": "Create a new fund for your message. Select recipients and amount.",
|
||||||
|
"amountOfSplits": "Amount of Splits",
|
||||||
|
"enterNumberOfSplits": "Enter Splits Amount",
|
||||||
|
"orCreateWith": "Or\ncreate with",
|
||||||
|
"unindexedFiles": "Unindexed files",
|
||||||
|
"folder": "Folder",
|
||||||
|
"clearCompleted": "Clear Completed",
|
||||||
|
"uploadSuccess": "Upload successful!",
|
||||||
|
"wouldYouLikeToViewFile": "Would you like to view the file?",
|
||||||
|
"contentCantEmpty": "Content cannot be empty",
|
||||||
|
"features": "Features",
|
||||||
|
"unnamed": "Unnamed",
|
||||||
|
"fundEnvelopeLoadFailed": "Failed to load fund envelope",
|
||||||
|
"fundEnvelope": "Fund Envelope",
|
||||||
|
"fundEnvelopeRemaining": "Remaining: {} {}",
|
||||||
|
"fundEnvelopeSplit": "Split: {}",
|
||||||
|
"fundEnvelopeSplitEvenly": "Evenly",
|
||||||
|
"fundEnvelopeSplitRandomly": "Randomly",
|
||||||
|
"fundEnvelopeClaimSuccess": "Fund claimed successfully!",
|
||||||
|
"fundEnvelopeStatusCreated": "Created",
|
||||||
|
"fundEnvelopeStatusPartial": "Partially Claimed",
|
||||||
|
"fundEnvelopeStatusCompleted": "Fully Claimed",
|
||||||
|
"fundEnvelopeStatusExpired": "Expired",
|
||||||
|
"fundEnvelopeStatusUnknown": "Unknown",
|
||||||
|
"fundEnvelopeRecipients": "Recipients ({}/{} claimed)",
|
||||||
|
"fundEnvelopeExpiredDaysAgo": {
|
||||||
|
"one": "Expired {} day ago",
|
||||||
|
"other": "Expired {} days ago"
|
||||||
|
},
|
||||||
|
"fundEnvelopeExpiresSoon": "Expires soon",
|
||||||
|
"fundEnvelopeExpiresInHours": {
|
||||||
|
"one": "Expires in {} hour",
|
||||||
|
"other": "Expires in {} hours"
|
||||||
|
},
|
||||||
|
"fundEnvelopeExpiresInDays": {
|
||||||
|
"one": "Expires in {} day",
|
||||||
|
"other": "Expires in {} days"
|
||||||
|
},
|
||||||
|
"fundEnvelopeRemainingWithSplits": "{} {} / {} splits",
|
||||||
|
"fundEnvelopeUnknownUser": "Unknown User",
|
||||||
|
"deleteSite": "Delete Site",
|
||||||
|
"deleteSiteConfirm": "Are you sure you want to delete this site?",
|
||||||
|
"siteDeletedSuccess": "Site deleted successfully",
|
||||||
|
"siteSlug": "Slug",
|
||||||
|
"siteSlugHint": "my-site",
|
||||||
|
"siteSlugRequired": "Please enter a slug",
|
||||||
|
"siteSlugInvalid": "Slug can only contain lowercase letters, numbers, and dashes",
|
||||||
|
"siteName": "Site Name",
|
||||||
|
"siteNameHint": "My Publication Site",
|
||||||
|
"siteNameRequired": "Please enter a site name",
|
||||||
|
"siteMode": "Mode",
|
||||||
|
"siteModeFullyManaged": "Fully Managed",
|
||||||
|
"siteModeSelfManaged": "Self-Managed",
|
||||||
|
"editPublicationSite": "Edit Publication Site",
|
||||||
|
"deletePublicationSite": "Delete Publication Site",
|
||||||
|
"publicationSiteSavedSuccess": "Publication site saved successfully",
|
||||||
|
"publicationSiteDeleteConfirm": "Are you sure you want to delete this publication site? This action cannot be undone.",
|
||||||
|
"publicationSiteDeletedSuccess": "Publication site deleted successfully",
|
||||||
|
"newPublicationSite": "New Publication Site",
|
||||||
|
"siteDetails": "Site Details",
|
||||||
|
"siteInformation": "Site Information",
|
||||||
|
"siteDomain": "Domain",
|
||||||
|
"siteCreated": "Created",
|
||||||
|
"siteUpdated": "Updated",
|
||||||
|
"failedToLoadSite": "Failed to load site",
|
||||||
|
"sitePages": "Pages",
|
||||||
|
"noPagesYet": "No pages yet",
|
||||||
|
"createFirstPage": "Create your first page to get started",
|
||||||
|
"failedToLoadPages": "Failed to load pages",
|
||||||
|
"fileManagement": "File Management",
|
||||||
|
"siteFiles": "Files",
|
||||||
|
"siteFolder": "Folder",
|
||||||
|
"siteRoot": "Root",
|
||||||
|
"noFilesUploadedYet": "No files uploaded yet",
|
||||||
|
"uploadFirstFile": "Upload your first file to get started",
|
||||||
|
"failedToLoadFiles": "Failed to load files",
|
||||||
|
"noFilesFoundInFolder": "No files found in the selected folder",
|
||||||
|
"fileActions": "File Actions",
|
||||||
|
"purgeFiles": "Purge Files",
|
||||||
|
"purgeFilesDescription": "Remove all uploaded files from the site",
|
||||||
|
"deploySite": "Deploy Site",
|
||||||
|
"deploySiteDescription": "Upload and deploy a new version from ZIP archive",
|
||||||
|
"confirmPurge": "Confirm Purge",
|
||||||
|
"purgeFilesConfirm": "This will permanently delete all files uploaded to this site. This action cannot be undone. Are you sure you want to continue?",
|
||||||
|
"purgeAllFiles": "Purge All Files",
|
||||||
|
"allFilesPurgedSuccess": "All files purged successfully",
|
||||||
|
"failedToPurgeFiles": "Failed to purge files: {}",
|
||||||
|
"siteDeployedSuccess": "Site deployed successfully",
|
||||||
|
"failedToDeploySite": "Failed to deploy site: {}",
|
||||||
|
"createPage": "Create Page",
|
||||||
|
"editPage": "Edit Page",
|
||||||
|
"pageType": "Page Type",
|
||||||
|
"htmlPage": "HTML Page",
|
||||||
|
"redirectPage": "Redirect Page",
|
||||||
|
"pageTypeRequired": "Please select a page type",
|
||||||
|
"pagePath": "Page Path",
|
||||||
|
"pagePathHint": "/about, /contact, etc.",
|
||||||
|
"pagePathRequired": "Please enter a page path",
|
||||||
|
"pagePathInvalid": "Page path can only contain letters, numbers, hyphens, underscores, and slashes",
|
||||||
|
"pagePathMustStartWithSlash": "Page path must start with /",
|
||||||
|
"pagePathNoConsecutiveSlashes": "Page path cannot have consecutive slashes",
|
||||||
|
"pageTitle": "Page Title",
|
||||||
|
"pageTitleHint": "About Us, Contact, etc.",
|
||||||
|
"pageTitleRequired": "Please enter a page title",
|
||||||
|
"pageContentHtml": "Page Content (HTML)",
|
||||||
|
"pageContentHint": "<h1>Hello World</h1><p>This is my page content...</p>",
|
||||||
|
"pageContentRequired": "Please enter HTML content for the page",
|
||||||
|
"redirectTarget": "Redirect Target",
|
||||||
|
"redirectTargetHint": "/new-page, https://example.com, etc.",
|
||||||
|
"redirectTargetRequired": "Please enter a redirect target",
|
||||||
|
"redirectTargetInvalid": "Target must be a relative path (/) or absolute URL (http/https)",
|
||||||
|
"deletePage": "Delete Page",
|
||||||
|
"deletePageConfirm": "Are you sure you want to delete this page?",
|
||||||
|
"savePage": "Save Page",
|
||||||
|
"pageCreatedSuccess": "Page created successfully",
|
||||||
|
"pageUpdatedSuccess": "Page updated successfully",
|
||||||
|
"pageDeletedSuccess": "Page deleted successfully",
|
||||||
|
"uploadFiles": "Upload Files",
|
||||||
|
"uploadPath": "Upload Path",
|
||||||
|
"uploadPathHint": "/ (root) or /assets/images/",
|
||||||
|
"uploadPathRequired": "Please enter an upload path",
|
||||||
|
"uploadPathMustStartWithSlash": "Path must start with /",
|
||||||
|
"uploadPathNoSpaces": "Path cannot contain spaces",
|
||||||
|
"uploadPathNoConsecutiveSlashes": "Path cannot have consecutive slashes",
|
||||||
|
"percentCompleted": "{}% completed",
|
||||||
|
"filesToUpload": "{} files to upload",
|
||||||
|
"fileSizeKb": "Size: {} KB",
|
||||||
|
"uploadingEllipsis": "Uploading...",
|
||||||
|
"uploadFilesCount": {
|
||||||
|
"one": "Upload {} File",
|
||||||
|
"other": "Upload {} Files"
|
||||||
|
},
|
||||||
|
"allUploadsCompleted": "All uploads completed",
|
||||||
|
"someUploadsFailed": "Some uploads failed",
|
||||||
|
"uploadingInProgress": "Uploading in progress",
|
||||||
|
"readyToUpload": "Ready to upload",
|
||||||
|
"allFilesUploadedSuccess": "All files uploaded successfully",
|
||||||
|
"lotteryLastNumberSpecial": "The last selected number will be your special number.",
|
||||||
|
"lotteryMultiplierRequired": "Please enter a multiplier",
|
||||||
|
"lotteryMultiplierRange": "Multiplier must be between 1 and 10",
|
||||||
|
"dropToShare": "Drop to share"
|
||||||
}
|
}
|
||||||
@@ -164,8 +164,6 @@
|
|||||||
"checkInResultLevel3": "好運",
|
"checkInResultLevel3": "好運",
|
||||||
"checkInResultLevel4": "最佳運氣",
|
"checkInResultLevel4": "最佳運氣",
|
||||||
"checkInActivityTitle": "{} 在 {} 簽到並獲得了 {}",
|
"checkInActivityTitle": "{} 在 {} 簽到並獲得了 {}",
|
||||||
"eventCalander": "活動日曆",
|
|
||||||
"eventCalanderEmpty": "該日無活動。",
|
|
||||||
"fortuneGraph": "時運趨勢",
|
"fortuneGraph": "時運趨勢",
|
||||||
"noFortuneData": "本月沒有時運數據。",
|
"noFortuneData": "本月沒有時運數據。",
|
||||||
"creatorHub": "創作者中心",
|
"creatorHub": "創作者中心",
|
||||||
@@ -752,21 +750,6 @@
|
|||||||
"rename": "重命名",
|
"rename": "重命名",
|
||||||
"markAsSensitive": "標記為敏感",
|
"markAsSensitive": "標記為敏感",
|
||||||
"fileName": "文件名",
|
"fileName": "文件名",
|
||||||
"sensitiveCategories": {
|
|
||||||
"language": "語言",
|
|
||||||
"sexualContent": "色情內容",
|
|
||||||
"violence": "暴力",
|
|
||||||
"profanity": "褻瀆",
|
|
||||||
"hateSpeech": "仇恨言論",
|
|
||||||
"racism": "種族主義",
|
|
||||||
"adultContent": "成人內容",
|
|
||||||
"drugAbuse": "藥物濫用",
|
|
||||||
"alcoholAbuse": "酗酒",
|
|
||||||
"gambling": "賭博",
|
|
||||||
"selfHarm": "自殘",
|
|
||||||
"childAbuse": "虐待兒童",
|
|
||||||
"other": "其他"
|
|
||||||
},
|
|
||||||
"poll": "投票",
|
"poll": "投票",
|
||||||
"pollsRecent": "最近投票",
|
"pollsRecent": "最近投票",
|
||||||
"pollCreateNew": "創建新投票",
|
"pollCreateNew": "創建新投票",
|
||||||
@@ -1076,6 +1059,420 @@
|
|||||||
"recycledFilesDeleted": "已回收檔案刪除成功",
|
"recycledFilesDeleted": "已回收檔案刪除成功",
|
||||||
"failedToDeleteRecycledFiles": "已回收檔案刪除失敗",
|
"failedToDeleteRecycledFiles": "已回收檔案刪除失敗",
|
||||||
"upload": "上傳",
|
"upload": "上傳",
|
||||||
|
"deleteMessage": "刪除訊息",
|
||||||
|
"deleteMessageConfirmation": "確定要刪除此郵件嗎?",
|
||||||
|
"customReaction": "自訂反應",
|
||||||
|
"customReactions": "自訂反應",
|
||||||
|
"stickerPlaceholder": "貼紙佔位符",
|
||||||
|
"reactionAttitude": "反應態度",
|
||||||
|
"addReaction": "添加反應",
|
||||||
|
"eventCalendar": "事件日曆",
|
||||||
|
"eventCalendarEmpty": "該日無活動。",
|
||||||
|
"walletStats": "錢包統計",
|
||||||
|
"totalTransactions": "交易總數",
|
||||||
|
"totalOrders": "訂單總數",
|
||||||
|
"totalIncome": "總收入",
|
||||||
|
"totalOutgoing": "總支出",
|
||||||
|
"netBalance": "淨餘額",
|
||||||
|
"messageUpdateLinks": "伺服器產生的連結預覽",
|
||||||
|
"messageUpdateEdited": "編輯一則訊息",
|
||||||
|
"settingsCardBackgroundOpacity": "卡片背景不透明度",
|
||||||
|
"settingsThemeMode": "主題模式",
|
||||||
|
"settingsThemeModeSystem": "跟隨系統",
|
||||||
|
"settingsThemeModeLight": "淺色",
|
||||||
|
"settingsThemeModeDark": "暗色",
|
||||||
|
"enterPin": "請輸入您的PIN碼",
|
||||||
|
"chatReplyingTo": "回復給 {}",
|
||||||
|
"chatForwarding": "正在轉傳訊息",
|
||||||
|
"chatEditing": "訊息編輯中",
|
||||||
|
"chatNoContent": "內容為空",
|
||||||
|
"sensitiveCategories": {
|
||||||
|
"language": "語言",
|
||||||
|
"sexualContent": "色情內容",
|
||||||
|
"violence": "暴力",
|
||||||
|
"profanity": "褻瀆",
|
||||||
|
"hateSpeech": "仇恨言論",
|
||||||
|
"racism": "種族主義",
|
||||||
|
"adultContent": "成人內容",
|
||||||
|
"drugAbuse": "藥物濫用",
|
||||||
|
"alcoholAbuse": "酗酒",
|
||||||
|
"gambling": "賭博",
|
||||||
|
"selfHarm": "自殘",
|
||||||
|
"childAbuse": "虐待兒童",
|
||||||
|
"other": "其他"
|
||||||
|
},
|
||||||
|
"Searching...": "檢索中……",
|
||||||
|
"searchError": "付款失敗,請重試。",
|
||||||
|
"tryDifferentKeywords": "嘗試不同的關鍵字或刪除搜尋過濾器",
|
||||||
|
"settingsWindowOpacity": "視窗不透明度",
|
||||||
|
"messageContent": "訊息內容",
|
||||||
|
"updateAvailable": "更新可用",
|
||||||
|
"noChangelogProvided": "無更新紀錄。",
|
||||||
|
"useSecondarySourceForDownload": "使用次要來源下載",
|
||||||
|
"installUpdate": "安装更新",
|
||||||
|
"openReleasePage": "開啟發行頁面",
|
||||||
"postCompose": "撰寫帖子",
|
"postCompose": "撰寫帖子",
|
||||||
"postPublish": "發佈帖子"
|
"postPublish": "發佈帖子",
|
||||||
|
"restoreDraftTitle": "還原草稿",
|
||||||
|
"restoreDraftMessage": "發現了一個草稿。你想要恢復它嗎?",
|
||||||
|
"draft": "草稿",
|
||||||
|
"purchaseGift": "充值有禮",
|
||||||
|
"selectRecipient": "選擇收件者",
|
||||||
|
"changeRecipient": "修改款件人",
|
||||||
|
"addMessage": "添加消息",
|
||||||
|
"skipRecipient": "跳過款件人",
|
||||||
|
"giftSubscriptions": "贈送訂閱",
|
||||||
|
"purchaseAGift": "充值有禮",
|
||||||
|
"redeemAGift": "兌換禮物",
|
||||||
|
"giftHistory": "禮物記錄",
|
||||||
|
"sentGifts": "發送禮物",
|
||||||
|
"receivedGifts": "接收禮物",
|
||||||
|
"noSentGifts": "沒有送過禮物",
|
||||||
|
"noReceivedGifts": "没有收到过礼物",
|
||||||
|
"stellarGift": "恆星禮物",
|
||||||
|
"novaGift": "新星禮物",
|
||||||
|
"supernovaGift": "Supernova Gift",
|
||||||
|
"sameAsMembership": "Same as membership",
|
||||||
|
"enterGiftCodeToRedeem": "Enter gift code to redeem",
|
||||||
|
"enterGiftCode": "Enter gift code",
|
||||||
|
"giftPurchased": "Gift Purchased!",
|
||||||
|
"shareCodeWithRecipient": "Share this code with the recipient to redeem the gift.",
|
||||||
|
"openGiftAnyoneCanRedeem": "This is an open gift that anyone can redeem.",
|
||||||
|
"ok": "OK",
|
||||||
|
"selectedRecipient": "Selected recipient",
|
||||||
|
"noRecipientSelected": "No recipient selected",
|
||||||
|
"thisWillBeAnOpenGift": "This will be an open gift",
|
||||||
|
"personalMessage": "Personal Message",
|
||||||
|
"addPersonalMessageForRecipient": "Add a personal message for the recipient",
|
||||||
|
"giftStatusCreated": "Created",
|
||||||
|
"giftStatusSent": "Sent",
|
||||||
|
"giftStatusRedeemed": "Redeemed",
|
||||||
|
"giftStatusCancelled": "Cancelled",
|
||||||
|
"giftStatusExpired": "Expired",
|
||||||
|
"giftStatusUnknown": "Unknown",
|
||||||
|
"giftCodeCopiedToClipboard": "Gift code copied to clipboard",
|
||||||
|
"codeLabel": "Code: ",
|
||||||
|
"subscriptionLabel": "Subscription: ",
|
||||||
|
"toLabel": "To: ",
|
||||||
|
"fromLabel": "From: ",
|
||||||
|
"messageLabel": "Message: ",
|
||||||
|
"giftRedeemed": "Gift Redeemed!",
|
||||||
|
"giftRedeemedSuccessfully": "You have successfully redeemed the gift. Your new subscription is now active.",
|
||||||
|
"cancelGift": "Cancel Gift",
|
||||||
|
"cancelGiftConfirm": "Are you sure you want to cancel this gift? This action cannot be undone.",
|
||||||
|
"giftCancelledSuccessfully": "Gift cancelled successfully",
|
||||||
|
"createFund": "Create Fund",
|
||||||
|
"fundAmount": "Fund Amount",
|
||||||
|
"enterAmount": "Enter Amount",
|
||||||
|
"selectCurrency": "Select Currency",
|
||||||
|
"splitType": "Split Type",
|
||||||
|
"evenSplit": "Even Split",
|
||||||
|
"equalAmountEach": "Equal amount for each recipient",
|
||||||
|
"randomSplit": "Random Split",
|
||||||
|
"randomAmountEach": "Random amount for each recipient",
|
||||||
|
"recipientCount": "Recipient Count",
|
||||||
|
"numberOfRecipients": "Number of Recipients",
|
||||||
|
"addPersonalMessageForRecipients": "Add a personal message for recipients",
|
||||||
|
"invalidAmount": "Invalid amount",
|
||||||
|
"invalidRecipientCount": "Invalid recipient count",
|
||||||
|
"fundOverview": "Fund Overview",
|
||||||
|
"totalFundsSent": "Total Funds Sent",
|
||||||
|
"totalFundsReceived": "Total Funds Received",
|
||||||
|
"transactions": "Transactions",
|
||||||
|
"myFunds": "My Funds",
|
||||||
|
"availableFunds": "Available Funds",
|
||||||
|
"fundStatusCreated": "Created",
|
||||||
|
"fundStatusPartial": "Partially Claimed",
|
||||||
|
"fundStatusCompleted": "Fully Claimed",
|
||||||
|
"fundStatusExpired": "Expired",
|
||||||
|
"fundStatusUnknown": "Unknown",
|
||||||
|
"recipients": "Recipients",
|
||||||
|
"fundClaimedSuccessfully": "Fund claimed successfully!",
|
||||||
|
"claim": "Claim",
|
||||||
|
"noFundsCreated": "No funds created yet",
|
||||||
|
"createYourFirstFund": "Create your first fund to get started",
|
||||||
|
"noAvailableFunds": "No available funds",
|
||||||
|
"fundsWillAppearHere": "Funds you can claim will appear here",
|
||||||
|
"fundCreatedSuccessfully": "Fund created successfully!",
|
||||||
|
"selectRecipients": "Select Recipients",
|
||||||
|
"noRecipientsSelected": "No recipients selected",
|
||||||
|
"selectRecipientsToSendFund": "Select recipients to send the fund to",
|
||||||
|
"addRecipient": "Add Recipient",
|
||||||
|
"addMoreRecipients": "Add More Recipients",
|
||||||
|
"transactionDetails": "Transaction Details",
|
||||||
|
"remarks": "Remarks",
|
||||||
|
"payer": "Payer",
|
||||||
|
"payee": "Payee",
|
||||||
|
"transactionType": "Transaction Type",
|
||||||
|
"transfer": "Transfer",
|
||||||
|
"payment": "Payment",
|
||||||
|
"systemWallet": "System Wallet",
|
||||||
|
"date": "Date",
|
||||||
|
"createTransfer": "Create Transfer",
|
||||||
|
"transferAmount": "Transfer Amount",
|
||||||
|
"selectPayee": "Select Payee",
|
||||||
|
"selectedPayee": "Selected Payee",
|
||||||
|
"noPayeeSelected": "No payee selected",
|
||||||
|
"selectPayeeToTransfer": "Select payee to transfer to",
|
||||||
|
"addRemark": "Add Remark",
|
||||||
|
"transferRemark": "Transfer Remark",
|
||||||
|
"addRemarkForTransfer": "Add remark for transfer",
|
||||||
|
"enterPinToConfirmTransfer": "Enter your 6-digit PIN to confirm transfer",
|
||||||
|
"transferCreatedSuccessfully": "Transfer created successfully!",
|
||||||
|
"postUpdate": "Update",
|
||||||
|
"fileMetadata": "File Metadata",
|
||||||
|
"resend": "Resend",
|
||||||
|
"fileInfoTitle": "File Information",
|
||||||
|
"download": "Download",
|
||||||
|
"info": "Info",
|
||||||
|
"noStickers": "No Stickers",
|
||||||
|
"noStickersInPack": "This pack does not contains stickers",
|
||||||
|
"noStickerPacks": "No Sticker Packs",
|
||||||
|
"refresh": "Refresh",
|
||||||
|
"spoiler": "Spoiler",
|
||||||
|
"activityHeatmap": "Activity Heatmap",
|
||||||
|
"custom": "Custom",
|
||||||
|
"usernameColor": "Username Color",
|
||||||
|
"colorType": "Color Type",
|
||||||
|
"plain": "Plain",
|
||||||
|
"gradient": "Gradient",
|
||||||
|
"colorValue": "Color Value",
|
||||||
|
"gradientDirection": "Gradient Direction",
|
||||||
|
"gradientDirectionToRight": "To Right",
|
||||||
|
"gradientDirectionToLeft": "To Left",
|
||||||
|
"gradientDirectionToBottom": "To Bottom",
|
||||||
|
"gradientDirectionToTop": "To Top",
|
||||||
|
"gradientDirectionToBottomRight": "To Bottom Right",
|
||||||
|
"gradientDirectionToBottomLeft": "To Bottom Left",
|
||||||
|
"gradientDirectionToTopRight": "To Top Right",
|
||||||
|
"gradientDirectionToTopLeft": "To Top Left",
|
||||||
|
"gradientColors": "Gradient Colors",
|
||||||
|
"color": "Color",
|
||||||
|
"addColor": "Add Color",
|
||||||
|
"availableWithYourPlan": "Available with your plan",
|
||||||
|
"upgradeRequired": "Upgrade required",
|
||||||
|
"settingsDisableAnimation": "Disable Animation",
|
||||||
|
"addTag": "Add Tag",
|
||||||
|
"accountConnectionProviderSpotify": "Spotify",
|
||||||
|
"accountConnectionProviderSteam": "Steam",
|
||||||
|
"timezoneNotFound": "Time zone not found",
|
||||||
|
"awardPoints": "Awarded {} points",
|
||||||
|
"postFeaturedOn": "Post featured on {}",
|
||||||
|
"messageSentAt": "Sent at {}",
|
||||||
|
"myTickets": "My Tickets",
|
||||||
|
"drawHistory": "Draw History",
|
||||||
|
"lottery": "Lottery",
|
||||||
|
"noLotteryTickets": "No lottery tickets yet",
|
||||||
|
"buyYourFirstTicket": "Buy your first lottery ticket to get started!",
|
||||||
|
"buyTicket": "Buy Ticket",
|
||||||
|
"ticketNumbers": "Numbers: {}, Special: {}",
|
||||||
|
"cost": "Cost",
|
||||||
|
"multiplier": "Multiplier",
|
||||||
|
"prizeWon": "Prize Won",
|
||||||
|
"pending": "Pending",
|
||||||
|
"drawn": "Drawn",
|
||||||
|
"won": "Won",
|
||||||
|
"lost": "Lost",
|
||||||
|
"noDrawHistory": "No draw history yet",
|
||||||
|
"buyLotteryTicket": "Buy Lottery Ticket",
|
||||||
|
"selectNumbers": "Select Numbers",
|
||||||
|
"select5UniqueNumbers": "Select 5 unique numbers",
|
||||||
|
"selectSpecialNumber": "Select Special Number",
|
||||||
|
"selectMultiplier": "Select Multiplier",
|
||||||
|
"baseCost": "Base Cost",
|
||||||
|
"totalCost": "Total Cost",
|
||||||
|
"prizeStructure": "Prize Structure",
|
||||||
|
"enterPinToConfirmPurchase": "Enter your PIN to confirm purchase",
|
||||||
|
"ticketPurchasedSuccessfully": "Ticket purchased successfully!",
|
||||||
|
"winningNumbers": "Winning Numbers",
|
||||||
|
"specialNumber": "Special Number",
|
||||||
|
"totalTickets": "Total Tickets",
|
||||||
|
"totalWinners": "Total Winners",
|
||||||
|
"prizePool": "Prize Pool",
|
||||||
|
"enterPinToConfirmPayment": "Enter your PIN code to confirm payment",
|
||||||
|
"purchase": "Purchase",
|
||||||
|
"multiplierLabel": "Multiplier",
|
||||||
|
"specialOnly": "Special Only",
|
||||||
|
"matches": "Matches",
|
||||||
|
"thoughtDefaultTopic": "Reflection",
|
||||||
|
"thoughtAiName": "SN-chan",
|
||||||
|
"thoughtUserName": "You",
|
||||||
|
"thoughtStreamingHint": "Sn-chan is thinking...",
|
||||||
|
"thoughtInputHint": "Ask sn-chan anything...",
|
||||||
|
"thoughtNewConversation": "Start New Conversation",
|
||||||
|
"thoughtParseError": "Failed to parse AI response",
|
||||||
|
"thoughtFunctionCall": "Use {}",
|
||||||
|
"aiThought": "AI Thought",
|
||||||
|
"aiThoughtTitle": "Let sn-chan think",
|
||||||
|
"postReferenceUnavailable": "Referenced post is unavailable",
|
||||||
|
"fabLocation": "FAB Location",
|
||||||
|
"activities": "Activities",
|
||||||
|
"presenceTypeGaming": "Playing",
|
||||||
|
"presenceTypeMusic": "Listening to Music",
|
||||||
|
"presenceTypeWorkout": "Working out",
|
||||||
|
"articleCompose": "Compose Article",
|
||||||
|
"backToHub": "Back to Hub",
|
||||||
|
"advancedFilters": "Advanced Filters",
|
||||||
|
"searchPosts": "Search Posts",
|
||||||
|
"sortBy": "Sort by",
|
||||||
|
"fromDate": "From Date",
|
||||||
|
"toDate": "To Date",
|
||||||
|
"popularity": "Popularity",
|
||||||
|
"descendingOrder": "Descending Order",
|
||||||
|
"selectDate": "Select Date",
|
||||||
|
"pinnedPosts": "Pinned Posts",
|
||||||
|
"customReactionHint": "Custom Reaction allow you to use user uploaded stickers as the symbol of the reaction for the post. Exclusive for Stellar Program members.",
|
||||||
|
"publicationSites": "Publication Sites",
|
||||||
|
"uploadTasks": "Upload Tasks",
|
||||||
|
"thoughtFunctionCallBegin": "Calling tool {}",
|
||||||
|
"thoughtFunctionCallFinish": "{} responded",
|
||||||
|
"thoughtUnpaidHint": "Thinking unavaiable due to unpaid orders",
|
||||||
|
"more": "More",
|
||||||
|
"collapse": "Collapse",
|
||||||
|
"pollConfirmDiscard": "Are you sure you want to leave? All the poll data you're editing will not be saved.",
|
||||||
|
"discard": "Discard",
|
||||||
|
"fund": "Fund",
|
||||||
|
"fundsRecent": "Recent Funds",
|
||||||
|
"fundCreateNew": "Create New",
|
||||||
|
"fundCreateNewHint": "Create a new fund for your message. Select recipients and amount.",
|
||||||
|
"amountOfSplits": "Amount of Splits",
|
||||||
|
"enterNumberOfSplits": "Enter Splits Amount",
|
||||||
|
"orCreateWith": "Or\ncreate with",
|
||||||
|
"unindexedFiles": "Unindexed files",
|
||||||
|
"folder": "Folder",
|
||||||
|
"clearCompleted": "Clear Completed",
|
||||||
|
"uploadSuccess": "Upload successful!",
|
||||||
|
"wouldYouLikeToViewFile": "Would you like to view the file?",
|
||||||
|
"contentCantEmpty": "Content cannot be empty",
|
||||||
|
"features": "Features",
|
||||||
|
"unnamed": "Unnamed",
|
||||||
|
"fundEnvelopeLoadFailed": "Failed to load fund envelope",
|
||||||
|
"fundEnvelope": "Fund Envelope",
|
||||||
|
"fundEnvelopeRemaining": "Remaining: {} {}",
|
||||||
|
"fundEnvelopeSplit": "Split: {}",
|
||||||
|
"fundEnvelopeSplitEvenly": "Evenly",
|
||||||
|
"fundEnvelopeSplitRandomly": "Randomly",
|
||||||
|
"fundEnvelopeClaimSuccess": "Fund claimed successfully!",
|
||||||
|
"fundEnvelopeStatusCreated": "Created",
|
||||||
|
"fundEnvelopeStatusPartial": "Partially Claimed",
|
||||||
|
"fundEnvelopeStatusCompleted": "Fully Claimed",
|
||||||
|
"fundEnvelopeStatusExpired": "Expired",
|
||||||
|
"fundEnvelopeStatusUnknown": "Unknown",
|
||||||
|
"fundEnvelopeRecipients": "Recipients ({}/{} claimed)",
|
||||||
|
"fundEnvelopeExpiredDaysAgo": {
|
||||||
|
"one": "Expired {} day ago",
|
||||||
|
"other": "Expired {} days ago"
|
||||||
|
},
|
||||||
|
"fundEnvelopeExpiresSoon": "Expires soon",
|
||||||
|
"fundEnvelopeExpiresInHours": {
|
||||||
|
"one": "Expires in {} hour",
|
||||||
|
"other": "Expires in {} hours"
|
||||||
|
},
|
||||||
|
"fundEnvelopeExpiresInDays": {
|
||||||
|
"one": "Expires in {} day",
|
||||||
|
"other": "Expires in {} days"
|
||||||
|
},
|
||||||
|
"fundEnvelopeRemainingWithSplits": "{} {} / {} splits",
|
||||||
|
"fundEnvelopeUnknownUser": "Unknown User",
|
||||||
|
"deleteSite": "Delete Site",
|
||||||
|
"deleteSiteConfirm": "Are you sure you want to delete this site?",
|
||||||
|
"siteDeletedSuccess": "Site deleted successfully",
|
||||||
|
"siteSlug": "Slug",
|
||||||
|
"siteSlugHint": "my-site",
|
||||||
|
"siteSlugRequired": "Please enter a slug",
|
||||||
|
"siteSlugInvalid": "Slug can only contain lowercase letters, numbers, and dashes",
|
||||||
|
"siteName": "Site Name",
|
||||||
|
"siteNameHint": "My Publication Site",
|
||||||
|
"siteNameRequired": "Please enter a site name",
|
||||||
|
"siteMode": "Mode",
|
||||||
|
"siteModeFullyManaged": "Fully Managed",
|
||||||
|
"siteModeSelfManaged": "Self-Managed",
|
||||||
|
"editPublicationSite": "Edit Publication Site",
|
||||||
|
"deletePublicationSite": "Delete Publication Site",
|
||||||
|
"publicationSiteSavedSuccess": "Publication site saved successfully",
|
||||||
|
"publicationSiteDeleteConfirm": "Are you sure you want to delete this publication site? This action cannot be undone.",
|
||||||
|
"publicationSiteDeletedSuccess": "Publication site deleted successfully",
|
||||||
|
"newPublicationSite": "New Publication Site",
|
||||||
|
"siteDetails": "Site Details",
|
||||||
|
"siteInformation": "Site Information",
|
||||||
|
"siteDomain": "Domain",
|
||||||
|
"siteCreated": "Created",
|
||||||
|
"siteUpdated": "Updated",
|
||||||
|
"failedToLoadSite": "Failed to load site",
|
||||||
|
"sitePages": "Pages",
|
||||||
|
"noPagesYet": "No pages yet",
|
||||||
|
"createFirstPage": "Create your first page to get started",
|
||||||
|
"failedToLoadPages": "Failed to load pages",
|
||||||
|
"fileManagement": "File Management",
|
||||||
|
"siteFiles": "Files",
|
||||||
|
"siteFolder": "Folder",
|
||||||
|
"siteRoot": "Root",
|
||||||
|
"noFilesUploadedYet": "No files uploaded yet",
|
||||||
|
"uploadFirstFile": "Upload your first file to get started",
|
||||||
|
"failedToLoadFiles": "Failed to load files",
|
||||||
|
"noFilesFoundInFolder": "No files found in the selected folder",
|
||||||
|
"fileActions": "File Actions",
|
||||||
|
"purgeFiles": "Purge Files",
|
||||||
|
"purgeFilesDescription": "Remove all uploaded files from the site",
|
||||||
|
"deploySite": "Deploy Site",
|
||||||
|
"deploySiteDescription": "Upload and deploy a new version from ZIP archive",
|
||||||
|
"confirmPurge": "Confirm Purge",
|
||||||
|
"purgeFilesConfirm": "This will permanently delete all files uploaded to this site. This action cannot be undone. Are you sure you want to continue?",
|
||||||
|
"purgeAllFiles": "Purge All Files",
|
||||||
|
"allFilesPurgedSuccess": "All files purged successfully",
|
||||||
|
"failedToPurgeFiles": "Failed to purge files: {}",
|
||||||
|
"siteDeployedSuccess": "Site deployed successfully",
|
||||||
|
"failedToDeploySite": "Failed to deploy site: {}",
|
||||||
|
"createPage": "Create Page",
|
||||||
|
"editPage": "Edit Page",
|
||||||
|
"pageType": "Page Type",
|
||||||
|
"htmlPage": "HTML Page",
|
||||||
|
"redirectPage": "Redirect Page",
|
||||||
|
"pageTypeRequired": "Please select a page type",
|
||||||
|
"pagePath": "Page Path",
|
||||||
|
"pagePathHint": "/about, /contact, etc.",
|
||||||
|
"pagePathRequired": "Please enter a page path",
|
||||||
|
"pagePathInvalid": "Page path can only contain letters, numbers, hyphens, underscores, and slashes",
|
||||||
|
"pagePathMustStartWithSlash": "Page path must start with /",
|
||||||
|
"pagePathNoConsecutiveSlashes": "Page path cannot have consecutive slashes",
|
||||||
|
"pageTitle": "Page Title",
|
||||||
|
"pageTitleHint": "About Us, Contact, etc.",
|
||||||
|
"pageTitleRequired": "Please enter a page title",
|
||||||
|
"pageContentHtml": "Page Content (HTML)",
|
||||||
|
"pageContentHint": "<h1>Hello World</h1><p>This is my page content...</p>",
|
||||||
|
"pageContentRequired": "Please enter HTML content for the page",
|
||||||
|
"redirectTarget": "Redirect Target",
|
||||||
|
"redirectTargetHint": "/new-page, https://example.com, etc.",
|
||||||
|
"redirectTargetRequired": "Please enter a redirect target",
|
||||||
|
"redirectTargetInvalid": "Target must be a relative path (/) or absolute URL (http/https)",
|
||||||
|
"deletePage": "Delete Page",
|
||||||
|
"deletePageConfirm": "Are you sure you want to delete this page?",
|
||||||
|
"savePage": "Save Page",
|
||||||
|
"pageCreatedSuccess": "Page created successfully",
|
||||||
|
"pageUpdatedSuccess": "Page updated successfully",
|
||||||
|
"pageDeletedSuccess": "Page deleted successfully",
|
||||||
|
"uploadFiles": "Upload Files",
|
||||||
|
"uploadPath": "Upload Path",
|
||||||
|
"uploadPathHint": "/ (root) or /assets/images/",
|
||||||
|
"uploadPathRequired": "Please enter an upload path",
|
||||||
|
"uploadPathMustStartWithSlash": "Path must start with /",
|
||||||
|
"uploadPathNoSpaces": "Path cannot contain spaces",
|
||||||
|
"uploadPathNoConsecutiveSlashes": "Path cannot have consecutive slashes",
|
||||||
|
"percentCompleted": "{}% completed",
|
||||||
|
"filesToUpload": "{} files to upload",
|
||||||
|
"fileSizeKb": "Size: {} KB",
|
||||||
|
"uploadingEllipsis": "Uploading...",
|
||||||
|
"uploadFilesCount": {
|
||||||
|
"one": "Upload {} File",
|
||||||
|
"other": "Upload {} Files"
|
||||||
|
},
|
||||||
|
"allUploadsCompleted": "All uploads completed",
|
||||||
|
"someUploadsFailed": "Some uploads failed",
|
||||||
|
"uploadingInProgress": "Uploading in progress",
|
||||||
|
"readyToUpload": "Ready to upload",
|
||||||
|
"allFilesUploadedSuccess": "All files uploaded successfully",
|
||||||
|
"lotteryLastNumberSpecial": "The last selected number will be your special number.",
|
||||||
|
"lotteryMultiplierRequired": "Please enter a multiplier",
|
||||||
|
"lotteryMultiplierRange": "Multiplier must be between 1 and 10",
|
||||||
|
"dropToShare": "Drop to share"
|
||||||
}
|
}
|
||||||
1
drift_schemas/app_database/drift_schema_v7.json
Normal file
1
drift_schemas/app_database/drift_schema_v7.json
Normal file
File diff suppressed because one or more lines are too long
@@ -57,7 +57,7 @@ PODS:
|
|||||||
- firebase_core (4.2.1):
|
- firebase_core (4.2.1):
|
||||||
- Firebase/CoreOnly (= 12.4.0)
|
- Firebase/CoreOnly (= 12.4.0)
|
||||||
- Flutter
|
- Flutter
|
||||||
- firebase_crashlytics (5.0.4):
|
- firebase_crashlytics (5.0.5):
|
||||||
- Firebase/Crashlytics (= 12.4.0)
|
- Firebase/Crashlytics (= 12.4.0)
|
||||||
- firebase_core
|
- firebase_core
|
||||||
- Flutter
|
- Flutter
|
||||||
@@ -140,15 +140,13 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- flutter_native_splash (2.4.3):
|
- flutter_native_splash (2.4.3):
|
||||||
- Flutter
|
- Flutter
|
||||||
- flutter_platform_alert (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- flutter_secure_storage (6.0.0):
|
- flutter_secure_storage (6.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- flutter_timezone (0.0.1):
|
- flutter_timezone (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- flutter_udid (0.0.1):
|
- flutter_udid (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- SAMKeychain
|
- KeychainAccess
|
||||||
- flutter_webrtc (1.2.0):
|
- flutter_webrtc (1.2.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- WebRTC-SDK (= 137.7151.04)
|
- WebRTC-SDK (= 137.7151.04)
|
||||||
@@ -216,7 +214,8 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- irondash_engine_context (0.0.1):
|
- irondash_engine_context (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- Kingfisher (8.6.1)
|
- KeychainAccess (4.2.2)
|
||||||
|
- Kingfisher (8.6.2)
|
||||||
- KingfisherWebP (1.7.2):
|
- KingfisherWebP (1.7.2):
|
||||||
- Kingfisher (~> 8.0)
|
- Kingfisher (~> 8.0)
|
||||||
- libwebp (>= 1.1.0)
|
- libwebp (>= 1.1.0)
|
||||||
@@ -250,14 +249,13 @@ PODS:
|
|||||||
- nanopb/encode (3.30910.0)
|
- nanopb/encode (3.30910.0)
|
||||||
- native_exif (0.0.1):
|
- native_exif (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- objective_c (0.0.1):
|
||||||
|
- Flutter
|
||||||
- OrderedSet (6.0.3)
|
- 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):
|
||||||
- Flutter
|
- Flutter
|
||||||
- path_provider_foundation (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- FlutterMacOS
|
|
||||||
- pointer_interceptor_ios (0.0.1):
|
- pointer_interceptor_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- PromisesObjC (2.4.0)
|
- PromisesObjC (2.4.0)
|
||||||
@@ -269,7 +267,6 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- record_ios (1.1.0):
|
- record_ios (1.1.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- SAMKeychain (1.5.3)
|
|
||||||
- SDWebImage (5.21.3):
|
- SDWebImage (5.21.3):
|
||||||
- SDWebImage/Core (= 5.21.3)
|
- SDWebImage/Core (= 5.21.3)
|
||||||
- SDWebImage/Core (5.21.3)
|
- SDWebImage/Core (5.21.3)
|
||||||
@@ -315,8 +312,6 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- url_launcher_ios (0.0.1):
|
- url_launcher_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- volume_controller (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- wakelock_plus (0.0.1):
|
- wakelock_plus (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- WebRTC-SDK (137.7151.04)
|
- WebRTC-SDK (137.7151.04)
|
||||||
@@ -338,7 +333,6 @@ DEPENDENCIES:
|
|||||||
- flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
|
- flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
|
||||||
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
||||||
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
||||||
- flutter_platform_alert (from `.symlinks/plugins/flutter_platform_alert/ios`)
|
|
||||||
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
||||||
- flutter_timezone (from `.symlinks/plugins/flutter_timezone/ios`)
|
- flutter_timezone (from `.symlinks/plugins/flutter_timezone/ios`)
|
||||||
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
|
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
|
||||||
@@ -353,9 +347,9 @@ DEPENDENCIES:
|
|||||||
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
|
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
|
||||||
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
|
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
|
||||||
- native_exif (from `.symlinks/plugins/native_exif/ios`)
|
- native_exif (from `.symlinks/plugins/native_exif/ios`)
|
||||||
|
- objective_c (from `.symlinks/plugins/objective_c/ios`)
|
||||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||||
- pasteboard (from `.symlinks/plugins/pasteboard/ios`)
|
- pasteboard (from `.symlinks/plugins/pasteboard/ios`)
|
||||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
|
||||||
- pointer_interceptor_ios (from `.symlinks/plugins/pointer_interceptor_ios/ios`)
|
- pointer_interceptor_ios (from `.symlinks/plugins/pointer_interceptor_ios/ios`)
|
||||||
- protocol_handler_ios (from `.symlinks/plugins/protocol_handler_ios/ios`)
|
- protocol_handler_ios (from `.symlinks/plugins/protocol_handler_ios/ios`)
|
||||||
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
|
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
|
||||||
@@ -368,7 +362,6 @@ DEPENDENCIES:
|
|||||||
- super_native_extensions (from `.symlinks/plugins/super_native_extensions/ios`)
|
- super_native_extensions (from `.symlinks/plugins/super_native_extensions/ios`)
|
||||||
- syncfusion_flutter_pdfviewer (from `.symlinks/plugins/syncfusion_flutter_pdfviewer/ios`)
|
- syncfusion_flutter_pdfviewer (from `.symlinks/plugins/syncfusion_flutter_pdfviewer/ios`)
|
||||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||||
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
|
|
||||||
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
||||||
|
|
||||||
SPEC REPOS:
|
SPEC REPOS:
|
||||||
@@ -390,6 +383,7 @@ SPEC REPOS:
|
|||||||
- GoogleAppMeasurement
|
- GoogleAppMeasurement
|
||||||
- GoogleDataTransport
|
- GoogleDataTransport
|
||||||
- GoogleUtilities
|
- GoogleUtilities
|
||||||
|
- KeychainAccess
|
||||||
- Kingfisher
|
- Kingfisher
|
||||||
- KingfisherWebP
|
- KingfisherWebP
|
||||||
- libwebp
|
- libwebp
|
||||||
@@ -397,7 +391,6 @@ SPEC REPOS:
|
|||||||
- OrderedSet
|
- OrderedSet
|
||||||
- PromisesObjC
|
- PromisesObjC
|
||||||
- PromisesSwift
|
- PromisesSwift
|
||||||
- SAMKeychain
|
|
||||||
- SDWebImage
|
- SDWebImage
|
||||||
- sqlite3
|
- sqlite3
|
||||||
- SwiftyGif
|
- SwiftyGif
|
||||||
@@ -434,8 +427,6 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/flutter_local_notifications/ios"
|
:path: ".symlinks/plugins/flutter_local_notifications/ios"
|
||||||
flutter_native_splash:
|
flutter_native_splash:
|
||||||
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
||||||
flutter_platform_alert:
|
|
||||||
:path: ".symlinks/plugins/flutter_platform_alert/ios"
|
|
||||||
flutter_secure_storage:
|
flutter_secure_storage:
|
||||||
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
||||||
flutter_timezone:
|
flutter_timezone:
|
||||||
@@ -460,12 +451,12 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/media_kit_video/ios"
|
:path: ".symlinks/plugins/media_kit_video/ios"
|
||||||
native_exif:
|
native_exif:
|
||||||
:path: ".symlinks/plugins/native_exif/ios"
|
:path: ".symlinks/plugins/native_exif/ios"
|
||||||
|
objective_c:
|
||||||
|
:path: ".symlinks/plugins/objective_c/ios"
|
||||||
package_info_plus:
|
package_info_plus:
|
||||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||||
pasteboard:
|
pasteboard:
|
||||||
:path: ".symlinks/plugins/pasteboard/ios"
|
:path: ".symlinks/plugins/pasteboard/ios"
|
||||||
path_provider_foundation:
|
|
||||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
|
||||||
pointer_interceptor_ios:
|
pointer_interceptor_ios:
|
||||||
:path: ".symlinks/plugins/pointer_interceptor_ios/ios"
|
:path: ".symlinks/plugins/pointer_interceptor_ios/ios"
|
||||||
protocol_handler_ios:
|
protocol_handler_ios:
|
||||||
@@ -490,8 +481,6 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/syncfusion_flutter_pdfviewer/ios"
|
:path: ".symlinks/plugins/syncfusion_flutter_pdfviewer/ios"
|
||||||
url_launcher_ios:
|
url_launcher_ios:
|
||||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||||
volume_controller:
|
|
||||||
:path: ".symlinks/plugins/volume_controller/ios"
|
|
||||||
wakelock_plus:
|
wakelock_plus:
|
||||||
:path: ".symlinks/plugins/wakelock_plus/ios"
|
:path: ".symlinks/plugins/wakelock_plus/ios"
|
||||||
|
|
||||||
@@ -507,7 +496,7 @@ SPEC CHECKSUMS:
|
|||||||
Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e
|
Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e
|
||||||
firebase_analytics: 67fbdd9f3c04e55048024f3da21cfc36f05e56cf
|
firebase_analytics: 67fbdd9f3c04e55048024f3da21cfc36f05e56cf
|
||||||
firebase_core: f1aafb21c14f497e5498f7ffc4dc63cbb52b2594
|
firebase_core: f1aafb21c14f497e5498f7ffc4dc63cbb52b2594
|
||||||
firebase_crashlytics: 83c7467d7534975a4d779af43bd226d0a4616464
|
firebase_crashlytics: c039028126cb45e32f4c217aa392408b0963d081
|
||||||
firebase_messaging: c17a29984eafce4b2997fe078bb0a9e0b06f5dde
|
firebase_messaging: c17a29984eafce4b2997fe078bb0a9e0b06f5dde
|
||||||
FirebaseAnalytics: 0fc2b20091f0ddd21bf73397cf8f0eb5346dc24f
|
FirebaseAnalytics: 0fc2b20091f0ddd21bf73397cf8f0eb5346dc24f
|
||||||
FirebaseCore: bb595f3114953664e3c1dc032f008a244147cfd3
|
FirebaseCore: bb595f3114953664e3c1dc032f008a244147cfd3
|
||||||
@@ -524,10 +513,9 @@ SPEC CHECKSUMS:
|
|||||||
flutter_keyboard_visibility: 4625131e43015dbbe759d9b20daaf77e0e3f6619
|
flutter_keyboard_visibility: 4625131e43015dbbe759d9b20daaf77e0e3f6619
|
||||||
flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb
|
flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb
|
||||||
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
|
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
|
||||||
flutter_platform_alert: bf3b5fcd4ac14bd637e20527e9c471633071afd3
|
|
||||||
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
|
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
|
||||||
flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544
|
flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544
|
||||||
flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9
|
flutter_udid: 92a5d31fe0526b7b6002a2318df702e12e7eb300
|
||||||
flutter_webrtc: c3e21fc0dcd9d8eb246ae4d5256fcbeb2f5ecd22
|
flutter_webrtc: c3e21fc0dcd9d8eb246ae4d5256fcbeb2f5ecd22
|
||||||
gal: baecd024ebfd13c441269ca7404792a7152fde89
|
gal: baecd024ebfd13c441269ca7404792a7152fde89
|
||||||
GoogleAdsOnDeviceConversion: e03a386840803ea7eef3fd22a061930142c039c1
|
GoogleAdsOnDeviceConversion: e03a386840803ea7eef3fd22a061930142c039c1
|
||||||
@@ -536,7 +524,8 @@ SPEC CHECKSUMS:
|
|||||||
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
||||||
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
|
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
|
||||||
irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
|
irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
|
||||||
Kingfisher: 7ac7a7288653787a54206b11a3c74f49ab650f1f
|
KeychainAccess: c0c4f7f38f6fc7bbe58f5702e25f7bd2f65abf51
|
||||||
|
Kingfisher: 23d18f54677d973b713e54ce6a8f5eef6e7056ba
|
||||||
KingfisherWebP: 38b9721821947f547afb78f933f75f4f9e0ae402
|
KingfisherWebP: 38b9721821947f547afb78f933f75f4f9e0ae402
|
||||||
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
|
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
|
||||||
livekit_client: 86c8af579274e4b7a215185a8080db2d4e176f40
|
livekit_client: 86c8af579274e4b7a215185a8080db2d4e176f40
|
||||||
@@ -545,17 +534,16 @@ SPEC CHECKSUMS:
|
|||||||
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
|
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
|
||||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||||
native_exif: 0eb73d3d5b3ca892719228df8d2d1b13d1ae396c
|
native_exif: 0eb73d3d5b3ca892719228df8d2d1b13d1ae396c
|
||||||
|
objective_c: 89e720c30d716b036faf9c9684022048eee1eee2
|
||||||
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
||||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||||
pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c
|
pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c
|
||||||
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
|
|
||||||
pointer_interceptor_ios: da06a662d5bfd329602b45b2ab41bc0fb5fdb0f0
|
pointer_interceptor_ios: da06a662d5bfd329602b45b2ab41bc0fb5fdb0f0
|
||||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||||
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
|
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
|
||||||
protocol_handler_ios: 59f23ee71f3ec602d67902ca7f669a80957888d5
|
protocol_handler_ios: 59f23ee71f3ec602d67902ca7f669a80957888d5
|
||||||
receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
|
receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
|
||||||
record_ios: f75fa1d57f840012775c0e93a38a7f3ceea1a374
|
record_ios: f75fa1d57f840012775c0e93a38a7f3ceea1a374
|
||||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
|
||||||
SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a
|
SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a
|
||||||
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||||
@@ -567,7 +555,6 @@ SPEC CHECKSUMS:
|
|||||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||||
syncfusion_flutter_pdfviewer: 90dc48305d2e33d4aa20681d1e98ddeda891bc14
|
syncfusion_flutter_pdfviewer: 90dc48305d2e33d4aa20681d1e98ddeda891bc14
|
||||||
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
||||||
volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12
|
|
||||||
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
||||||
WebRTC-SDK: 40d4f5ba05cadff14e4db5614aec402a633f007e
|
WebRTC-SDK: 40d4f5ba05cadff14e4db5614aec402a633f007e
|
||||||
|
|
||||||
|
|||||||
@@ -140,21 +140,29 @@ class NotificationService: UNNotificationServiceExtension {
|
|||||||
|
|
||||||
guard !attachmentUrls.isEmpty else {
|
guard !attachmentUrls.isEmpty else {
|
||||||
print("Invalid URLs for attachments: \(attachmentUrls)")
|
print("Invalid URLs for attachments: \(attachmentUrls)")
|
||||||
|
self.contentHandler?(content)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let targetSize = 512
|
let targetSize = 512
|
||||||
let scaleProcessor = ResizingImageProcessor(referenceSize: CGSize(width: targetSize, height: targetSize), mode: .aspectFit)
|
let scaleProcessor = ResizingImageProcessor(referenceSize: CGSize(width: targetSize, height: targetSize), mode: .aspectFit)
|
||||||
|
|
||||||
|
let dispatchGroup = DispatchGroup()
|
||||||
|
var attachments: [UNNotificationAttachment] = []
|
||||||
|
let lock = NSLock() // To synchronize access to the attachments array
|
||||||
|
|
||||||
for attachmentUrl in attachmentUrls {
|
for attachmentUrl in attachmentUrls {
|
||||||
guard let remoteUrl = URL(string: attachmentUrl) else {
|
guard let remoteUrl = URL(string: attachmentUrl) else {
|
||||||
print("Invalid URL for attachment: \(attachmentUrl)")
|
print("Invalid URL for attachment: \(attachmentUrl)")
|
||||||
continue // Skip this URL and move to the next one
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dispatchGroup.enter()
|
||||||
|
|
||||||
KingfisherManager.shared.retrieveImage(with: remoteUrl, options: scaleDown ? [
|
KingfisherManager.shared.retrieveImage(with: remoteUrl, options: scaleDown ? [
|
||||||
.processor(scaleProcessor)
|
.processor(scaleProcessor)
|
||||||
] : nil) { [weak self] result in
|
] : nil) { [weak self] result in
|
||||||
|
defer { dispatchGroup.leave() }
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|
||||||
switch result {
|
switch result {
|
||||||
@@ -166,49 +174,34 @@ class NotificationService: UNNotificationServiceExtension {
|
|||||||
do {
|
do {
|
||||||
// Write the image data to a temporary file for UNNotificationAttachment
|
// Write the image data to a temporary file for UNNotificationAttachment
|
||||||
try retrievalResult.image.pngData()?.write(to: cachedFileUrl)
|
try retrievalResult.image.pngData()?.write(to: cachedFileUrl)
|
||||||
self.attachLocalMedia(to: content, fileType: type?.identifier, from: cachedFileUrl, withIdentifier: attachmentUrl)
|
|
||||||
|
if let attachment = try? UNNotificationAttachment(identifier: attachmentUrl, url: cachedFileUrl, options: [
|
||||||
|
UNNotificationAttachmentOptionsTypeHintKey: type?.identifier as Any,
|
||||||
|
UNNotificationAttachmentOptionsThumbnailHiddenKey: 0,
|
||||||
|
]) {
|
||||||
|
lock.lock()
|
||||||
|
attachments.append(attachment)
|
||||||
|
lock.unlock()
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
print("Failed to write media to temporary file: \(error.localizedDescription)")
|
print("Failed to write media to temporary file: \(error.localizedDescription)")
|
||||||
self.contentHandler?(content)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
print("Failed to retrieve image: \(error.localizedDescription)")
|
print("Failed to retrieve image: \(error.localizedDescription)")
|
||||||
self.contentHandler?(content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func attachLocalMedia(to content: UNMutableNotificationContent, fileType type: String?, from localUrl: URL, withIdentifier identifier: String) {
|
|
||||||
do {
|
|
||||||
let attachment = try UNNotificationAttachment(identifier: identifier, url: localUrl, options: [
|
|
||||||
UNNotificationAttachmentOptionsTypeHintKey: type as Any,
|
|
||||||
UNNotificationAttachmentOptionsThumbnailHiddenKey: 0,
|
|
||||||
])
|
|
||||||
content.attachments = [attachment]
|
|
||||||
} catch let error as NSError {
|
|
||||||
// Log detailed error information
|
|
||||||
print("Failed to create attachment from file at \(localUrl.path)")
|
|
||||||
print("Error: \(error.localizedDescription)")
|
|
||||||
|
|
||||||
// Check specific error codes if needed
|
|
||||||
if error.domain == NSCocoaErrorDomain {
|
|
||||||
switch error.code {
|
|
||||||
case NSFileReadNoSuchFileError:
|
|
||||||
print("File does not exist at \(localUrl.path)")
|
|
||||||
case NSFileReadNoPermissionError:
|
|
||||||
print("No permission to read file at \(localUrl.path)")
|
|
||||||
default:
|
|
||||||
print("Unhandled file error: \(error.code)")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call content handler regardless of success or failure
|
dispatchGroup.notify(queue: .main) { [weak self] in
|
||||||
self.contentHandler?(content)
|
guard let self = self else { return }
|
||||||
|
content.attachments = attachments
|
||||||
|
self.contentHandler?(content)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private func createMessageIntent(with sender: INPerson, meta: [AnyHashable: Any], body: String) -> INSendMessageIntent {
|
private func createMessageIntent(with sender: INPerson, meta: [AnyHashable: Any], body: String) -> INSendMessageIntent {
|
||||||
INSendMessageIntent(
|
INSendMessageIntent(
|
||||||
recipients: nil,
|
recipients: nil,
|
||||||
|
|||||||
@@ -2,17 +2,19 @@ import 'dart:convert';
|
|||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:island/database/message.dart';
|
import 'package:island/database/message.dart';
|
||||||
import 'package:island/database/draft.dart';
|
import 'package:island/database/draft.dart';
|
||||||
|
import 'package:island/models/account.dart';
|
||||||
|
import 'package:island/models/chat.dart';
|
||||||
import 'package:island/models/post.dart';
|
import 'package:island/models/post.dart';
|
||||||
|
|
||||||
part 'drift_db.g.dart';
|
part 'drift_db.g.dart';
|
||||||
|
|
||||||
// Define the database
|
// Define the database
|
||||||
@DriftDatabase(tables: [ChatMessages, PostDrafts])
|
@DriftDatabase(tables: [ChatRooms, ChatMembers, ChatMessages, PostDrafts])
|
||||||
class AppDatabase extends _$AppDatabase {
|
class AppDatabase extends _$AppDatabase {
|
||||||
AppDatabase(super.e);
|
AppDatabase(super.e);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get schemaVersion => 7;
|
int get schemaVersion => 8;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MigrationStrategy get migration => MigrationStrategy(
|
MigrationStrategy get migration => MigrationStrategy(
|
||||||
@@ -55,6 +57,11 @@ class AppDatabase extends _$AppDatabase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (from < 8) {
|
||||||
|
// Add new tables for separate sender and room data
|
||||||
|
await m.createTable(chatRooms);
|
||||||
|
await m.createTable(chatMembers);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -153,6 +160,7 @@ class AppDatabase extends _$AppDatabase {
|
|||||||
String roomId,
|
String roomId,
|
||||||
String query, {
|
String query, {
|
||||||
bool? withAttachments,
|
bool? withAttachments,
|
||||||
|
Future<SnAccount?> Function(String accountId)? fetchAccount,
|
||||||
}) async {
|
}) async {
|
||||||
var selectStatement = select(chatMessages)
|
var selectStatement = select(chatMessages)
|
||||||
..where((m) => m.roomId.equals(roomId));
|
..where((m) => m.roomId.equals(roomId));
|
||||||
@@ -178,7 +186,11 @@ class AppDatabase extends _$AppDatabase {
|
|||||||
await (selectStatement
|
await (selectStatement
|
||||||
..orderBy([(m) => OrderingTerm.desc(m.createdAt)]))
|
..orderBy([(m) => OrderingTerm.desc(m.createdAt)]))
|
||||||
.get();
|
.get();
|
||||||
return messages.map((msg) => companionToMessage(msg)).toList();
|
final messageFutures =
|
||||||
|
messages
|
||||||
|
.map((msg) => companionToMessage(msg, fetchAccount: fetchAccount))
|
||||||
|
.toList();
|
||||||
|
return await Future.wait(messageFutures);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert between Drift and model objects
|
// Convert between Drift and model objects
|
||||||
@@ -206,12 +218,88 @@ class AppDatabase extends _$AppDatabase {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
LocalChatMessage companionToMessage(ChatMessage dbMessage) {
|
Future<LocalChatMessage> companionToMessage(
|
||||||
|
ChatMessage dbMessage, {
|
||||||
|
Future<SnAccount?> Function(String accountId)? fetchAccount,
|
||||||
|
}) async {
|
||||||
final data = jsonDecode(dbMessage.data);
|
final data = jsonDecode(dbMessage.data);
|
||||||
|
SnChatMember? sender;
|
||||||
|
try {
|
||||||
|
final senderRow =
|
||||||
|
await (select(chatMembers)
|
||||||
|
..where((m) => m.id.equals(dbMessage.senderId))).getSingle();
|
||||||
|
SnAccount senderAccount;
|
||||||
|
senderAccount = SnAccount.fromJson(senderRow.account);
|
||||||
|
|
||||||
|
sender = SnChatMember(
|
||||||
|
id: senderRow.id,
|
||||||
|
chatRoomId: senderRow.chatRoomId,
|
||||||
|
accountId: senderRow.accountId,
|
||||||
|
account: senderAccount,
|
||||||
|
nick: senderRow.nick,
|
||||||
|
role: senderRow.role,
|
||||||
|
notify: senderRow.notify,
|
||||||
|
joinedAt: senderRow.joinedAt,
|
||||||
|
breakUntil: senderRow.breakUntil,
|
||||||
|
timeoutUntil: senderRow.timeoutUntil,
|
||||||
|
isBot: senderRow.isBot,
|
||||||
|
status: null,
|
||||||
|
lastTyped: senderRow.lastTyped,
|
||||||
|
createdAt: senderRow.createdAt,
|
||||||
|
updatedAt: senderRow.updatedAt,
|
||||||
|
deletedAt: senderRow.deletedAt,
|
||||||
|
chatRoom: null,
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
// Fallback to dummy sender with senderId as display name
|
||||||
|
sender = SnChatMember(
|
||||||
|
id: 'unknown',
|
||||||
|
chatRoomId: dbMessage.roomId,
|
||||||
|
accountId: dbMessage.senderId,
|
||||||
|
account: SnAccount(
|
||||||
|
id: 'unknown',
|
||||||
|
name: 'unknown',
|
||||||
|
nick: dbMessage.senderId, // Show the ID instead of Unknown
|
||||||
|
profile: SnAccountProfile(
|
||||||
|
picture: null,
|
||||||
|
id: 'unknown',
|
||||||
|
experience: 0,
|
||||||
|
level: 1,
|
||||||
|
levelingProgress: 0.0,
|
||||||
|
background: null,
|
||||||
|
verification: null,
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
deletedAt: null,
|
||||||
|
),
|
||||||
|
language: '',
|
||||||
|
isSuperuser: false,
|
||||||
|
automatedId: null,
|
||||||
|
perkSubscription: null,
|
||||||
|
deletedAt: null,
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
),
|
||||||
|
nick: dbMessage.senderId, // Show the senderId as fallback
|
||||||
|
role: 0,
|
||||||
|
notify: 0,
|
||||||
|
joinedAt: null,
|
||||||
|
breakUntil: null,
|
||||||
|
timeoutUntil: null,
|
||||||
|
isBot: false,
|
||||||
|
status: null,
|
||||||
|
lastTyped: null,
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
deletedAt: null,
|
||||||
|
chatRoom: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
return LocalChatMessage(
|
return LocalChatMessage(
|
||||||
id: dbMessage.id,
|
id: dbMessage.id,
|
||||||
roomId: dbMessage.roomId,
|
roomId: dbMessage.roomId,
|
||||||
senderId: dbMessage.senderId,
|
senderId: dbMessage.senderId,
|
||||||
|
sender: sender,
|
||||||
data: data,
|
data: data,
|
||||||
createdAt: dbMessage.createdAt,
|
createdAt: dbMessage.createdAt,
|
||||||
status: dbMessage.status,
|
status: dbMessage.status,
|
||||||
@@ -231,6 +319,85 @@ class AppDatabase extends _$AppDatabase {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ChatRoomsCompanion companionFromRoom(SnChatRoom room) {
|
||||||
|
return ChatRoomsCompanion(
|
||||||
|
id: Value(room.id),
|
||||||
|
name: Value(room.name),
|
||||||
|
description: Value(room.description),
|
||||||
|
type: Value(room.type),
|
||||||
|
isPublic: Value(room.isPublic),
|
||||||
|
isCommunity: Value(room.isCommunity),
|
||||||
|
picture: Value(room.picture?.toJson()),
|
||||||
|
background: Value(room.background?.toJson()),
|
||||||
|
realmId: Value(room.realmId),
|
||||||
|
createdAt: Value(room.createdAt),
|
||||||
|
updatedAt: Value(room.updatedAt),
|
||||||
|
deletedAt: Value(room.deletedAt),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatMembersCompanion companionFromMember(SnChatMember member) {
|
||||||
|
return ChatMembersCompanion(
|
||||||
|
id: Value(member.id),
|
||||||
|
chatRoomId: Value(member.chatRoomId),
|
||||||
|
accountId: Value(member.accountId),
|
||||||
|
account: Value(member.account.toJson()),
|
||||||
|
nick: Value(member.nick),
|
||||||
|
role: Value(member.role),
|
||||||
|
notify: Value(member.notify),
|
||||||
|
joinedAt: Value(member.joinedAt),
|
||||||
|
breakUntil: Value(member.breakUntil),
|
||||||
|
timeoutUntil: Value(member.timeoutUntil),
|
||||||
|
isBot: Value(member.isBot),
|
||||||
|
status: Value(
|
||||||
|
member.status == null ? null : jsonEncode(member.status!.toJson()),
|
||||||
|
),
|
||||||
|
lastTyped: Value(member.lastTyped),
|
||||||
|
createdAt: Value(member.createdAt),
|
||||||
|
updatedAt: Value(member.updatedAt),
|
||||||
|
deletedAt: Value(member.deletedAt),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> saveChatRooms(List<SnChatRoom> rooms) async {
|
||||||
|
await transaction(() async {
|
||||||
|
// 1. Identify rooms to remove
|
||||||
|
final remoteRoomIds = rooms.map((r) => r.id).toSet();
|
||||||
|
final currentRooms = await select(chatRooms).get();
|
||||||
|
final currentRoomIds = currentRooms.map((r) => r.id).toSet();
|
||||||
|
final idsToRemove = currentRoomIds.difference(remoteRoomIds);
|
||||||
|
|
||||||
|
if (idsToRemove.isNotEmpty) {
|
||||||
|
final idsList = idsToRemove.toList();
|
||||||
|
// Remove messages
|
||||||
|
await (delete(chatMessages)..where((t) => t.roomId.isIn(idsList))).go();
|
||||||
|
// Remove members
|
||||||
|
await (delete(chatMembers)
|
||||||
|
..where((t) => t.chatRoomId.isIn(idsList))).go();
|
||||||
|
// Remove rooms
|
||||||
|
await (delete(chatRooms)..where((t) => t.id.isIn(idsList))).go();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Upsert remote rooms
|
||||||
|
await batch((batch) {
|
||||||
|
for (final room in rooms) {
|
||||||
|
batch.insert(
|
||||||
|
chatRooms,
|
||||||
|
companionFromRoom(room),
|
||||||
|
mode: InsertMode.insertOrReplace,
|
||||||
|
);
|
||||||
|
for (final member in room.members ?? []) {
|
||||||
|
batch.insert(
|
||||||
|
chatMembers,
|
||||||
|
companionFromMember(member),
|
||||||
|
mode: InsertMode.insertOrReplace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Methods for post drafts
|
// Methods for post drafts
|
||||||
Future<List<SnPost>> getAllPostDrafts() async {
|
Future<List<SnPost>> getAllPostDrafts() async {
|
||||||
final drafts = await select(postDrafts).get();
|
final drafts = await select(postDrafts).get();
|
||||||
@@ -276,4 +443,19 @@ class AppDatabase extends _$AppDatabase {
|
|||||||
return await (select(postDrafts)
|
return await (select(postDrafts)
|
||||||
..where((tbl) => tbl.id.equals(id))).getSingleOrNull();
|
..where((tbl) => tbl.id.equals(id))).getSingleOrNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> saveMember(SnChatMember member) async {
|
||||||
|
await into(
|
||||||
|
chatMembers,
|
||||||
|
).insert(companionFromMember(member), mode: InsertMode.insertOrReplace);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> saveMessageWithSender(LocalChatMessage message) async {
|
||||||
|
// First save the sender if it exists
|
||||||
|
if (message.sender != null) {
|
||||||
|
await saveMember(message.sender!);
|
||||||
|
}
|
||||||
|
// Then save the message
|
||||||
|
return await saveMessage(messageToCompanion(message));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
616
lib/database/drift_db.steps.dart
Normal file
616
lib/database/drift_db.steps.dart
Normal file
@@ -0,0 +1,616 @@
|
|||||||
|
// dart format width=80
|
||||||
|
import 'package:drift/internal/versioned_schema.dart' as i0;
|
||||||
|
import 'package:drift/drift.dart' as i1;
|
||||||
|
import 'package:drift/drift.dart'; // ignore_for_file: type=lint,unused_import
|
||||||
|
|
||||||
|
// GENERATED BY drift_dev, DO NOT MODIFY.
|
||||||
|
final class Schema7 extends i0.VersionedSchema {
|
||||||
|
Schema7({required super.database}) : super(version: 7);
|
||||||
|
@override
|
||||||
|
late final List<i1.DatabaseSchemaEntity> entities = [
|
||||||
|
chatRooms,
|
||||||
|
chatMembers,
|
||||||
|
chatMessages,
|
||||||
|
postDrafts,
|
||||||
|
];
|
||||||
|
late final Shape0 chatRooms = Shape0(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'chat_rooms',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_1,
|
||||||
|
_column_2,
|
||||||
|
_column_3,
|
||||||
|
_column_4,
|
||||||
|
_column_5,
|
||||||
|
_column_6,
|
||||||
|
_column_7,
|
||||||
|
_column_8,
|
||||||
|
_column_9,
|
||||||
|
_column_10,
|
||||||
|
_column_11,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape1 chatMembers = Shape1(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'chat_members',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_12,
|
||||||
|
_column_13,
|
||||||
|
_column_14,
|
||||||
|
_column_15,
|
||||||
|
_column_16,
|
||||||
|
_column_17,
|
||||||
|
_column_18,
|
||||||
|
_column_19,
|
||||||
|
_column_20,
|
||||||
|
_column_21,
|
||||||
|
_column_22,
|
||||||
|
_column_23,
|
||||||
|
_column_9,
|
||||||
|
_column_10,
|
||||||
|
_column_11,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape2 chatMessages = Shape2(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'chat_messages',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_24,
|
||||||
|
_column_25,
|
||||||
|
_column_26,
|
||||||
|
_column_27,
|
||||||
|
_column_28,
|
||||||
|
_column_9,
|
||||||
|
_column_29,
|
||||||
|
_column_30,
|
||||||
|
_column_31,
|
||||||
|
_column_11,
|
||||||
|
_column_32,
|
||||||
|
_column_33,
|
||||||
|
_column_34,
|
||||||
|
_column_35,
|
||||||
|
_column_36,
|
||||||
|
_column_37,
|
||||||
|
_column_38,
|
||||||
|
_column_39,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape3 postDrafts = Shape3(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'post_drafts',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_40,
|
||||||
|
_column_2,
|
||||||
|
_column_26,
|
||||||
|
_column_41,
|
||||||
|
_column_42,
|
||||||
|
_column_43,
|
||||||
|
_column_44,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Shape0 extends i0.VersionedTable {
|
||||||
|
Shape0({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<String> get id =>
|
||||||
|
columnsByName['id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get name =>
|
||||||
|
columnsByName['name']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get description =>
|
||||||
|
columnsByName['description']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<int> get type =>
|
||||||
|
columnsByName['type']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<bool> get isPublic =>
|
||||||
|
columnsByName['is_public']! as i1.GeneratedColumn<bool>;
|
||||||
|
i1.GeneratedColumn<bool> get isCommunity =>
|
||||||
|
columnsByName['is_community']! as i1.GeneratedColumn<bool>;
|
||||||
|
i1.GeneratedColumn<String> get picture =>
|
||||||
|
columnsByName['picture']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get background =>
|
||||||
|
columnsByName['background']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get realmId =>
|
||||||
|
columnsByName['realm_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<DateTime> get createdAt =>
|
||||||
|
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<DateTime> get updatedAt =>
|
||||||
|
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<DateTime> get deletedAt =>
|
||||||
|
columnsByName['deleted_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<String> _column_0(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'id',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_1(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'name',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_2(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'description',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<int> _column_3(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>(
|
||||||
|
'type',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.int,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<bool> _column_4(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<bool>(
|
||||||
|
'is_public',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.bool,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'CHECK ("is_public" IN (0, 1))',
|
||||||
|
),
|
||||||
|
defaultValue: const CustomExpression('0'),
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<bool> _column_5(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<bool>(
|
||||||
|
'is_community',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.bool,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'CHECK ("is_community" IN (0, 1))',
|
||||||
|
),
|
||||||
|
defaultValue: const CustomExpression('0'),
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_6(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'picture',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_7(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'background',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_8(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'realm_id',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<DateTime> _column_9(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<DateTime>(
|
||||||
|
'created_at',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.dateTime,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<DateTime> _column_10(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<DateTime>(
|
||||||
|
'updated_at',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.dateTime,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<DateTime> _column_11(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<DateTime>(
|
||||||
|
'deleted_at',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.dateTime,
|
||||||
|
);
|
||||||
|
|
||||||
|
class Shape1 extends i0.VersionedTable {
|
||||||
|
Shape1({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<String> get id =>
|
||||||
|
columnsByName['id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get chatRoomId =>
|
||||||
|
columnsByName['chat_room_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get accountId =>
|
||||||
|
columnsByName['account_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get account =>
|
||||||
|
columnsByName['account']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get nick =>
|
||||||
|
columnsByName['nick']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<int> get role =>
|
||||||
|
columnsByName['role']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<int> get notify =>
|
||||||
|
columnsByName['notify']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<DateTime> get joinedAt =>
|
||||||
|
columnsByName['joined_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<DateTime> get breakUntil =>
|
||||||
|
columnsByName['break_until']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<DateTime> get timeoutUntil =>
|
||||||
|
columnsByName['timeout_until']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<bool> get isBot =>
|
||||||
|
columnsByName['is_bot']! as i1.GeneratedColumn<bool>;
|
||||||
|
i1.GeneratedColumn<String> get status =>
|
||||||
|
columnsByName['status']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<DateTime> get lastTyped =>
|
||||||
|
columnsByName['last_typed']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<DateTime> get createdAt =>
|
||||||
|
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<DateTime> get updatedAt =>
|
||||||
|
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<DateTime> get deletedAt =>
|
||||||
|
columnsByName['deleted_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<String> _column_12(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'chat_room_id',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'REFERENCES chat_rooms (id)',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_13(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'account_id',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_14(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'account',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_15(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'nick',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<int> _column_16(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>(
|
||||||
|
'role',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.int,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<int> _column_17(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>(
|
||||||
|
'notify',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.int,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<DateTime> _column_18(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<DateTime>(
|
||||||
|
'joined_at',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.dateTime,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<DateTime> _column_19(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<DateTime>(
|
||||||
|
'break_until',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.dateTime,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<DateTime> _column_20(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<DateTime>(
|
||||||
|
'timeout_until',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.dateTime,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<bool> _column_21(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<bool>(
|
||||||
|
'is_bot',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.bool,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'CHECK ("is_bot" IN (0, 1))',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_22(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'status',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<DateTime> _column_23(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<DateTime>(
|
||||||
|
'last_typed',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.dateTime,
|
||||||
|
);
|
||||||
|
|
||||||
|
class Shape2 extends i0.VersionedTable {
|
||||||
|
Shape2({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<String> get id =>
|
||||||
|
columnsByName['id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get roomId =>
|
||||||
|
columnsByName['room_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get senderId =>
|
||||||
|
columnsByName['sender_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get content =>
|
||||||
|
columnsByName['content']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get nonce =>
|
||||||
|
columnsByName['nonce']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get data =>
|
||||||
|
columnsByName['data']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<DateTime> get createdAt =>
|
||||||
|
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<int> get status =>
|
||||||
|
columnsByName['status']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<bool> get isDeleted =>
|
||||||
|
columnsByName['is_deleted']! as i1.GeneratedColumn<bool>;
|
||||||
|
i1.GeneratedColumn<DateTime> get updatedAt =>
|
||||||
|
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<DateTime> get deletedAt =>
|
||||||
|
columnsByName['deleted_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<String> get type =>
|
||||||
|
columnsByName['type']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get meta =>
|
||||||
|
columnsByName['meta']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get membersMentioned =>
|
||||||
|
columnsByName['members_mentioned']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<DateTime> get editedAt =>
|
||||||
|
columnsByName['edited_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<String> get attachments =>
|
||||||
|
columnsByName['attachments']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get reactions =>
|
||||||
|
columnsByName['reactions']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get repliedMessageId =>
|
||||||
|
columnsByName['replied_message_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get forwardedMessageId =>
|
||||||
|
columnsByName['forwarded_message_id']! as i1.GeneratedColumn<String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<String> _column_24(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'room_id',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'REFERENCES chat_rooms (id)',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_25(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'sender_id',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'REFERENCES chat_members (id)',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_26(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'content',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_27(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'nonce',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_28(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'data',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<int> _column_29(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>(
|
||||||
|
'status',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.int,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<bool> _column_30(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<bool>(
|
||||||
|
'is_deleted',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.bool,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'CHECK ("is_deleted" IN (0, 1))',
|
||||||
|
),
|
||||||
|
defaultValue: const CustomExpression('0'),
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<DateTime> _column_31(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<DateTime>(
|
||||||
|
'updated_at',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.dateTime,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_32(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'type',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
defaultValue: const CustomExpression('\'text\''),
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_33(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'meta',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
defaultValue: const CustomExpression('\'{}\''),
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_34(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'members_mentioned',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
defaultValue: const CustomExpression('\'[]\''),
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<DateTime> _column_35(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<DateTime>(
|
||||||
|
'edited_at',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.dateTime,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_36(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'attachments',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
defaultValue: const CustomExpression('\'[]\''),
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_37(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'reactions',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
defaultValue: const CustomExpression('\'[]\''),
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_38(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'replied_message_id',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_39(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'forwarded_message_id',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
);
|
||||||
|
|
||||||
|
class Shape3 extends i0.VersionedTable {
|
||||||
|
Shape3({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<String> get id =>
|
||||||
|
columnsByName['id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get title =>
|
||||||
|
columnsByName['title']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get description =>
|
||||||
|
columnsByName['description']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get content =>
|
||||||
|
columnsByName['content']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<int> get visibility =>
|
||||||
|
columnsByName['visibility']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<int> get type =>
|
||||||
|
columnsByName['type']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<DateTime> get lastModified =>
|
||||||
|
columnsByName['last_modified']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<String> get postData =>
|
||||||
|
columnsByName['post_data']! as i1.GeneratedColumn<String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<String> _column_40(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'title',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<int> _column_41(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>(
|
||||||
|
'visibility',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.int,
|
||||||
|
defaultValue: const CustomExpression('0'),
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<int> _column_42(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>(
|
||||||
|
'type',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.int,
|
||||||
|
defaultValue: const CustomExpression('0'),
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<DateTime> _column_43(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<DateTime>(
|
||||||
|
'last_modified',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.dateTime,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_44(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'post_data',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
);
|
||||||
|
i0.MigrationStepWithVersion migrationSteps({
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7,
|
||||||
|
}) {
|
||||||
|
return (currentVersion, database) async {
|
||||||
|
switch (currentVersion) {
|
||||||
|
case 6:
|
||||||
|
final schema = Schema7(database: database);
|
||||||
|
final migrator = i1.Migrator(database, schema);
|
||||||
|
await from6To7(migrator, schema);
|
||||||
|
return 7;
|
||||||
|
default:
|
||||||
|
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.OnUpgrade stepByStep({
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7,
|
||||||
|
}) => i0.VersionedSchema.stepByStepHelper(
|
||||||
|
step: migrationSteps(from6To7: from6To7),
|
||||||
|
);
|
||||||
@@ -36,10 +36,52 @@ class ListMapConverter
|
|||||||
String toSql(List<Map<String, dynamic>> value) => json.encode(value);
|
String toSql(List<Map<String, dynamic>> value) => json.encode(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ChatRooms extends Table {
|
||||||
|
TextColumn get id => text()();
|
||||||
|
TextColumn get name => text().nullable()();
|
||||||
|
TextColumn get description => text().nullable()();
|
||||||
|
IntColumn get type => integer()();
|
||||||
|
BoolColumn get isPublic =>
|
||||||
|
boolean().nullable().withDefault(const Constant(false))();
|
||||||
|
BoolColumn get isCommunity =>
|
||||||
|
boolean().nullable().withDefault(const Constant(false))();
|
||||||
|
TextColumn get picture => text().map(const MapConverter()).nullable()();
|
||||||
|
TextColumn get background => text().map(const MapConverter()).nullable()();
|
||||||
|
TextColumn get realmId => text().nullable()();
|
||||||
|
DateTimeColumn get createdAt => dateTime()();
|
||||||
|
DateTimeColumn get updatedAt => dateTime()();
|
||||||
|
DateTimeColumn get deletedAt => dateTime().nullable()();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<Column> get primaryKey => {id};
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChatMembers extends Table {
|
||||||
|
TextColumn get id => text()();
|
||||||
|
TextColumn get chatRoomId => text().references(ChatRooms, #id)();
|
||||||
|
TextColumn get accountId => text()();
|
||||||
|
TextColumn get account => text().map(const MapConverter())();
|
||||||
|
TextColumn get nick => text().nullable()();
|
||||||
|
IntColumn get role => integer()();
|
||||||
|
IntColumn get notify => integer()();
|
||||||
|
DateTimeColumn get joinedAt => dateTime().nullable()();
|
||||||
|
DateTimeColumn get breakUntil => dateTime().nullable()();
|
||||||
|
DateTimeColumn get timeoutUntil => dateTime().nullable()();
|
||||||
|
BoolColumn get isBot => boolean()();
|
||||||
|
TextColumn get status => text().nullable()();
|
||||||
|
DateTimeColumn get lastTyped => dateTime().nullable()();
|
||||||
|
DateTimeColumn get createdAt => dateTime()();
|
||||||
|
DateTimeColumn get updatedAt => dateTime()();
|
||||||
|
DateTimeColumn get deletedAt => dateTime().nullable()();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<Column> get primaryKey => {id};
|
||||||
|
}
|
||||||
|
|
||||||
class ChatMessages extends Table {
|
class ChatMessages extends Table {
|
||||||
TextColumn get id => text()();
|
TextColumn get id => text()();
|
||||||
TextColumn get roomId => text()();
|
TextColumn get roomId => text().references(ChatRooms, #id)();
|
||||||
TextColumn get senderId => text()();
|
TextColumn get senderId => text().references(ChatMembers, #id)();
|
||||||
TextColumn get content => text().nullable()();
|
TextColumn get content => text().nullable()();
|
||||||
TextColumn get nonce => text().nullable()();
|
TextColumn get nonce => text().nullable()();
|
||||||
TextColumn get data => text()();
|
TextColumn get data => text()();
|
||||||
@@ -72,6 +114,7 @@ class LocalChatMessage {
|
|||||||
final String id;
|
final String id;
|
||||||
final String roomId;
|
final String roomId;
|
||||||
final String senderId;
|
final String senderId;
|
||||||
|
final SnChatMember? sender;
|
||||||
final Map<String, dynamic> data;
|
final Map<String, dynamic> data;
|
||||||
final DateTime createdAt;
|
final DateTime createdAt;
|
||||||
MessageStatus status;
|
MessageStatus status;
|
||||||
@@ -94,6 +137,7 @@ class LocalChatMessage {
|
|||||||
required this.id,
|
required this.id,
|
||||||
required this.roomId,
|
required this.roomId,
|
||||||
required this.senderId,
|
required this.senderId,
|
||||||
|
required this.sender,
|
||||||
required this.data,
|
required this.data,
|
||||||
required this.createdAt,
|
required this.createdAt,
|
||||||
required this.nonce,
|
required this.nonce,
|
||||||
@@ -114,7 +158,12 @@ class LocalChatMessage {
|
|||||||
});
|
});
|
||||||
|
|
||||||
SnChatMessage toRemoteMessage() {
|
SnChatMessage toRemoteMessage() {
|
||||||
return SnChatMessage.fromJson(data);
|
if (sender == null) {
|
||||||
|
throw Exception('Cannot create remote message without sender');
|
||||||
|
}
|
||||||
|
final msgData = Map<String, dynamic>.from(data);
|
||||||
|
msgData['sender'] = sender!.toJson();
|
||||||
|
return SnChatMessage.fromJson(msgData);
|
||||||
}
|
}
|
||||||
|
|
||||||
static LocalChatMessage fromRemoteMessage(
|
static LocalChatMessage fromRemoteMessage(
|
||||||
@@ -122,11 +171,26 @@ class LocalChatMessage {
|
|||||||
MessageStatus status, {
|
MessageStatus status, {
|
||||||
String? nonce,
|
String? nonce,
|
||||||
}) {
|
}) {
|
||||||
|
final jsonData = message.toJson();
|
||||||
|
jsonData.remove('sender');
|
||||||
|
// Ensure proper defaults for collections to prevent type cast errors
|
||||||
|
if (jsonData['meta'] == null) jsonData['meta'] = <String, dynamic>{};
|
||||||
|
if (jsonData['members_mentioned'] == null) {
|
||||||
|
jsonData['members_mentioned'] = <String>[];
|
||||||
|
}
|
||||||
|
if (jsonData['attachments'] == null) {
|
||||||
|
jsonData['attachments'] = <Map<String, dynamic>>[];
|
||||||
|
}
|
||||||
|
if (jsonData['reactions'] == null) {
|
||||||
|
jsonData['reactions'] = <Map<String, dynamic>>[];
|
||||||
|
}
|
||||||
|
final msgData = Map<String, dynamic>.from(jsonData);
|
||||||
return LocalChatMessage(
|
return LocalChatMessage(
|
||||||
id: message.id,
|
id: message.id,
|
||||||
roomId: message.chatRoomId,
|
roomId: message.chatRoomId,
|
||||||
senderId: message.senderId,
|
senderId: message.senderId,
|
||||||
data: message.toJson(),
|
sender: message.sender,
|
||||||
|
data: msgData,
|
||||||
createdAt: message.createdAt,
|
createdAt: message.createdAt,
|
||||||
status: status,
|
status: status,
|
||||||
nonce: nonce ?? message.nonce,
|
nonce: nonce ?? message.nonce,
|
||||||
|
|||||||
41
lib/models/publication_site.dart
Normal file
41
lib/models/publication_site.dart
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'publication_site.freezed.dart';
|
||||||
|
part 'publication_site.g.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
sealed class SnPublicationSite with _$SnPublicationSite {
|
||||||
|
const factory SnPublicationSite({
|
||||||
|
required String id,
|
||||||
|
required String slug,
|
||||||
|
required String name,
|
||||||
|
String? description,
|
||||||
|
int? mode,
|
||||||
|
required String publisherId,
|
||||||
|
required String accountId,
|
||||||
|
required DateTime createdAt,
|
||||||
|
required DateTime updatedAt,
|
||||||
|
required List<SnPublicationPage> pages,
|
||||||
|
}) = _SnPublicationSite;
|
||||||
|
|
||||||
|
factory SnPublicationSite.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SnPublicationSiteFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
sealed class SnPublicationPage with _$SnPublicationPage {
|
||||||
|
const factory SnPublicationPage({
|
||||||
|
required String id,
|
||||||
|
String? preset,
|
||||||
|
String? path,
|
||||||
|
Map<String, dynamic>? config,
|
||||||
|
required String siteId,
|
||||||
|
required DateTime createdAt,
|
||||||
|
required DateTime updatedAt,
|
||||||
|
}) = _SnPublicationPage;
|
||||||
|
|
||||||
|
factory SnPublicationPage.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SnPublicationPageFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PublicationPagePreset { landing, profile, posts, custom }
|
||||||
587
lib/models/publication_site.freezed.dart
Normal file
587
lib/models/publication_site.freezed.dart
Normal file
@@ -0,0 +1,587 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// coverage:ignore-file
|
||||||
|
// 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 'publication_site.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// FreezedGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// dart format off
|
||||||
|
T _$identity<T>(T value) => value;
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$SnPublicationSite {
|
||||||
|
|
||||||
|
String get id; String get slug; String get name; String? get description; int? get mode; String get publisherId; String get accountId; DateTime get createdAt; DateTime get updatedAt; List<SnPublicationPage> get pages;
|
||||||
|
/// Create a copy of SnPublicationSite
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnPublicationSiteCopyWith<SnPublicationSite> get copyWith => _$SnPublicationSiteCopyWithImpl<SnPublicationSite>(this as SnPublicationSite, _$identity);
|
||||||
|
|
||||||
|
/// Serializes this SnPublicationSite to a JSON map.
|
||||||
|
Map<String, dynamic> toJson();
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPublicationSite&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.mode, mode) || other.mode == mode)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&const DeepCollectionEquality().equals(other.pages, pages));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,id,slug,name,description,mode,publisherId,accountId,createdAt,updatedAt,const DeepCollectionEquality().hash(pages));
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SnPublicationSite(id: $id, slug: $slug, name: $name, description: $description, mode: $mode, publisherId: $publisherId, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, pages: $pages)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class $SnPublicationSiteCopyWith<$Res> {
|
||||||
|
factory $SnPublicationSiteCopyWith(SnPublicationSite value, $Res Function(SnPublicationSite) _then) = _$SnPublicationSiteCopyWithImpl;
|
||||||
|
@useResult
|
||||||
|
$Res call({
|
||||||
|
String id, String slug, String name, String? description, int? mode, String publisherId, String accountId, DateTime createdAt, DateTime updatedAt, List<SnPublicationPage> pages
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class _$SnPublicationSiteCopyWithImpl<$Res>
|
||||||
|
implements $SnPublicationSiteCopyWith<$Res> {
|
||||||
|
_$SnPublicationSiteCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final SnPublicationSite _self;
|
||||||
|
final $Res Function(SnPublicationSite) _then;
|
||||||
|
|
||||||
|
/// Create a copy of SnPublicationSite
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? slug = null,Object? name = null,Object? description = freezed,Object? mode = freezed,Object? publisherId = null,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? pages = null,}) {
|
||||||
|
return _then(_self.copyWith(
|
||||||
|
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,slug: null == slug ? _self.slug : slug // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,mode: freezed == mode ? _self.mode : mode // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int?,publisherId: null == publisherId ? _self.publisherId : publisherId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,pages: null == pages ? _self.pages : pages // ignore: cast_nullable_to_non_nullable
|
||||||
|
as List<SnPublicationPage>,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Adds pattern-matching-related methods to [SnPublicationSite].
|
||||||
|
extension SnPublicationSitePatterns on SnPublicationSite {
|
||||||
|
/// A variant of `map` that fallback to returning `orElse`.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return orElse();
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnPublicationSite value)? $default,{required TResult orElse(),}){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnPublicationSite() when $default != null:
|
||||||
|
return $default(_that);case _:
|
||||||
|
return orElse();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A `switch`-like method, using callbacks.
|
||||||
|
///
|
||||||
|
/// Callbacks receives the raw object, upcasted.
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case final Subclass2 value:
|
||||||
|
/// return ...;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnPublicationSite value) $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnPublicationSite():
|
||||||
|
return $default(_that);}
|
||||||
|
}
|
||||||
|
/// A variant of `map` that fallback to returning `null`.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return null;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnPublicationSite value)? $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnPublicationSite() when $default != null:
|
||||||
|
return $default(_that);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A variant of `when` that fallback to an `orElse` callback.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return orElse();
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String slug, String name, String? description, int? mode, String publisherId, String accountId, DateTime createdAt, DateTime updatedAt, List<SnPublicationPage> pages)? $default,{required TResult orElse(),}) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnPublicationSite() when $default != null:
|
||||||
|
return $default(_that.id,_that.slug,_that.name,_that.description,_that.mode,_that.publisherId,_that.accountId,_that.createdAt,_that.updatedAt,_that.pages);case _:
|
||||||
|
return orElse();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A `switch`-like method, using callbacks.
|
||||||
|
///
|
||||||
|
/// As opposed to `map`, this offers destructuring.
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case Subclass2(:final field2):
|
||||||
|
/// return ...;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String slug, String name, String? description, int? mode, String publisherId, String accountId, DateTime createdAt, DateTime updatedAt, List<SnPublicationPage> pages) $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnPublicationSite():
|
||||||
|
return $default(_that.id,_that.slug,_that.name,_that.description,_that.mode,_that.publisherId,_that.accountId,_that.createdAt,_that.updatedAt,_that.pages);}
|
||||||
|
}
|
||||||
|
/// A variant of `when` that fallback to returning `null`
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return null;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String slug, String name, String? description, int? mode, String publisherId, String accountId, DateTime createdAt, DateTime updatedAt, List<SnPublicationPage> pages)? $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnPublicationSite() when $default != null:
|
||||||
|
return $default(_that.id,_that.slug,_that.name,_that.description,_that.mode,_that.publisherId,_that.accountId,_that.createdAt,_that.updatedAt,_that.pages);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
|
||||||
|
class _SnPublicationSite implements SnPublicationSite {
|
||||||
|
const _SnPublicationSite({required this.id, required this.slug, required this.name, this.description, this.mode, required this.publisherId, required this.accountId, required this.createdAt, required this.updatedAt, required final List<SnPublicationPage> pages}): _pages = pages;
|
||||||
|
factory _SnPublicationSite.fromJson(Map<String, dynamic> json) => _$SnPublicationSiteFromJson(json);
|
||||||
|
|
||||||
|
@override final String id;
|
||||||
|
@override final String slug;
|
||||||
|
@override final String name;
|
||||||
|
@override final String? description;
|
||||||
|
@override final int? mode;
|
||||||
|
@override final String publisherId;
|
||||||
|
@override final String accountId;
|
||||||
|
@override final DateTime createdAt;
|
||||||
|
@override final DateTime updatedAt;
|
||||||
|
final List<SnPublicationPage> _pages;
|
||||||
|
@override List<SnPublicationPage> get pages {
|
||||||
|
if (_pages is EqualUnmodifiableListView) return _pages;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableListView(_pages);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Create a copy of SnPublicationSite
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$SnPublicationSiteCopyWith<_SnPublicationSite> get copyWith => __$SnPublicationSiteCopyWithImpl<_SnPublicationSite>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$SnPublicationSiteToJson(this, );
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPublicationSite&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.mode, mode) || other.mode == mode)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&const DeepCollectionEquality().equals(other._pages, _pages));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,id,slug,name,description,mode,publisherId,accountId,createdAt,updatedAt,const DeepCollectionEquality().hash(_pages));
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SnPublicationSite(id: $id, slug: $slug, name: $name, description: $description, mode: $mode, publisherId: $publisherId, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, pages: $pages)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class _$SnPublicationSiteCopyWith<$Res> implements $SnPublicationSiteCopyWith<$Res> {
|
||||||
|
factory _$SnPublicationSiteCopyWith(_SnPublicationSite value, $Res Function(_SnPublicationSite) _then) = __$SnPublicationSiteCopyWithImpl;
|
||||||
|
@override @useResult
|
||||||
|
$Res call({
|
||||||
|
String id, String slug, String name, String? description, int? mode, String publisherId, String accountId, DateTime createdAt, DateTime updatedAt, List<SnPublicationPage> pages
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class __$SnPublicationSiteCopyWithImpl<$Res>
|
||||||
|
implements _$SnPublicationSiteCopyWith<$Res> {
|
||||||
|
__$SnPublicationSiteCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final _SnPublicationSite _self;
|
||||||
|
final $Res Function(_SnPublicationSite) _then;
|
||||||
|
|
||||||
|
/// Create a copy of SnPublicationSite
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? slug = null,Object? name = null,Object? description = freezed,Object? mode = freezed,Object? publisherId = null,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? pages = null,}) {
|
||||||
|
return _then(_SnPublicationSite(
|
||||||
|
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,slug: null == slug ? _self.slug : slug // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,mode: freezed == mode ? _self.mode : mode // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int?,publisherId: null == publisherId ? _self.publisherId : publisherId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,pages: null == pages ? _self._pages : pages // ignore: cast_nullable_to_non_nullable
|
||||||
|
as List<SnPublicationPage>,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$SnPublicationPage {
|
||||||
|
|
||||||
|
String get id; String? get preset; String? get path; Map<String, dynamic>? get config; String get siteId; DateTime get createdAt; DateTime get updatedAt;
|
||||||
|
/// Create a copy of SnPublicationPage
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnPublicationPageCopyWith<SnPublicationPage> get copyWith => _$SnPublicationPageCopyWithImpl<SnPublicationPage>(this as SnPublicationPage, _$identity);
|
||||||
|
|
||||||
|
/// Serializes this SnPublicationPage to a JSON map.
|
||||||
|
Map<String, dynamic> toJson();
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPublicationPage&&(identical(other.id, id) || other.id == id)&&(identical(other.preset, preset) || other.preset == preset)&&(identical(other.path, path) || other.path == path)&&const DeepCollectionEquality().equals(other.config, config)&&(identical(other.siteId, siteId) || other.siteId == siteId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,id,preset,path,const DeepCollectionEquality().hash(config),siteId,createdAt,updatedAt);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SnPublicationPage(id: $id, preset: $preset, path: $path, config: $config, siteId: $siteId, createdAt: $createdAt, updatedAt: $updatedAt)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class $SnPublicationPageCopyWith<$Res> {
|
||||||
|
factory $SnPublicationPageCopyWith(SnPublicationPage value, $Res Function(SnPublicationPage) _then) = _$SnPublicationPageCopyWithImpl;
|
||||||
|
@useResult
|
||||||
|
$Res call({
|
||||||
|
String id, String? preset, String? path, Map<String, dynamic>? config, String siteId, DateTime createdAt, DateTime updatedAt
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class _$SnPublicationPageCopyWithImpl<$Res>
|
||||||
|
implements $SnPublicationPageCopyWith<$Res> {
|
||||||
|
_$SnPublicationPageCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final SnPublicationPage _self;
|
||||||
|
final $Res Function(SnPublicationPage) _then;
|
||||||
|
|
||||||
|
/// Create a copy of SnPublicationPage
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? preset = freezed,Object? path = freezed,Object? config = freezed,Object? siteId = null,Object? createdAt = null,Object? updatedAt = null,}) {
|
||||||
|
return _then(_self.copyWith(
|
||||||
|
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,preset: freezed == preset ? _self.preset : preset // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,path: freezed == path ? _self.path : path // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,config: freezed == config ? _self.config : config // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<String, dynamic>?,siteId: null == siteId ? _self.siteId : siteId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Adds pattern-matching-related methods to [SnPublicationPage].
|
||||||
|
extension SnPublicationPagePatterns on SnPublicationPage {
|
||||||
|
/// A variant of `map` that fallback to returning `orElse`.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return orElse();
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnPublicationPage value)? $default,{required TResult orElse(),}){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnPublicationPage() when $default != null:
|
||||||
|
return $default(_that);case _:
|
||||||
|
return orElse();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A `switch`-like method, using callbacks.
|
||||||
|
///
|
||||||
|
/// Callbacks receives the raw object, upcasted.
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case final Subclass2 value:
|
||||||
|
/// return ...;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnPublicationPage value) $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnPublicationPage():
|
||||||
|
return $default(_that);}
|
||||||
|
}
|
||||||
|
/// A variant of `map` that fallback to returning `null`.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return null;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnPublicationPage value)? $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnPublicationPage() when $default != null:
|
||||||
|
return $default(_that);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A variant of `when` that fallback to an `orElse` callback.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return orElse();
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String? preset, String? path, Map<String, dynamic>? config, String siteId, DateTime createdAt, DateTime updatedAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnPublicationPage() when $default != null:
|
||||||
|
return $default(_that.id,_that.preset,_that.path,_that.config,_that.siteId,_that.createdAt,_that.updatedAt);case _:
|
||||||
|
return orElse();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A `switch`-like method, using callbacks.
|
||||||
|
///
|
||||||
|
/// As opposed to `map`, this offers destructuring.
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case Subclass2(:final field2):
|
||||||
|
/// return ...;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String? preset, String? path, Map<String, dynamic>? config, String siteId, DateTime createdAt, DateTime updatedAt) $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnPublicationPage():
|
||||||
|
return $default(_that.id,_that.preset,_that.path,_that.config,_that.siteId,_that.createdAt,_that.updatedAt);}
|
||||||
|
}
|
||||||
|
/// A variant of `when` that fallback to returning `null`
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return null;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String? preset, String? path, Map<String, dynamic>? config, String siteId, DateTime createdAt, DateTime updatedAt)? $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnPublicationPage() when $default != null:
|
||||||
|
return $default(_that.id,_that.preset,_that.path,_that.config,_that.siteId,_that.createdAt,_that.updatedAt);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
|
||||||
|
class _SnPublicationPage implements SnPublicationPage {
|
||||||
|
const _SnPublicationPage({required this.id, this.preset, this.path, final Map<String, dynamic>? config, required this.siteId, required this.createdAt, required this.updatedAt}): _config = config;
|
||||||
|
factory _SnPublicationPage.fromJson(Map<String, dynamic> json) => _$SnPublicationPageFromJson(json);
|
||||||
|
|
||||||
|
@override final String id;
|
||||||
|
@override final String? preset;
|
||||||
|
@override final String? path;
|
||||||
|
final Map<String, dynamic>? _config;
|
||||||
|
@override Map<String, dynamic>? get config {
|
||||||
|
final value = _config;
|
||||||
|
if (value == null) return null;
|
||||||
|
if (_config is EqualUnmodifiableMapView) return _config;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableMapView(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override final String siteId;
|
||||||
|
@override final DateTime createdAt;
|
||||||
|
@override final DateTime updatedAt;
|
||||||
|
|
||||||
|
/// Create a copy of SnPublicationPage
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$SnPublicationPageCopyWith<_SnPublicationPage> get copyWith => __$SnPublicationPageCopyWithImpl<_SnPublicationPage>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$SnPublicationPageToJson(this, );
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPublicationPage&&(identical(other.id, id) || other.id == id)&&(identical(other.preset, preset) || other.preset == preset)&&(identical(other.path, path) || other.path == path)&&const DeepCollectionEquality().equals(other._config, _config)&&(identical(other.siteId, siteId) || other.siteId == siteId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,id,preset,path,const DeepCollectionEquality().hash(_config),siteId,createdAt,updatedAt);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SnPublicationPage(id: $id, preset: $preset, path: $path, config: $config, siteId: $siteId, createdAt: $createdAt, updatedAt: $updatedAt)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class _$SnPublicationPageCopyWith<$Res> implements $SnPublicationPageCopyWith<$Res> {
|
||||||
|
factory _$SnPublicationPageCopyWith(_SnPublicationPage value, $Res Function(_SnPublicationPage) _then) = __$SnPublicationPageCopyWithImpl;
|
||||||
|
@override @useResult
|
||||||
|
$Res call({
|
||||||
|
String id, String? preset, String? path, Map<String, dynamic>? config, String siteId, DateTime createdAt, DateTime updatedAt
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class __$SnPublicationPageCopyWithImpl<$Res>
|
||||||
|
implements _$SnPublicationPageCopyWith<$Res> {
|
||||||
|
__$SnPublicationPageCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final _SnPublicationPage _self;
|
||||||
|
final $Res Function(_SnPublicationPage) _then;
|
||||||
|
|
||||||
|
/// Create a copy of SnPublicationPage
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? preset = freezed,Object? path = freezed,Object? config = freezed,Object? siteId = null,Object? createdAt = null,Object? updatedAt = null,}) {
|
||||||
|
return _then(_SnPublicationPage(
|
||||||
|
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,preset: freezed == preset ? _self.preset : preset // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,path: freezed == path ? _self.path : path // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,config: freezed == config ? _self._config : config // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<String, dynamic>?,siteId: null == siteId ? _self.siteId : siteId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// dart format on
|
||||||
60
lib/models/publication_site.g.dart
Normal file
60
lib/models/publication_site.g.dart
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'publication_site.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
_SnPublicationSite _$SnPublicationSiteFromJson(Map<String, dynamic> json) =>
|
||||||
|
_SnPublicationSite(
|
||||||
|
id: json['id'] as String,
|
||||||
|
slug: json['slug'] as String,
|
||||||
|
name: json['name'] as String,
|
||||||
|
description: json['description'] as String?,
|
||||||
|
mode: (json['mode'] as num?)?.toInt(),
|
||||||
|
publisherId: json['publisher_id'] as String,
|
||||||
|
accountId: json['account_id'] as String,
|
||||||
|
createdAt: DateTime.parse(json['created_at'] as String),
|
||||||
|
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||||
|
pages:
|
||||||
|
(json['pages'] as List<dynamic>)
|
||||||
|
.map((e) => SnPublicationPage.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$SnPublicationSiteToJson(_SnPublicationSite instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'slug': instance.slug,
|
||||||
|
'name': instance.name,
|
||||||
|
'description': instance.description,
|
||||||
|
'mode': instance.mode,
|
||||||
|
'publisher_id': instance.publisherId,
|
||||||
|
'account_id': instance.accountId,
|
||||||
|
'created_at': instance.createdAt.toIso8601String(),
|
||||||
|
'updated_at': instance.updatedAt.toIso8601String(),
|
||||||
|
'pages': instance.pages.map((e) => e.toJson()).toList(),
|
||||||
|
};
|
||||||
|
|
||||||
|
_SnPublicationPage _$SnPublicationPageFromJson(Map<String, dynamic> json) =>
|
||||||
|
_SnPublicationPage(
|
||||||
|
id: json['id'] as String,
|
||||||
|
preset: json['preset'] as String?,
|
||||||
|
path: json['path'] as String?,
|
||||||
|
config: json['config'] as Map<String, dynamic>?,
|
||||||
|
siteId: json['site_id'] as String,
|
||||||
|
createdAt: DateTime.parse(json['created_at'] as String),
|
||||||
|
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$SnPublicationPageToJson(_SnPublicationPage instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'preset': instance.preset,
|
||||||
|
'path': instance.path,
|
||||||
|
'config': instance.config,
|
||||||
|
'site_id': instance.siteId,
|
||||||
|
'created_at': instance.createdAt.toIso8601String(),
|
||||||
|
'updated_at': instance.updatedAt.toIso8601String(),
|
||||||
|
};
|
||||||
23
lib/models/reference.dart
Normal file
23
lib/models/reference.dart
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
import 'package:island/models/file.dart';
|
||||||
|
|
||||||
|
part 'reference.freezed.dart';
|
||||||
|
part 'reference.g.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
sealed class Reference with _$Reference {
|
||||||
|
const factory Reference({
|
||||||
|
required String id,
|
||||||
|
@JsonKey(name: 'file_id') required String fileId,
|
||||||
|
SnCloudFile? file,
|
||||||
|
required String usage,
|
||||||
|
@JsonKey(name: 'resource_id') required String resourceId,
|
||||||
|
@JsonKey(name: 'expired_at') DateTime? expiredAt,
|
||||||
|
@JsonKey(name: 'created_at') required DateTime createdAt,
|
||||||
|
@JsonKey(name: 'updated_at') required DateTime updatedAt,
|
||||||
|
@JsonKey(name: 'deleted_at') DateTime? deletedAt,
|
||||||
|
}) = _Reference;
|
||||||
|
|
||||||
|
factory Reference.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$ReferenceFromJson(json);
|
||||||
|
}
|
||||||
319
lib/models/reference.freezed.dart
Normal file
319
lib/models/reference.freezed.dart
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// coverage:ignore-file
|
||||||
|
// 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 'reference.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// FreezedGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// dart format off
|
||||||
|
T _$identity<T>(T value) => value;
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$Reference {
|
||||||
|
|
||||||
|
String get id;@JsonKey(name: 'file_id') String get fileId; SnCloudFile? get file; String get usage;@JsonKey(name: 'resource_id') String get resourceId;@JsonKey(name: 'expired_at') DateTime? get expiredAt;@JsonKey(name: 'created_at') DateTime get createdAt;@JsonKey(name: 'updated_at') DateTime get updatedAt;@JsonKey(name: 'deleted_at') DateTime? get deletedAt;
|
||||||
|
/// Create a copy of Reference
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$ReferenceCopyWith<Reference> get copyWith => _$ReferenceCopyWithImpl<Reference>(this as Reference, _$identity);
|
||||||
|
|
||||||
|
/// Serializes this Reference to a JSON map.
|
||||||
|
Map<String, dynamic> toJson();
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is Reference&&(identical(other.id, id) || other.id == id)&&(identical(other.fileId, fileId) || other.fileId == fileId)&&(identical(other.file, file) || other.file == file)&&(identical(other.usage, usage) || other.usage == usage)&&(identical(other.resourceId, resourceId) || other.resourceId == resourceId)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,id,fileId,file,usage,resourceId,expiredAt,createdAt,updatedAt,deletedAt);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'Reference(id: $id, fileId: $fileId, file: $file, usage: $usage, resourceId: $resourceId, expiredAt: $expiredAt, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class $ReferenceCopyWith<$Res> {
|
||||||
|
factory $ReferenceCopyWith(Reference value, $Res Function(Reference) _then) = _$ReferenceCopyWithImpl;
|
||||||
|
@useResult
|
||||||
|
$Res call({
|
||||||
|
String id,@JsonKey(name: 'file_id') String fileId, SnCloudFile? file, String usage,@JsonKey(name: 'resource_id') String resourceId,@JsonKey(name: 'expired_at') DateTime? expiredAt,@JsonKey(name: 'created_at') DateTime createdAt,@JsonKey(name: 'updated_at') DateTime updatedAt,@JsonKey(name: 'deleted_at') DateTime? deletedAt
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
$SnCloudFileCopyWith<$Res>? get file;
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class _$ReferenceCopyWithImpl<$Res>
|
||||||
|
implements $ReferenceCopyWith<$Res> {
|
||||||
|
_$ReferenceCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final Reference _self;
|
||||||
|
final $Res Function(Reference) _then;
|
||||||
|
|
||||||
|
/// Create a copy of Reference
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? fileId = null,Object? file = freezed,Object? usage = null,Object? resourceId = null,Object? expiredAt = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||||
|
return _then(_self.copyWith(
|
||||||
|
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,fileId: null == fileId ? _self.fileId : fileId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,file: freezed == file ? _self.file : file // ignore: cast_nullable_to_non_nullable
|
||||||
|
as SnCloudFile?,usage: null == usage ? _self.usage : usage // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,resourceId: null == resourceId ? _self.resourceId : resourceId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
/// Create a copy of Reference
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnCloudFileCopyWith<$Res>? get file {
|
||||||
|
if (_self.file == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $SnCloudFileCopyWith<$Res>(_self.file!, (value) {
|
||||||
|
return _then(_self.copyWith(file: value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Adds pattern-matching-related methods to [Reference].
|
||||||
|
extension ReferencePatterns on Reference {
|
||||||
|
/// A variant of `map` that fallback to returning `orElse`.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return orElse();
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _Reference value)? $default,{required TResult orElse(),}){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _Reference() when $default != null:
|
||||||
|
return $default(_that);case _:
|
||||||
|
return orElse();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A `switch`-like method, using callbacks.
|
||||||
|
///
|
||||||
|
/// Callbacks receives the raw object, upcasted.
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case final Subclass2 value:
|
||||||
|
/// return ...;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _Reference value) $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _Reference():
|
||||||
|
return $default(_that);}
|
||||||
|
}
|
||||||
|
/// A variant of `map` that fallback to returning `null`.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return null;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _Reference value)? $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _Reference() when $default != null:
|
||||||
|
return $default(_that);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A variant of `when` that fallback to an `orElse` callback.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return orElse();
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, @JsonKey(name: 'file_id') String fileId, SnCloudFile? file, String usage, @JsonKey(name: 'resource_id') String resourceId, @JsonKey(name: 'expired_at') DateTime? expiredAt, @JsonKey(name: 'created_at') DateTime createdAt, @JsonKey(name: 'updated_at') DateTime updatedAt, @JsonKey(name: 'deleted_at') DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _Reference() when $default != null:
|
||||||
|
return $default(_that.id,_that.fileId,_that.file,_that.usage,_that.resourceId,_that.expiredAt,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||||
|
return orElse();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A `switch`-like method, using callbacks.
|
||||||
|
///
|
||||||
|
/// As opposed to `map`, this offers destructuring.
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case Subclass2(:final field2):
|
||||||
|
/// return ...;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, @JsonKey(name: 'file_id') String fileId, SnCloudFile? file, String usage, @JsonKey(name: 'resource_id') String resourceId, @JsonKey(name: 'expired_at') DateTime? expiredAt, @JsonKey(name: 'created_at') DateTime createdAt, @JsonKey(name: 'updated_at') DateTime updatedAt, @JsonKey(name: 'deleted_at') DateTime? deletedAt) $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _Reference():
|
||||||
|
return $default(_that.id,_that.fileId,_that.file,_that.usage,_that.resourceId,_that.expiredAt,_that.createdAt,_that.updatedAt,_that.deletedAt);}
|
||||||
|
}
|
||||||
|
/// A variant of `when` that fallback to returning `null`
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return null;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, @JsonKey(name: 'file_id') String fileId, SnCloudFile? file, String usage, @JsonKey(name: 'resource_id') String resourceId, @JsonKey(name: 'expired_at') DateTime? expiredAt, @JsonKey(name: 'created_at') DateTime createdAt, @JsonKey(name: 'updated_at') DateTime updatedAt, @JsonKey(name: 'deleted_at') DateTime? deletedAt)? $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _Reference() when $default != null:
|
||||||
|
return $default(_that.id,_that.fileId,_that.file,_that.usage,_that.resourceId,_that.expiredAt,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
|
||||||
|
class _Reference implements Reference {
|
||||||
|
const _Reference({required this.id, @JsonKey(name: 'file_id') required this.fileId, this.file, required this.usage, @JsonKey(name: 'resource_id') required this.resourceId, @JsonKey(name: 'expired_at') this.expiredAt, @JsonKey(name: 'created_at') required this.createdAt, @JsonKey(name: 'updated_at') required this.updatedAt, @JsonKey(name: 'deleted_at') this.deletedAt});
|
||||||
|
factory _Reference.fromJson(Map<String, dynamic> json) => _$ReferenceFromJson(json);
|
||||||
|
|
||||||
|
@override final String id;
|
||||||
|
@override@JsonKey(name: 'file_id') final String fileId;
|
||||||
|
@override final SnCloudFile? file;
|
||||||
|
@override final String usage;
|
||||||
|
@override@JsonKey(name: 'resource_id') final String resourceId;
|
||||||
|
@override@JsonKey(name: 'expired_at') final DateTime? expiredAt;
|
||||||
|
@override@JsonKey(name: 'created_at') final DateTime createdAt;
|
||||||
|
@override@JsonKey(name: 'updated_at') final DateTime updatedAt;
|
||||||
|
@override@JsonKey(name: 'deleted_at') final DateTime? deletedAt;
|
||||||
|
|
||||||
|
/// Create a copy of Reference
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$ReferenceCopyWith<_Reference> get copyWith => __$ReferenceCopyWithImpl<_Reference>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$ReferenceToJson(this, );
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is _Reference&&(identical(other.id, id) || other.id == id)&&(identical(other.fileId, fileId) || other.fileId == fileId)&&(identical(other.file, file) || other.file == file)&&(identical(other.usage, usage) || other.usage == usage)&&(identical(other.resourceId, resourceId) || other.resourceId == resourceId)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,id,fileId,file,usage,resourceId,expiredAt,createdAt,updatedAt,deletedAt);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'Reference(id: $id, fileId: $fileId, file: $file, usage: $usage, resourceId: $resourceId, expiredAt: $expiredAt, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class _$ReferenceCopyWith<$Res> implements $ReferenceCopyWith<$Res> {
|
||||||
|
factory _$ReferenceCopyWith(_Reference value, $Res Function(_Reference) _then) = __$ReferenceCopyWithImpl;
|
||||||
|
@override @useResult
|
||||||
|
$Res call({
|
||||||
|
String id,@JsonKey(name: 'file_id') String fileId, SnCloudFile? file, String usage,@JsonKey(name: 'resource_id') String resourceId,@JsonKey(name: 'expired_at') DateTime? expiredAt,@JsonKey(name: 'created_at') DateTime createdAt,@JsonKey(name: 'updated_at') DateTime updatedAt,@JsonKey(name: 'deleted_at') DateTime? deletedAt
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
@override $SnCloudFileCopyWith<$Res>? get file;
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class __$ReferenceCopyWithImpl<$Res>
|
||||||
|
implements _$ReferenceCopyWith<$Res> {
|
||||||
|
__$ReferenceCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final _Reference _self;
|
||||||
|
final $Res Function(_Reference) _then;
|
||||||
|
|
||||||
|
/// Create a copy of Reference
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? fileId = null,Object? file = freezed,Object? usage = null,Object? resourceId = null,Object? expiredAt = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||||
|
return _then(_Reference(
|
||||||
|
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,fileId: null == fileId ? _self.fileId : fileId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,file: freezed == file ? _self.file : file // ignore: cast_nullable_to_non_nullable
|
||||||
|
as SnCloudFile?,usage: null == usage ? _self.usage : usage // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,resourceId: null == resourceId ? _self.resourceId : resourceId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a copy of Reference
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnCloudFileCopyWith<$Res>? get file {
|
||||||
|
if (_self.file == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $SnCloudFileCopyWith<$Res>(_self.file!, (value) {
|
||||||
|
return _then(_self.copyWith(file: value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// dart format on
|
||||||
41
lib/models/reference.g.dart
Normal file
41
lib/models/reference.g.dart
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'reference.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
_Reference _$ReferenceFromJson(Map<String, dynamic> json) => _Reference(
|
||||||
|
id: json['id'] as String,
|
||||||
|
fileId: json['file_id'] as String,
|
||||||
|
file:
|
||||||
|
json['file'] == null
|
||||||
|
? null
|
||||||
|
: SnCloudFile.fromJson(json['file'] as Map<String, dynamic>),
|
||||||
|
usage: json['usage'] as String,
|
||||||
|
resourceId: json['resource_id'] as String,
|
||||||
|
expiredAt:
|
||||||
|
json['expired_at'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['expired_at'] as String),
|
||||||
|
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),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$ReferenceToJson(_Reference instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'file_id': instance.fileId,
|
||||||
|
'file': instance.file?.toJson(),
|
||||||
|
'usage': instance.usage,
|
||||||
|
'resource_id': instance.resourceId,
|
||||||
|
'expired_at': instance.expiredAt?.toIso8601String(),
|
||||||
|
'created_at': instance.createdAt.toIso8601String(),
|
||||||
|
'updated_at': instance.updatedAt.toIso8601String(),
|
||||||
|
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||||
|
};
|
||||||
25
lib/models/site_file.dart
Normal file
25
lib/models/site_file.dart
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'site_file.freezed.dart';
|
||||||
|
part 'site_file.g.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
sealed class SnSiteFileEntry with _$SnSiteFileEntry {
|
||||||
|
const factory SnSiteFileEntry({
|
||||||
|
required bool isDirectory,
|
||||||
|
required String relativePath,
|
||||||
|
required int size, // Size in bytes (0 for directories)
|
||||||
|
required DateTime modified, // ISO 8601 timestamp
|
||||||
|
}) = _SnSiteFileEntry;
|
||||||
|
|
||||||
|
factory SnSiteFileEntry.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SnSiteFileEntryFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
sealed class SnFileContent with _$SnFileContent {
|
||||||
|
const factory SnFileContent({required String content}) = _SnFileContent;
|
||||||
|
|
||||||
|
factory SnFileContent.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SnFileContentFromJson(json);
|
||||||
|
}
|
||||||
539
lib/models/site_file.freezed.dart
Normal file
539
lib/models/site_file.freezed.dart
Normal file
@@ -0,0 +1,539 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// coverage:ignore-file
|
||||||
|
// 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 'site_file.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// FreezedGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// dart format off
|
||||||
|
T _$identity<T>(T value) => value;
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$SnSiteFileEntry {
|
||||||
|
|
||||||
|
bool get isDirectory; String get relativePath; int get size;// Size in bytes (0 for directories)
|
||||||
|
DateTime get modified;
|
||||||
|
/// Create a copy of SnSiteFileEntry
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnSiteFileEntryCopyWith<SnSiteFileEntry> get copyWith => _$SnSiteFileEntryCopyWithImpl<SnSiteFileEntry>(this as SnSiteFileEntry, _$identity);
|
||||||
|
|
||||||
|
/// Serializes this SnSiteFileEntry to a JSON map.
|
||||||
|
Map<String, dynamic> toJson();
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnSiteFileEntry&&(identical(other.isDirectory, isDirectory) || other.isDirectory == isDirectory)&&(identical(other.relativePath, relativePath) || other.relativePath == relativePath)&&(identical(other.size, size) || other.size == size)&&(identical(other.modified, modified) || other.modified == modified));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,isDirectory,relativePath,size,modified);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SnSiteFileEntry(isDirectory: $isDirectory, relativePath: $relativePath, size: $size, modified: $modified)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class $SnSiteFileEntryCopyWith<$Res> {
|
||||||
|
factory $SnSiteFileEntryCopyWith(SnSiteFileEntry value, $Res Function(SnSiteFileEntry) _then) = _$SnSiteFileEntryCopyWithImpl;
|
||||||
|
@useResult
|
||||||
|
$Res call({
|
||||||
|
bool isDirectory, String relativePath, int size, DateTime modified
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class _$SnSiteFileEntryCopyWithImpl<$Res>
|
||||||
|
implements $SnSiteFileEntryCopyWith<$Res> {
|
||||||
|
_$SnSiteFileEntryCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final SnSiteFileEntry _self;
|
||||||
|
final $Res Function(SnSiteFileEntry) _then;
|
||||||
|
|
||||||
|
/// Create a copy of SnSiteFileEntry
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline') @override $Res call({Object? isDirectory = null,Object? relativePath = null,Object? size = null,Object? modified = null,}) {
|
||||||
|
return _then(_self.copyWith(
|
||||||
|
isDirectory: null == isDirectory ? _self.isDirectory : isDirectory // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,relativePath: null == relativePath ? _self.relativePath : relativePath // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,size: null == size ? _self.size : size // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,modified: null == modified ? _self.modified : modified // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Adds pattern-matching-related methods to [SnSiteFileEntry].
|
||||||
|
extension SnSiteFileEntryPatterns on SnSiteFileEntry {
|
||||||
|
/// A variant of `map` that fallback to returning `orElse`.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return orElse();
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnSiteFileEntry value)? $default,{required TResult orElse(),}){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnSiteFileEntry() when $default != null:
|
||||||
|
return $default(_that);case _:
|
||||||
|
return orElse();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A `switch`-like method, using callbacks.
|
||||||
|
///
|
||||||
|
/// Callbacks receives the raw object, upcasted.
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case final Subclass2 value:
|
||||||
|
/// return ...;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnSiteFileEntry value) $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnSiteFileEntry():
|
||||||
|
return $default(_that);}
|
||||||
|
}
|
||||||
|
/// A variant of `map` that fallback to returning `null`.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return null;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnSiteFileEntry value)? $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnSiteFileEntry() when $default != null:
|
||||||
|
return $default(_that);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A variant of `when` that fallback to an `orElse` callback.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return orElse();
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool isDirectory, String relativePath, int size, DateTime modified)? $default,{required TResult orElse(),}) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnSiteFileEntry() when $default != null:
|
||||||
|
return $default(_that.isDirectory,_that.relativePath,_that.size,_that.modified);case _:
|
||||||
|
return orElse();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A `switch`-like method, using callbacks.
|
||||||
|
///
|
||||||
|
/// As opposed to `map`, this offers destructuring.
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case Subclass2(:final field2):
|
||||||
|
/// return ...;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool isDirectory, String relativePath, int size, DateTime modified) $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnSiteFileEntry():
|
||||||
|
return $default(_that.isDirectory,_that.relativePath,_that.size,_that.modified);}
|
||||||
|
}
|
||||||
|
/// A variant of `when` that fallback to returning `null`
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return null;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool isDirectory, String relativePath, int size, DateTime modified)? $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnSiteFileEntry() when $default != null:
|
||||||
|
return $default(_that.isDirectory,_that.relativePath,_that.size,_that.modified);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
|
||||||
|
class _SnSiteFileEntry implements SnSiteFileEntry {
|
||||||
|
const _SnSiteFileEntry({required this.isDirectory, required this.relativePath, required this.size, required this.modified});
|
||||||
|
factory _SnSiteFileEntry.fromJson(Map<String, dynamic> json) => _$SnSiteFileEntryFromJson(json);
|
||||||
|
|
||||||
|
@override final bool isDirectory;
|
||||||
|
@override final String relativePath;
|
||||||
|
@override final int size;
|
||||||
|
// Size in bytes (0 for directories)
|
||||||
|
@override final DateTime modified;
|
||||||
|
|
||||||
|
/// Create a copy of SnSiteFileEntry
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$SnSiteFileEntryCopyWith<_SnSiteFileEntry> get copyWith => __$SnSiteFileEntryCopyWithImpl<_SnSiteFileEntry>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$SnSiteFileEntryToJson(this, );
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnSiteFileEntry&&(identical(other.isDirectory, isDirectory) || other.isDirectory == isDirectory)&&(identical(other.relativePath, relativePath) || other.relativePath == relativePath)&&(identical(other.size, size) || other.size == size)&&(identical(other.modified, modified) || other.modified == modified));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,isDirectory,relativePath,size,modified);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SnSiteFileEntry(isDirectory: $isDirectory, relativePath: $relativePath, size: $size, modified: $modified)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class _$SnSiteFileEntryCopyWith<$Res> implements $SnSiteFileEntryCopyWith<$Res> {
|
||||||
|
factory _$SnSiteFileEntryCopyWith(_SnSiteFileEntry value, $Res Function(_SnSiteFileEntry) _then) = __$SnSiteFileEntryCopyWithImpl;
|
||||||
|
@override @useResult
|
||||||
|
$Res call({
|
||||||
|
bool isDirectory, String relativePath, int size, DateTime modified
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class __$SnSiteFileEntryCopyWithImpl<$Res>
|
||||||
|
implements _$SnSiteFileEntryCopyWith<$Res> {
|
||||||
|
__$SnSiteFileEntryCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final _SnSiteFileEntry _self;
|
||||||
|
final $Res Function(_SnSiteFileEntry) _then;
|
||||||
|
|
||||||
|
/// Create a copy of SnSiteFileEntry
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @pragma('vm:prefer-inline') $Res call({Object? isDirectory = null,Object? relativePath = null,Object? size = null,Object? modified = null,}) {
|
||||||
|
return _then(_SnSiteFileEntry(
|
||||||
|
isDirectory: null == isDirectory ? _self.isDirectory : isDirectory // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,relativePath: null == relativePath ? _self.relativePath : relativePath // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,size: null == size ? _self.size : size // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,modified: null == modified ? _self.modified : modified // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$SnFileContent {
|
||||||
|
|
||||||
|
String get content;
|
||||||
|
/// Create a copy of SnFileContent
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnFileContentCopyWith<SnFileContent> get copyWith => _$SnFileContentCopyWithImpl<SnFileContent>(this as SnFileContent, _$identity);
|
||||||
|
|
||||||
|
/// Serializes this SnFileContent to a JSON map.
|
||||||
|
Map<String, dynamic> toJson();
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnFileContent&&(identical(other.content, content) || other.content == content));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,content);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SnFileContent(content: $content)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class $SnFileContentCopyWith<$Res> {
|
||||||
|
factory $SnFileContentCopyWith(SnFileContent value, $Res Function(SnFileContent) _then) = _$SnFileContentCopyWithImpl;
|
||||||
|
@useResult
|
||||||
|
$Res call({
|
||||||
|
String content
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class _$SnFileContentCopyWithImpl<$Res>
|
||||||
|
implements $SnFileContentCopyWith<$Res> {
|
||||||
|
_$SnFileContentCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final SnFileContent _self;
|
||||||
|
final $Res Function(SnFileContent) _then;
|
||||||
|
|
||||||
|
/// Create a copy of SnFileContent
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline') @override $Res call({Object? content = null,}) {
|
||||||
|
return _then(_self.copyWith(
|
||||||
|
content: null == content ? _self.content : content // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Adds pattern-matching-related methods to [SnFileContent].
|
||||||
|
extension SnFileContentPatterns on SnFileContent {
|
||||||
|
/// A variant of `map` that fallback to returning `orElse`.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return orElse();
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnFileContent value)? $default,{required TResult orElse(),}){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnFileContent() when $default != null:
|
||||||
|
return $default(_that);case _:
|
||||||
|
return orElse();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A `switch`-like method, using callbacks.
|
||||||
|
///
|
||||||
|
/// Callbacks receives the raw object, upcasted.
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case final Subclass2 value:
|
||||||
|
/// return ...;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnFileContent value) $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnFileContent():
|
||||||
|
return $default(_that);}
|
||||||
|
}
|
||||||
|
/// A variant of `map` that fallback to returning `null`.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return null;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnFileContent value)? $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnFileContent() when $default != null:
|
||||||
|
return $default(_that);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A variant of `when` that fallback to an `orElse` callback.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return orElse();
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String content)? $default,{required TResult orElse(),}) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnFileContent() when $default != null:
|
||||||
|
return $default(_that.content);case _:
|
||||||
|
return orElse();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A `switch`-like method, using callbacks.
|
||||||
|
///
|
||||||
|
/// As opposed to `map`, this offers destructuring.
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case Subclass2(:final field2):
|
||||||
|
/// return ...;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String content) $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnFileContent():
|
||||||
|
return $default(_that.content);}
|
||||||
|
}
|
||||||
|
/// A variant of `when` that fallback to returning `null`
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return null;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String content)? $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnFileContent() when $default != null:
|
||||||
|
return $default(_that.content);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
|
||||||
|
class _SnFileContent implements SnFileContent {
|
||||||
|
const _SnFileContent({required this.content});
|
||||||
|
factory _SnFileContent.fromJson(Map<String, dynamic> json) => _$SnFileContentFromJson(json);
|
||||||
|
|
||||||
|
@override final String content;
|
||||||
|
|
||||||
|
/// Create a copy of SnFileContent
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$SnFileContentCopyWith<_SnFileContent> get copyWith => __$SnFileContentCopyWithImpl<_SnFileContent>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$SnFileContentToJson(this, );
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnFileContent&&(identical(other.content, content) || other.content == content));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,content);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SnFileContent(content: $content)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class _$SnFileContentCopyWith<$Res> implements $SnFileContentCopyWith<$Res> {
|
||||||
|
factory _$SnFileContentCopyWith(_SnFileContent value, $Res Function(_SnFileContent) _then) = __$SnFileContentCopyWithImpl;
|
||||||
|
@override @useResult
|
||||||
|
$Res call({
|
||||||
|
String content
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class __$SnFileContentCopyWithImpl<$Res>
|
||||||
|
implements _$SnFileContentCopyWith<$Res> {
|
||||||
|
__$SnFileContentCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final _SnFileContent _self;
|
||||||
|
final $Res Function(_SnFileContent) _then;
|
||||||
|
|
||||||
|
/// Create a copy of SnFileContent
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @pragma('vm:prefer-inline') $Res call({Object? content = null,}) {
|
||||||
|
return _then(_SnFileContent(
|
||||||
|
content: null == content ? _self.content : content // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// dart format on
|
||||||
29
lib/models/site_file.g.dart
Normal file
29
lib/models/site_file.g.dart
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'site_file.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
_SnSiteFileEntry _$SnSiteFileEntryFromJson(Map<String, dynamic> json) =>
|
||||||
|
_SnSiteFileEntry(
|
||||||
|
isDirectory: json['is_directory'] as bool,
|
||||||
|
relativePath: json['relative_path'] as String,
|
||||||
|
size: (json['size'] as num).toInt(),
|
||||||
|
modified: DateTime.parse(json['modified'] as String),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$SnSiteFileEntryToJson(_SnSiteFileEntry instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'is_directory': instance.isDirectory,
|
||||||
|
'relative_path': instance.relativePath,
|
||||||
|
'size': instance.size,
|
||||||
|
'modified': instance.modified.toIso8601String(),
|
||||||
|
};
|
||||||
|
|
||||||
|
_SnFileContent _$SnFileContentFromJson(Map<String, dynamic> json) =>
|
||||||
|
_SnFileContent(content: json['content'] as String);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$SnFileContentToJson(_SnFileContent instance) =>
|
||||||
|
<String, dynamic>{'content': instance.content};
|
||||||
@@ -212,8 +212,14 @@ class CallNotifier extends _$CallNotifier {
|
|||||||
String? _roomId;
|
String? _roomId;
|
||||||
String? get roomId => _roomId;
|
String? get roomId => _roomId;
|
||||||
|
|
||||||
Future<void> joinRoom(String roomId) async {
|
SnChatRoom? _chatRoom;
|
||||||
if (_roomId == roomId && _room != null) {
|
SnChatRoom? get chatRoom => _chatRoom;
|
||||||
|
|
||||||
|
Future<void> joinRoom(SnChatRoom room) async {
|
||||||
|
var roomId = room.id;
|
||||||
|
if (_roomId == roomId &&
|
||||||
|
_room != null &&
|
||||||
|
_room?.connectionState == lk.ConnectionState.connected) {
|
||||||
talker.info('[Call] Call skipped. Already has data');
|
talker.info('[Call] Call skipped. Already has data');
|
||||||
return;
|
return;
|
||||||
} else if (_room != null) {
|
} else if (_room != null) {
|
||||||
@@ -223,6 +229,7 @@ class CallNotifier extends _$CallNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
_roomId = roomId;
|
_roomId = roomId;
|
||||||
|
_chatRoom = room;
|
||||||
if (_room != null) {
|
if (_room != null) {
|
||||||
await _room!.disconnect();
|
await _room!.disconnect();
|
||||||
await _room!.dispose();
|
await _room!.dispose();
|
||||||
@@ -355,6 +362,7 @@ class CallNotifier extends _$CallNotifier {
|
|||||||
sourceId: source.id,
|
sourceId: source.id,
|
||||||
maxFrameRate: 30.0,
|
maxFrameRate: 30.0,
|
||||||
captureScreenAudio: true,
|
captureScreenAudio: true,
|
||||||
|
useiOSBroadcastExtension: true,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
await _localParticipant!.publishVideoTrack(track);
|
await _localParticipant!.publishVideoTrack(track);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ part of 'call.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$callNotifierHash() => r'a8ca3f625c0db3ad9992033ae70864ce15efc281';
|
String _$callNotifierHash() => r'ef4e3e9c9d411cf9dce1ceb456a3b866b2c87db3';
|
||||||
|
|
||||||
/// See also [CallNotifier].
|
/// See also [CallNotifier].
|
||||||
@ProviderFor(CallNotifier)
|
@ProviderFor(CallNotifier)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:math' as math;
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:island/models/chat.dart';
|
import 'package:island/models/chat.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
@@ -6,6 +8,58 @@ import 'package:island/pods/chat/chat_subscribe.dart';
|
|||||||
|
|
||||||
part 'chat_summary.g.dart';
|
part 'chat_summary.g.dart';
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
class ChatUnreadCountNotifier extends _$ChatUnreadCountNotifier {
|
||||||
|
StreamSubscription<WebSocketPacket>? _subscription;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<int> build() async {
|
||||||
|
// Subscribe to websocket events when this provider is built
|
||||||
|
_subscribeToWebSocket();
|
||||||
|
|
||||||
|
// Dispose the subscription when this provider is disposed
|
||||||
|
ref.onDispose(() {
|
||||||
|
_subscription?.cancel();
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final client = ref.read(apiClientProvider);
|
||||||
|
final response = await client.get('/sphere/chat/unread');
|
||||||
|
return (response.data as num).toInt();
|
||||||
|
} catch (_) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _subscribeToWebSocket() {
|
||||||
|
final webSocketService = ref.read(websocketProvider);
|
||||||
|
_subscription = webSocketService.dataStream.listen((packet) {
|
||||||
|
if (packet.type == 'messages.new' && packet.data != null) {
|
||||||
|
final message = SnChatMessage.fromJson(packet.data!);
|
||||||
|
final currentSubscribed = ref.read(currentSubscribedChatIdProvider);
|
||||||
|
// Only increment if the message is not from the currently subscribed chat
|
||||||
|
if (message.chatRoomId != currentSubscribed) {
|
||||||
|
_incrementCounter();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _incrementCounter() async {
|
||||||
|
final current = await future;
|
||||||
|
state = AsyncData(current + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> decrement(int count) async {
|
||||||
|
final current = await future;
|
||||||
|
state = AsyncData(math.max(current - count, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear() async {
|
||||||
|
state = AsyncData(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
class ChatSummary extends _$ChatSummary {
|
class ChatSummary extends _$ChatSummary {
|
||||||
@override
|
@override
|
||||||
@@ -41,6 +95,14 @@ class ChatSummary extends _$ChatSummary {
|
|||||||
state.whenData((summaries) {
|
state.whenData((summaries) {
|
||||||
final summary = summaries[chatId];
|
final summary = summaries[chatId];
|
||||||
if (summary != null) {
|
if (summary != null) {
|
||||||
|
// Decrement global unread count
|
||||||
|
final unreadToDecrement = summary.unreadCount;
|
||||||
|
if (unreadToDecrement > 0) {
|
||||||
|
ref
|
||||||
|
.read(chatUnreadCountNotifierProvider.notifier)
|
||||||
|
.decrement(unreadToDecrement);
|
||||||
|
}
|
||||||
|
|
||||||
state = AsyncData({
|
state = AsyncData({
|
||||||
...summaries,
|
...summaries,
|
||||||
chatId: SnChatSummary(
|
chatId: SnChatSummary(
|
||||||
|
|||||||
@@ -6,6 +6,24 @@ part of 'chat_summary.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
|
String _$chatUnreadCountNotifierHash() =>
|
||||||
|
r'b8d93589dc37f772d4c3a07d9afd81c37026e57d';
|
||||||
|
|
||||||
|
/// See also [ChatUnreadCountNotifier].
|
||||||
|
@ProviderFor(ChatUnreadCountNotifier)
|
||||||
|
final chatUnreadCountNotifierProvider =
|
||||||
|
AutoDisposeAsyncNotifierProvider<ChatUnreadCountNotifier, int>.internal(
|
||||||
|
ChatUnreadCountNotifier.new,
|
||||||
|
name: r'chatUnreadCountNotifierProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$chatUnreadCountNotifierHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef _$ChatUnreadCountNotifier = AutoDisposeAsyncNotifier<int>;
|
||||||
String _$chatSummaryHash() => r'33815a3bd81d20902b7063e8194fe336930df9b4';
|
String _$chatSummaryHash() => r'33815a3bd81d20902b7063e8194fe336930df9b4';
|
||||||
|
|
||||||
/// See also [ChatSummary].
|
/// See also [ChatSummary].
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import "package:flutter/material.dart";
|
|||||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||||
import "package:island/database/drift_db.dart";
|
import "package:island/database/drift_db.dart";
|
||||||
import "package:island/database/message.dart";
|
import "package:island/database/message.dart";
|
||||||
|
import "package:island/models/account.dart";
|
||||||
import "package:island/models/chat.dart";
|
import "package:island/models/chat.dart";
|
||||||
import "package:island/models/file.dart";
|
import "package:island/models/file.dart";
|
||||||
import "package:island/models/poll.dart";
|
import "package:island/models/poll.dart";
|
||||||
@@ -20,6 +21,7 @@ import "package:riverpod_annotation/riverpod_annotation.dart";
|
|||||||
import "package:uuid/uuid.dart";
|
import "package:uuid/uuid.dart";
|
||||||
import "package:island/screens/chat/chat.dart";
|
import "package:island/screens/chat/chat.dart";
|
||||||
import "package:island/pods/chat/chat_rooms.dart";
|
import "package:island/pods/chat/chat_rooms.dart";
|
||||||
|
import "package:island/screens/account/profile.dart";
|
||||||
|
|
||||||
part 'messages_notifier.g.dart';
|
part 'messages_notifier.g.dart';
|
||||||
|
|
||||||
@@ -43,8 +45,11 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
bool _isSyncing = false;
|
bool _isSyncing = false;
|
||||||
bool _isJumping = false;
|
bool _isJumping = false;
|
||||||
bool _isUpdatingState = false;
|
bool _isUpdatingState = false;
|
||||||
|
bool _allRemoteMessagesFetched = false;
|
||||||
DateTime? _lastPauseTime;
|
DateTime? _lastPauseTime;
|
||||||
|
|
||||||
|
late final Future<SnAccount?> Function(String) _fetchAccount;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<List<LocalChatMessage>> build(String roomId) async {
|
FutureOr<List<LocalChatMessage>> build(String roomId) async {
|
||||||
_roomId = roomId;
|
_roomId = roomId;
|
||||||
@@ -53,6 +58,15 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
final room = await ref.watch(chatroomProvider(roomId).future);
|
final room = await ref.watch(chatroomProvider(roomId).future);
|
||||||
final identity = await ref.watch(chatroomIdentityProvider(roomId).future);
|
final identity = await ref.watch(chatroomIdentityProvider(roomId).future);
|
||||||
|
|
||||||
|
// Initialize fetch account method for corrupted data recovery
|
||||||
|
_fetchAccount = (String accountId) async {
|
||||||
|
try {
|
||||||
|
return await ref.watch(accountProvider(accountId).future);
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (room == null) {
|
if (room == null) {
|
||||||
throw Exception('Room not found');
|
throw Exception('Room not found');
|
||||||
}
|
}
|
||||||
@@ -133,6 +147,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
_roomId,
|
_roomId,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
withAttachments: withAttachments,
|
withAttachments: withAttachments,
|
||||||
|
fetchAccount: _fetchAccount,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
final chatMessagesFromDb = await _database.getMessagesForRoom(
|
final chatMessagesFromDb = await _database.getMessagesForRoom(
|
||||||
@@ -140,8 +155,16 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
offset: offset,
|
offset: offset,
|
||||||
limit: take,
|
limit: take,
|
||||||
);
|
);
|
||||||
dbMessages =
|
dbMessages = await Future.wait(
|
||||||
chatMessagesFromDb.map(_database.companionToMessage).toList();
|
chatMessagesFromDb
|
||||||
|
.map(
|
||||||
|
(msg) => _database.companionToMessage(
|
||||||
|
msg,
|
||||||
|
fetchAccount: _fetchAccount,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<LocalChatMessage> filteredMessages = dbMessages;
|
List<LocalChatMessage> filteredMessages = dbMessages;
|
||||||
@@ -202,8 +225,14 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
offset: offset,
|
offset: offset,
|
||||||
limit: take,
|
limit: take,
|
||||||
);
|
);
|
||||||
final dbMessages =
|
final dbMessages = await Future.wait(
|
||||||
chatMessagesFromDb.map(_database.companionToMessage).toList();
|
chatMessagesFromDb
|
||||||
|
.map(
|
||||||
|
(msg) =>
|
||||||
|
_database.companionToMessage(msg, fetchAccount: _fetchAccount),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
|
||||||
// Always ensure unique messages to prevent duplicate keys
|
// Always ensure unique messages to prevent duplicate keys
|
||||||
final uniqueMessages = <LocalChatMessage>[];
|
final uniqueMessages = <LocalChatMessage>[];
|
||||||
@@ -250,6 +279,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (offset >= _totalCount!) {
|
if (offset >= _totalCount!) {
|
||||||
|
_allRemoteMessagesFetched = true;
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,7 +301,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
for (final message in messages) {
|
for (final message in messages) {
|
||||||
await _database.saveMessage(_database.messageToCompanion(message));
|
await _database.saveMessageWithSender(message);
|
||||||
if (message.nonce != null) {
|
if (message.nonce != null) {
|
||||||
_pendingMessages.removeWhere(
|
_pendingMessages.removeWhere(
|
||||||
(_, pendingMsg) => pendingMsg.nonce == message.nonce,
|
(_, pendingMsg) => pendingMsg.nonce == message.nonce,
|
||||||
@@ -279,6 +309,11 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if we've fetched all remote messages
|
||||||
|
if (offset + messages.length >= _totalCount!) {
|
||||||
|
_allRemoteMessagesFetched = true;
|
||||||
|
}
|
||||||
|
|
||||||
return messages;
|
return messages;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,6 +323,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_isSyncing = true;
|
_isSyncing = true;
|
||||||
|
_allRemoteMessagesFetched = false;
|
||||||
|
|
||||||
talker.log('Starting message sync');
|
talker.log('Starting message sync');
|
||||||
Future.microtask(() => ref.read(isSyncingProvider.notifier).state = true);
|
Future.microtask(() => ref.read(isSyncingProvider.notifier).state = true);
|
||||||
@@ -300,7 +336,10 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
final lastMessage =
|
final lastMessage =
|
||||||
dbMessages.isEmpty
|
dbMessages.isEmpty
|
||||||
? null
|
? null
|
||||||
: _database.companionToMessage(dbMessages.first);
|
: await _database.companionToMessage(
|
||||||
|
dbMessages.first,
|
||||||
|
fetchAccount: _fetchAccount,
|
||||||
|
);
|
||||||
|
|
||||||
if (lastMessage == null) {
|
if (lastMessage == null) {
|
||||||
talker.log('No local messages, fetching from network');
|
talker.log('No local messages, fetching from network');
|
||||||
@@ -312,19 +351,48 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final resp = await _apiClient.post(
|
// Sync with pagination support using timestamp-based cursor
|
||||||
'/sphere/chat/${_room.id}/sync',
|
int? totalMessages;
|
||||||
data: {
|
int syncedCount = 0;
|
||||||
'last_sync_timestamp':
|
int lastSyncTimestamp =
|
||||||
lastMessage.toRemoteMessage().updatedAt.millisecondsSinceEpoch,
|
lastMessage.toRemoteMessage().updatedAt.millisecondsSinceEpoch;
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
final response = MessageSyncResponse.fromJson(resp.data);
|
do {
|
||||||
talker.log('Sync response: ${response.messages.length} changes');
|
final resp = await _apiClient.post(
|
||||||
for (final message in response.messages) {
|
'/sphere/chat/${_room.id}/sync',
|
||||||
await receiveMessage(message);
|
data: {'last_sync_timestamp': lastSyncTimestamp},
|
||||||
}
|
);
|
||||||
|
|
||||||
|
// Read total count from header on first request
|
||||||
|
if (totalMessages == null) {
|
||||||
|
totalMessages = int.parse(
|
||||||
|
resp.headers['x-total']?.firstOrNull ?? '0',
|
||||||
|
);
|
||||||
|
talker.log('Total messages to sync: $totalMessages');
|
||||||
|
}
|
||||||
|
|
||||||
|
final response = MessageSyncResponse.fromJson(resp.data);
|
||||||
|
final messagesCount = response.messages.length;
|
||||||
|
talker.log(
|
||||||
|
'Sync page: synced=$syncedCount/$totalMessages, count=$messagesCount',
|
||||||
|
);
|
||||||
|
|
||||||
|
for (final message in response.messages) {
|
||||||
|
await receiveMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
syncedCount += messagesCount;
|
||||||
|
|
||||||
|
// Update cursor to the last message's createdAt for next page
|
||||||
|
if (response.messages.isNotEmpty) {
|
||||||
|
lastSyncTimestamp =
|
||||||
|
response.messages.last.createdAt.millisecondsSinceEpoch;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue if there are more messages to fetch
|
||||||
|
} while (syncedCount < totalMessages);
|
||||||
|
|
||||||
|
talker.log('Sync complete: synced $syncedCount messages');
|
||||||
} catch (err, stackTrace) {
|
} catch (err, stackTrace) {
|
||||||
talker.log(
|
talker.log(
|
||||||
'Error syncing messages',
|
'Error syncing messages',
|
||||||
@@ -363,14 +431,35 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
withAttachments: _withAttachments,
|
withAttachments: _withAttachments,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (localMessages.isNotEmpty) {
|
// If we have local messages AND we've fetched all remote messages, return local
|
||||||
|
if (localMessages.isNotEmpty && _allRemoteMessagesFetched) {
|
||||||
return localMessages;
|
return localMessages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we haven't fetched all remote messages, check remote even if we have local
|
||||||
|
// OR if we have no local messages at all
|
||||||
if (_searchQuery == null || _searchQuery!.isEmpty) {
|
if (_searchQuery == null || _searchQuery!.isEmpty) {
|
||||||
return await _fetchAndCacheMessages(offset: offset, take: take);
|
final remoteMessages = await _fetchAndCacheMessages(
|
||||||
|
offset: offset,
|
||||||
|
take: take,
|
||||||
|
);
|
||||||
|
|
||||||
|
// If we got remote messages, re-fetch from cache to get merged result
|
||||||
|
if (remoteMessages.isNotEmpty) {
|
||||||
|
return await _getCachedMessages(
|
||||||
|
offset: offset,
|
||||||
|
take: take,
|
||||||
|
searchQuery: _searchQuery,
|
||||||
|
withLinks: _withLinks,
|
||||||
|
withAttachments: _withAttachments,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// No remote messages, return local (if any)
|
||||||
|
return localMessages;
|
||||||
} else {
|
} else {
|
||||||
return []; // If searching, and no local messages, don't fetch from network
|
// For search queries, return local only
|
||||||
|
return localMessages;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
final localMessages = await _getCachedMessages(
|
final localMessages = await _getCachedMessages(
|
||||||
@@ -390,6 +479,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
|
|
||||||
Future<void> loadInitial() async {
|
Future<void> loadInitial() async {
|
||||||
talker.log('Loading initial messages');
|
talker.log('Loading initial messages');
|
||||||
|
_allRemoteMessagesFetched = false;
|
||||||
if (_searchQuery == null || _searchQuery!.isEmpty) {
|
if (_searchQuery == null || _searchQuery!.isEmpty) {
|
||||||
syncMessages();
|
syncMessages();
|
||||||
}
|
}
|
||||||
@@ -411,6 +501,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
if (!_hasMore || state is AsyncLoading) return;
|
if (!_hasMore || state is AsyncLoading) return;
|
||||||
talker.log('Loading more messages');
|
talker.log('Loading more messages');
|
||||||
|
|
||||||
|
Future.microtask(() => ref.read(isSyncingProvider.notifier).state = true);
|
||||||
try {
|
try {
|
||||||
final currentMessages = state.value ?? [];
|
final currentMessages = state.value ?? [];
|
||||||
final offset = currentMessages.length;
|
final offset = currentMessages.length;
|
||||||
@@ -432,6 +523,10 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
stackTrace: stackTrace,
|
stackTrace: stackTrace,
|
||||||
);
|
);
|
||||||
showErrorAlert(err);
|
showErrorAlert(err);
|
||||||
|
} finally {
|
||||||
|
Future.microtask(
|
||||||
|
() => ref.read(isSyncingProvider.notifier).state = false,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -467,7 +562,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
|
|
||||||
_pendingMessages[localMessage.id] = localMessage;
|
_pendingMessages[localMessage.id] = localMessage;
|
||||||
_fileUploadProgress[localMessage.id] = {};
|
_fileUploadProgress[localMessage.id] = {};
|
||||||
await _database.saveMessage(_database.messageToCompanion(localMessage));
|
await _database.saveMessageWithSender(localMessage);
|
||||||
|
|
||||||
final currentMessages = state.value ?? [];
|
final currentMessages = state.value ?? [];
|
||||||
state = AsyncValue.data([localMessage, ...currentMessages]);
|
state = AsyncValue.data([localMessage, ...currentMessages]);
|
||||||
@@ -518,7 +613,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
|
|
||||||
_pendingMessages.remove(localMessage.id);
|
_pendingMessages.remove(localMessage.id);
|
||||||
await _database.deleteMessage(localMessage.id);
|
await _database.deleteMessage(localMessage.id);
|
||||||
await _database.saveMessage(_database.messageToCompanion(updatedMessage));
|
await _database.saveMessageWithSender(updatedMessage);
|
||||||
|
|
||||||
final currentMessages = state.value ?? [];
|
final currentMessages = state.value ?? [];
|
||||||
if (editingTo != null) {
|
if (editingTo != null) {
|
||||||
@@ -600,7 +695,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
|
|
||||||
_pendingMessages.remove(pendingMessageId);
|
_pendingMessages.remove(pendingMessageId);
|
||||||
await _database.deleteMessage(pendingMessageId);
|
await _database.deleteMessage(pendingMessageId);
|
||||||
await _database.saveMessage(_database.messageToCompanion(updatedMessage));
|
await _database.saveMessageWithSender(updatedMessage);
|
||||||
|
|
||||||
final newMessages =
|
final newMessages =
|
||||||
(state.value ?? []).map((m) {
|
(state.value ?? []).map((m) {
|
||||||
@@ -657,7 +752,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await _database.saveMessage(_database.messageToCompanion(localMessage));
|
await _database.saveMessageWithSender(localMessage);
|
||||||
|
|
||||||
final currentMessages = state.value ?? [];
|
final currentMessages = state.value ?? [];
|
||||||
final existingIndex = currentMessages.indexWhere(
|
final existingIndex = currentMessages.indexWhere(
|
||||||
@@ -754,7 +849,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
messageToUpdate.status,
|
messageToUpdate.status,
|
||||||
);
|
);
|
||||||
|
|
||||||
await _database.saveMessage(_database.messageToCompanion(deletedMessage));
|
await _database.saveMessageWithSender(deletedMessage);
|
||||||
|
|
||||||
if (messageIndex != -1) {
|
if (messageIndex != -1) {
|
||||||
final newList = [...currentMessages];
|
final newList = [...currentMessages];
|
||||||
@@ -878,6 +973,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
_searchQuery = null;
|
_searchQuery = null;
|
||||||
_withLinks = null;
|
_withLinks = null;
|
||||||
_withAttachments = null;
|
_withAttachments = null;
|
||||||
|
_allRemoteMessagesFetched = false;
|
||||||
loadInitial();
|
loadInitial();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -888,7 +984,10 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
await (_database.select(_database.chatMessages)
|
await (_database.select(_database.chatMessages)
|
||||||
..where((tbl) => tbl.id.equals(messageId))).getSingleOrNull();
|
..where((tbl) => tbl.id.equals(messageId))).getSingleOrNull();
|
||||||
if (localMessage != null) {
|
if (localMessage != null) {
|
||||||
return _database.companionToMessage(localMessage);
|
return _database.companionToMessage(
|
||||||
|
localMessage,
|
||||||
|
fetchAccount: _fetchAccount,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final response = await _apiClient.get(
|
final response = await _apiClient.get(
|
||||||
@@ -900,7 +999,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
MessageStatus.sent,
|
MessageStatus.sent,
|
||||||
);
|
);
|
||||||
|
|
||||||
await _database.saveMessage(_database.messageToCompanion(message));
|
await _database.saveMessageWithSender(message);
|
||||||
return message;
|
return message;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e is DioException) return null;
|
if (e is DioException) return null;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ part of 'messages_notifier.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$messagesNotifierHash() => r'fc9c99024a0801efa4894f250aea8bdc6127a0b6';
|
String _$messagesNotifierHash() => r'27ce32c54e317a04e1d554ed4a70a24e4503fdd1';
|
||||||
|
|
||||||
/// Copied from Dart SDK
|
/// Copied from Dart SDK
|
||||||
class _SystemHash {
|
class _SystemHash {
|
||||||
|
|||||||
@@ -11,12 +11,36 @@ part 'file_list.g.dart';
|
|||||||
class CloudFileListNotifier extends _$CloudFileListNotifier
|
class CloudFileListNotifier extends _$CloudFileListNotifier
|
||||||
with CursorPagingNotifierMixin<FileListItem> {
|
with CursorPagingNotifierMixin<FileListItem> {
|
||||||
String _currentPath = '/';
|
String _currentPath = '/';
|
||||||
|
String? _poolId;
|
||||||
|
String? _query;
|
||||||
|
String? _order;
|
||||||
|
bool _orderDesc = false;
|
||||||
|
|
||||||
void setPath(String path) {
|
void setPath(String path) {
|
||||||
_currentPath = path;
|
_currentPath = path;
|
||||||
ref.invalidateSelf();
|
ref.invalidateSelf();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setPool(String? poolId) {
|
||||||
|
_poolId = poolId;
|
||||||
|
ref.invalidateSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setQuery(String? query) {
|
||||||
|
_query = query;
|
||||||
|
ref.invalidateSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setOrder(String? order) {
|
||||||
|
_order = order;
|
||||||
|
ref.invalidateSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setOrderDesc(bool orderDesc) {
|
||||||
|
_orderDesc = orderDesc;
|
||||||
|
ref.invalidateSelf();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<CursorPagingData<FileListItem>> build() => fetch(cursor: null);
|
Future<CursorPagingData<FileListItem>> build() => fetch(cursor: null);
|
||||||
|
|
||||||
@@ -26,9 +50,25 @@ class CloudFileListNotifier extends _$CloudFileListNotifier
|
|||||||
}) async {
|
}) async {
|
||||||
final client = ref.read(apiClientProvider);
|
final client = ref.read(apiClientProvider);
|
||||||
|
|
||||||
|
final queryParameters = <String, String>{'path': _currentPath};
|
||||||
|
|
||||||
|
if (_poolId != null) {
|
||||||
|
queryParameters['pool'] = _poolId!;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_query != null) {
|
||||||
|
queryParameters['query'] = _query!;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_order != null) {
|
||||||
|
queryParameters['order'] = _order!;
|
||||||
|
}
|
||||||
|
|
||||||
|
queryParameters['orderDesc'] = _orderDesc.toString();
|
||||||
|
|
||||||
final response = await client.get(
|
final response = await client.get(
|
||||||
'/drive/index/browse',
|
'/drive/index/browse',
|
||||||
queryParameters: {'path': _currentPath},
|
queryParameters: queryParameters,
|
||||||
);
|
);
|
||||||
|
|
||||||
final List<String> folders =
|
final List<String> folders =
|
||||||
@@ -58,6 +98,37 @@ Future<Map<String, dynamic>?> billingUsage(Ref ref) async {
|
|||||||
@riverpod
|
@riverpod
|
||||||
class UnindexedFileListNotifier extends _$UnindexedFileListNotifier
|
class UnindexedFileListNotifier extends _$UnindexedFileListNotifier
|
||||||
with CursorPagingNotifierMixin<FileListItem> {
|
with CursorPagingNotifierMixin<FileListItem> {
|
||||||
|
String? _poolId;
|
||||||
|
bool _recycled = false;
|
||||||
|
String? _query;
|
||||||
|
String? _order;
|
||||||
|
bool _orderDesc = false;
|
||||||
|
|
||||||
|
void setPool(String? poolId) {
|
||||||
|
_poolId = poolId;
|
||||||
|
ref.invalidateSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setRecycled(bool recycled) {
|
||||||
|
_recycled = recycled;
|
||||||
|
ref.invalidateSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setQuery(String? query) {
|
||||||
|
_query = query;
|
||||||
|
ref.invalidateSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setOrder(String? order) {
|
||||||
|
_order = order;
|
||||||
|
ref.invalidateSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setOrderDesc(bool orderDesc) {
|
||||||
|
_orderDesc = orderDesc;
|
||||||
|
ref.invalidateSelf();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<CursorPagingData<FileListItem>> build() => fetch(cursor: null);
|
Future<CursorPagingData<FileListItem>> build() => fetch(cursor: null);
|
||||||
|
|
||||||
@@ -70,9 +141,32 @@ class UnindexedFileListNotifier extends _$UnindexedFileListNotifier
|
|||||||
final offset = cursor != null ? int.tryParse(cursor) ?? 0 : 0;
|
final offset = cursor != null ? int.tryParse(cursor) ?? 0 : 0;
|
||||||
const take = 50; // Default page size
|
const take = 50; // Default page size
|
||||||
|
|
||||||
|
final queryParameters = <String, String>{
|
||||||
|
'take': take.toString(),
|
||||||
|
'offset': offset.toString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (_poolId != null) {
|
||||||
|
queryParameters['pool'] = _poolId!;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_recycled) {
|
||||||
|
queryParameters['recycled'] = _recycled.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_query != null) {
|
||||||
|
queryParameters['query'] = _query!;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_order != null) {
|
||||||
|
queryParameters['order'] = _order!;
|
||||||
|
}
|
||||||
|
|
||||||
|
queryParameters['orderDesc'] = _orderDesc.toString();
|
||||||
|
|
||||||
final response = await client.get(
|
final response = await client.get(
|
||||||
'/drive/index/unindexed',
|
'/drive/index/unindexed',
|
||||||
queryParameters: {'take': take.toString(), 'offset': offset.toString()},
|
queryParameters: queryParameters,
|
||||||
);
|
);
|
||||||
|
|
||||||
final total = int.tryParse(response.headers.value('x-total') ?? '0') ?? 0;
|
final total = int.tryParse(response.headers.value('x-total') ?? '0') ?? 0;
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ final billingQuotaProvider =
|
|||||||
// ignore: unused_element
|
// ignore: unused_element
|
||||||
typedef BillingQuotaRef = AutoDisposeFutureProviderRef<Map<String, dynamic>?>;
|
typedef BillingQuotaRef = AutoDisposeFutureProviderRef<Map<String, dynamic>?>;
|
||||||
String _$cloudFileListNotifierHash() =>
|
String _$cloudFileListNotifierHash() =>
|
||||||
r'5f2f80357cb31ac6473df5ac2101f9a462004f81';
|
r'533dfa86f920b60cf7491fb4aeb95ece19e428af';
|
||||||
|
|
||||||
/// See also [CloudFileListNotifier].
|
/// See also [CloudFileListNotifier].
|
||||||
@ProviderFor(CloudFileListNotifier)
|
@ProviderFor(CloudFileListNotifier)
|
||||||
@@ -66,7 +66,7 @@ final cloudFileListNotifierProvider = AutoDisposeAsyncNotifierProvider<
|
|||||||
typedef _$CloudFileListNotifier =
|
typedef _$CloudFileListNotifier =
|
||||||
AutoDisposeAsyncNotifier<CursorPagingData<FileListItem>>;
|
AutoDisposeAsyncNotifier<CursorPagingData<FileListItem>>;
|
||||||
String _$unindexedFileListNotifierHash() =>
|
String _$unindexedFileListNotifierHash() =>
|
||||||
r'48fc92432a50a562190da5fe8ed0920d171b07b6';
|
r'afa487d7b956b71b21ca1b073a01364a34ede1d5';
|
||||||
|
|
||||||
/// See also [UnindexedFileListNotifier].
|
/// See also [UnindexedFileListNotifier].
|
||||||
@ProviderFor(UnindexedFileListNotifier)
|
@ProviderFor(UnindexedFileListNotifier)
|
||||||
|
|||||||
16
lib/pods/file_references.dart
Normal file
16
lib/pods/file_references.dart
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
import 'package:island/models/reference.dart';
|
||||||
|
import 'package:island/pods/network.dart';
|
||||||
|
|
||||||
|
part 'file_references.g.dart';
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
Future<List<Reference>> fileReferences(Ref ref, String fileId) async {
|
||||||
|
final client = ref.read(apiClientProvider);
|
||||||
|
final response = await client.get('/drive/files/$fileId/references');
|
||||||
|
final list = response.data as List<dynamic>;
|
||||||
|
return list
|
||||||
|
.map((json) => Reference.fromJson(json as Map<String, dynamic>))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
153
lib/pods/file_references.g.dart
Normal file
153
lib/pods/file_references.g.dart
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'file_references.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
String _$fileReferencesHash() => r'd66c678c221f61978bdb242b98e6dbe31d0c204b';
|
||||||
|
|
||||||
|
/// Copied from Dart SDK
|
||||||
|
class _SystemHash {
|
||||||
|
_SystemHash._();
|
||||||
|
|
||||||
|
static int combine(int hash, int value) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + value);
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||||
|
return hash ^ (hash >> 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int finish(int hash) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = hash ^ (hash >> 11);
|
||||||
|
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [fileReferences].
|
||||||
|
@ProviderFor(fileReferences)
|
||||||
|
const fileReferencesProvider = FileReferencesFamily();
|
||||||
|
|
||||||
|
/// See also [fileReferences].
|
||||||
|
class FileReferencesFamily extends Family<AsyncValue<List<Reference>>> {
|
||||||
|
/// See also [fileReferences].
|
||||||
|
const FileReferencesFamily();
|
||||||
|
|
||||||
|
/// See also [fileReferences].
|
||||||
|
FileReferencesProvider call(String fileId) {
|
||||||
|
return FileReferencesProvider(fileId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
FileReferencesProvider getProviderOverride(
|
||||||
|
covariant FileReferencesProvider provider,
|
||||||
|
) {
|
||||||
|
return call(provider.fileId);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||||
|
_allTransitiveDependencies;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get name => r'fileReferencesProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [fileReferences].
|
||||||
|
class FileReferencesProvider
|
||||||
|
extends AutoDisposeFutureProvider<List<Reference>> {
|
||||||
|
/// See also [fileReferences].
|
||||||
|
FileReferencesProvider(String fileId)
|
||||||
|
: this._internal(
|
||||||
|
(ref) => fileReferences(ref as FileReferencesRef, fileId),
|
||||||
|
from: fileReferencesProvider,
|
||||||
|
name: r'fileReferencesProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$fileReferencesHash,
|
||||||
|
dependencies: FileReferencesFamily._dependencies,
|
||||||
|
allTransitiveDependencies:
|
||||||
|
FileReferencesFamily._allTransitiveDependencies,
|
||||||
|
fileId: fileId,
|
||||||
|
);
|
||||||
|
|
||||||
|
FileReferencesProvider._internal(
|
||||||
|
super._createNotifier, {
|
||||||
|
required super.name,
|
||||||
|
required super.dependencies,
|
||||||
|
required super.allTransitiveDependencies,
|
||||||
|
required super.debugGetCreateSourceHash,
|
||||||
|
required super.from,
|
||||||
|
required this.fileId,
|
||||||
|
}) : super.internal();
|
||||||
|
|
||||||
|
final String fileId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Override overrideWith(
|
||||||
|
FutureOr<List<Reference>> Function(FileReferencesRef provider) create,
|
||||||
|
) {
|
||||||
|
return ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
override: FileReferencesProvider._internal(
|
||||||
|
(ref) => create(ref as FileReferencesRef),
|
||||||
|
from: from,
|
||||||
|
name: null,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
debugGetCreateSourceHash: null,
|
||||||
|
fileId: fileId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AutoDisposeFutureProviderElement<List<Reference>> createElement() {
|
||||||
|
return _FileReferencesProviderElement(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is FileReferencesProvider && other.fileId == fileId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, fileId.hashCode);
|
||||||
|
|
||||||
|
return _SystemHash.finish(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
mixin FileReferencesRef on AutoDisposeFutureProviderRef<List<Reference>> {
|
||||||
|
/// The parameter `fileId` of this provider.
|
||||||
|
String get fileId;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FileReferencesProviderElement
|
||||||
|
extends AutoDisposeFutureProviderElement<List<Reference>>
|
||||||
|
with FileReferencesRef {
|
||||||
|
_FileReferencesProviderElement(super.provider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get fileId => (origin as FileReferencesProvider).fileId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
159
lib/pods/site_files.dart
Normal file
159
lib/pods/site_files.dart
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:http_parser/http_parser.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:island/models/site_file.dart';
|
||||||
|
import 'package:island/pods/network.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
|
part 'site_files.g.dart';
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
Future<List<SnSiteFileEntry>> siteFiles(
|
||||||
|
Ref ref, {
|
||||||
|
required String siteId,
|
||||||
|
String? path,
|
||||||
|
}) async {
|
||||||
|
final apiClient = ref.watch(apiClientProvider);
|
||||||
|
final queryParams = path != null ? {'path': path} : null;
|
||||||
|
final resp = await apiClient.get(
|
||||||
|
'/zone/sites/$siteId/files',
|
||||||
|
queryParameters: queryParams,
|
||||||
|
);
|
||||||
|
final data = resp.data as List<dynamic>;
|
||||||
|
return data.map((json) => SnSiteFileEntry.fromJson(json)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
Future<SnFileContent> siteFileContent(
|
||||||
|
Ref ref, {
|
||||||
|
required String siteId,
|
||||||
|
required String relativePath,
|
||||||
|
}) async {
|
||||||
|
final apiClient = ref.watch(apiClientProvider);
|
||||||
|
final resp = await apiClient.get(
|
||||||
|
'/zone/sites/$siteId/files/content/$relativePath',
|
||||||
|
);
|
||||||
|
final content =
|
||||||
|
resp.data is String
|
||||||
|
? resp.data
|
||||||
|
: SnFileContent.fromJson(resp.data).content;
|
||||||
|
return SnFileContent(content: content);
|
||||||
|
}
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
Future<String> siteFileContentRaw(
|
||||||
|
Ref ref, {
|
||||||
|
required String siteId,
|
||||||
|
required String relativePath,
|
||||||
|
}) async {
|
||||||
|
final apiClient = ref.watch(apiClientProvider);
|
||||||
|
final resp = await apiClient.get(
|
||||||
|
'/zone/sites/$siteId/files/content/$relativePath',
|
||||||
|
);
|
||||||
|
return resp.data is String ? resp.data : resp.data['content'] as String;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SiteFilesNotifier
|
||||||
|
extends
|
||||||
|
AutoDisposeFamilyAsyncNotifier<
|
||||||
|
List<SnSiteFileEntry>,
|
||||||
|
({String siteId, String? path})
|
||||||
|
> {
|
||||||
|
@override
|
||||||
|
Future<List<SnSiteFileEntry>> build(
|
||||||
|
({String siteId, String? path}) arg,
|
||||||
|
) async {
|
||||||
|
return fetchFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<SnSiteFileEntry>> fetchFiles() async {
|
||||||
|
try {
|
||||||
|
final apiClient = ref.read(apiClientProvider);
|
||||||
|
final queryParams = arg.path != null ? {'path': arg.path} : null;
|
||||||
|
final resp = await apiClient.get(
|
||||||
|
'/zone/sites/${arg.siteId}/files',
|
||||||
|
queryParameters: queryParams,
|
||||||
|
);
|
||||||
|
final data = resp.data as List<dynamic>;
|
||||||
|
return data.map((json) => SnSiteFileEntry.fromJson(json)).toList();
|
||||||
|
} catch (e) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> uploadFile(File file, String filePath) async {
|
||||||
|
state = const AsyncValue.loading();
|
||||||
|
try {
|
||||||
|
final apiClient = ref.read(apiClientProvider);
|
||||||
|
|
||||||
|
// Create multipart form data
|
||||||
|
final formData = FormData.fromMap({
|
||||||
|
'filePath': filePath,
|
||||||
|
'file': await MultipartFile.fromFile(
|
||||||
|
file.path,
|
||||||
|
filename: file.path.split('/').last,
|
||||||
|
contentType: MediaType('application', 'octet-stream'),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
await apiClient.post(
|
||||||
|
'/zone/sites/${arg.siteId}/files/upload',
|
||||||
|
data: formData,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Refresh the files list
|
||||||
|
ref.invalidate(siteFilesProvider(siteId: arg.siteId, path: arg.path));
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
state = AsyncValue.error(error, stackTrace);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateFileContent(String relativePath, String newContent) async {
|
||||||
|
state = const AsyncValue.loading();
|
||||||
|
try {
|
||||||
|
final apiClient = ref.read(apiClientProvider);
|
||||||
|
await apiClient.put(
|
||||||
|
'/zone/sites/${arg.siteId}/files/edit/$relativePath',
|
||||||
|
data: {'new_content': newContent},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Refresh the files list
|
||||||
|
ref.invalidate(siteFilesProvider(siteId: arg.siteId, path: arg.path));
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
state = AsyncValue.error(error, stackTrace);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteFile(String relativePath) async {
|
||||||
|
state = const AsyncValue.loading();
|
||||||
|
try {
|
||||||
|
final apiClient = ref.read(apiClientProvider);
|
||||||
|
await apiClient.delete(
|
||||||
|
'/zone/sites/${arg.siteId}/files/delete/$relativePath',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Refresh the files list
|
||||||
|
ref.invalidate(siteFilesProvider(siteId: arg.siteId, path: arg.path));
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
state = AsyncValue.error(error, stackTrace);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> createDirectory(String directoryPath) async {
|
||||||
|
// For directories, we upload a dummy file first then delete it or create through upload
|
||||||
|
// Actually, according to API docs, directories are created when uploading files to them
|
||||||
|
// So we'll just invalidate to refresh the list
|
||||||
|
ref.invalidate(siteFilesProvider(siteId: arg.siteId, path: arg.path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final siteFilesNotifierProvider = AsyncNotifierProvider.autoDispose.family<
|
||||||
|
SiteFilesNotifier,
|
||||||
|
List<SnSiteFileEntry>,
|
||||||
|
({String siteId, String? path})
|
||||||
|
>(SiteFilesNotifier.new);
|
||||||
451
lib/pods/site_files.g.dart
Normal file
451
lib/pods/site_files.g.dart
Normal file
@@ -0,0 +1,451 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'site_files.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
String _$siteFilesHash() => r'd4029e6c160edcd454eb39ef1c19427b7f95a8d8';
|
||||||
|
|
||||||
|
/// Copied from Dart SDK
|
||||||
|
class _SystemHash {
|
||||||
|
_SystemHash._();
|
||||||
|
|
||||||
|
static int combine(int hash, int value) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + value);
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||||
|
return hash ^ (hash >> 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int finish(int hash) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = hash ^ (hash >> 11);
|
||||||
|
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [siteFiles].
|
||||||
|
@ProviderFor(siteFiles)
|
||||||
|
const siteFilesProvider = SiteFilesFamily();
|
||||||
|
|
||||||
|
/// See also [siteFiles].
|
||||||
|
class SiteFilesFamily extends Family<AsyncValue<List<SnSiteFileEntry>>> {
|
||||||
|
/// See also [siteFiles].
|
||||||
|
const SiteFilesFamily();
|
||||||
|
|
||||||
|
/// See also [siteFiles].
|
||||||
|
SiteFilesProvider call({required String siteId, String? path}) {
|
||||||
|
return SiteFilesProvider(siteId: siteId, path: path);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
SiteFilesProvider getProviderOverride(covariant SiteFilesProvider provider) {
|
||||||
|
return call(siteId: provider.siteId, path: provider.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||||
|
_allTransitiveDependencies;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get name => r'siteFilesProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [siteFiles].
|
||||||
|
class SiteFilesProvider
|
||||||
|
extends AutoDisposeFutureProvider<List<SnSiteFileEntry>> {
|
||||||
|
/// See also [siteFiles].
|
||||||
|
SiteFilesProvider({required String siteId, String? path})
|
||||||
|
: this._internal(
|
||||||
|
(ref) => siteFiles(ref as SiteFilesRef, siteId: siteId, path: path),
|
||||||
|
from: siteFilesProvider,
|
||||||
|
name: r'siteFilesProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$siteFilesHash,
|
||||||
|
dependencies: SiteFilesFamily._dependencies,
|
||||||
|
allTransitiveDependencies: SiteFilesFamily._allTransitiveDependencies,
|
||||||
|
siteId: siteId,
|
||||||
|
path: path,
|
||||||
|
);
|
||||||
|
|
||||||
|
SiteFilesProvider._internal(
|
||||||
|
super._createNotifier, {
|
||||||
|
required super.name,
|
||||||
|
required super.dependencies,
|
||||||
|
required super.allTransitiveDependencies,
|
||||||
|
required super.debugGetCreateSourceHash,
|
||||||
|
required super.from,
|
||||||
|
required this.siteId,
|
||||||
|
required this.path,
|
||||||
|
}) : super.internal();
|
||||||
|
|
||||||
|
final String siteId;
|
||||||
|
final String? path;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Override overrideWith(
|
||||||
|
FutureOr<List<SnSiteFileEntry>> Function(SiteFilesRef provider) create,
|
||||||
|
) {
|
||||||
|
return ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
override: SiteFilesProvider._internal(
|
||||||
|
(ref) => create(ref as SiteFilesRef),
|
||||||
|
from: from,
|
||||||
|
name: null,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
debugGetCreateSourceHash: null,
|
||||||
|
siteId: siteId,
|
||||||
|
path: path,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AutoDisposeFutureProviderElement<List<SnSiteFileEntry>> createElement() {
|
||||||
|
return _SiteFilesProviderElement(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is SiteFilesProvider &&
|
||||||
|
other.siteId == siteId &&
|
||||||
|
other.path == path;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, siteId.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, path.hashCode);
|
||||||
|
|
||||||
|
return _SystemHash.finish(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
mixin SiteFilesRef on AutoDisposeFutureProviderRef<List<SnSiteFileEntry>> {
|
||||||
|
/// The parameter `siteId` of this provider.
|
||||||
|
String get siteId;
|
||||||
|
|
||||||
|
/// The parameter `path` of this provider.
|
||||||
|
String? get path;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SiteFilesProviderElement
|
||||||
|
extends AutoDisposeFutureProviderElement<List<SnSiteFileEntry>>
|
||||||
|
with SiteFilesRef {
|
||||||
|
_SiteFilesProviderElement(super.provider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get siteId => (origin as SiteFilesProvider).siteId;
|
||||||
|
@override
|
||||||
|
String? get path => (origin as SiteFilesProvider).path;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$siteFileContentHash() => r'b594ad4f8c54555e742ece94ee001092cb2f83d1';
|
||||||
|
|
||||||
|
/// See also [siteFileContent].
|
||||||
|
@ProviderFor(siteFileContent)
|
||||||
|
const siteFileContentProvider = SiteFileContentFamily();
|
||||||
|
|
||||||
|
/// See also [siteFileContent].
|
||||||
|
class SiteFileContentFamily extends Family<AsyncValue<SnFileContent>> {
|
||||||
|
/// See also [siteFileContent].
|
||||||
|
const SiteFileContentFamily();
|
||||||
|
|
||||||
|
/// See also [siteFileContent].
|
||||||
|
SiteFileContentProvider call({
|
||||||
|
required String siteId,
|
||||||
|
required String relativePath,
|
||||||
|
}) {
|
||||||
|
return SiteFileContentProvider(siteId: siteId, relativePath: relativePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
SiteFileContentProvider getProviderOverride(
|
||||||
|
covariant SiteFileContentProvider provider,
|
||||||
|
) {
|
||||||
|
return call(siteId: provider.siteId, relativePath: provider.relativePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||||
|
_allTransitiveDependencies;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get name => r'siteFileContentProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [siteFileContent].
|
||||||
|
class SiteFileContentProvider extends AutoDisposeFutureProvider<SnFileContent> {
|
||||||
|
/// See also [siteFileContent].
|
||||||
|
SiteFileContentProvider({
|
||||||
|
required String siteId,
|
||||||
|
required String relativePath,
|
||||||
|
}) : this._internal(
|
||||||
|
(ref) => siteFileContent(
|
||||||
|
ref as SiteFileContentRef,
|
||||||
|
siteId: siteId,
|
||||||
|
relativePath: relativePath,
|
||||||
|
),
|
||||||
|
from: siteFileContentProvider,
|
||||||
|
name: r'siteFileContentProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$siteFileContentHash,
|
||||||
|
dependencies: SiteFileContentFamily._dependencies,
|
||||||
|
allTransitiveDependencies:
|
||||||
|
SiteFileContentFamily._allTransitiveDependencies,
|
||||||
|
siteId: siteId,
|
||||||
|
relativePath: relativePath,
|
||||||
|
);
|
||||||
|
|
||||||
|
SiteFileContentProvider._internal(
|
||||||
|
super._createNotifier, {
|
||||||
|
required super.name,
|
||||||
|
required super.dependencies,
|
||||||
|
required super.allTransitiveDependencies,
|
||||||
|
required super.debugGetCreateSourceHash,
|
||||||
|
required super.from,
|
||||||
|
required this.siteId,
|
||||||
|
required this.relativePath,
|
||||||
|
}) : super.internal();
|
||||||
|
|
||||||
|
final String siteId;
|
||||||
|
final String relativePath;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Override overrideWith(
|
||||||
|
FutureOr<SnFileContent> Function(SiteFileContentRef provider) create,
|
||||||
|
) {
|
||||||
|
return ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
override: SiteFileContentProvider._internal(
|
||||||
|
(ref) => create(ref as SiteFileContentRef),
|
||||||
|
from: from,
|
||||||
|
name: null,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
debugGetCreateSourceHash: null,
|
||||||
|
siteId: siteId,
|
||||||
|
relativePath: relativePath,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AutoDisposeFutureProviderElement<SnFileContent> createElement() {
|
||||||
|
return _SiteFileContentProviderElement(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is SiteFileContentProvider &&
|
||||||
|
other.siteId == siteId &&
|
||||||
|
other.relativePath == relativePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, siteId.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, relativePath.hashCode);
|
||||||
|
|
||||||
|
return _SystemHash.finish(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
mixin SiteFileContentRef on AutoDisposeFutureProviderRef<SnFileContent> {
|
||||||
|
/// The parameter `siteId` of this provider.
|
||||||
|
String get siteId;
|
||||||
|
|
||||||
|
/// The parameter `relativePath` of this provider.
|
||||||
|
String get relativePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SiteFileContentProviderElement
|
||||||
|
extends AutoDisposeFutureProviderElement<SnFileContent>
|
||||||
|
with SiteFileContentRef {
|
||||||
|
_SiteFileContentProviderElement(super.provider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get siteId => (origin as SiteFileContentProvider).siteId;
|
||||||
|
@override
|
||||||
|
String get relativePath => (origin as SiteFileContentProvider).relativePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$siteFileContentRawHash() =>
|
||||||
|
r'd0331c30698a9f4b90fe9b79273ff5914fa46616';
|
||||||
|
|
||||||
|
/// See also [siteFileContentRaw].
|
||||||
|
@ProviderFor(siteFileContentRaw)
|
||||||
|
const siteFileContentRawProvider = SiteFileContentRawFamily();
|
||||||
|
|
||||||
|
/// See also [siteFileContentRaw].
|
||||||
|
class SiteFileContentRawFamily extends Family<AsyncValue<String>> {
|
||||||
|
/// See also [siteFileContentRaw].
|
||||||
|
const SiteFileContentRawFamily();
|
||||||
|
|
||||||
|
/// See also [siteFileContentRaw].
|
||||||
|
SiteFileContentRawProvider call({
|
||||||
|
required String siteId,
|
||||||
|
required String relativePath,
|
||||||
|
}) {
|
||||||
|
return SiteFileContentRawProvider(
|
||||||
|
siteId: siteId,
|
||||||
|
relativePath: relativePath,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
SiteFileContentRawProvider getProviderOverride(
|
||||||
|
covariant SiteFileContentRawProvider provider,
|
||||||
|
) {
|
||||||
|
return call(siteId: provider.siteId, relativePath: provider.relativePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||||
|
_allTransitiveDependencies;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get name => r'siteFileContentRawProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [siteFileContentRaw].
|
||||||
|
class SiteFileContentRawProvider extends AutoDisposeFutureProvider<String> {
|
||||||
|
/// See also [siteFileContentRaw].
|
||||||
|
SiteFileContentRawProvider({
|
||||||
|
required String siteId,
|
||||||
|
required String relativePath,
|
||||||
|
}) : this._internal(
|
||||||
|
(ref) => siteFileContentRaw(
|
||||||
|
ref as SiteFileContentRawRef,
|
||||||
|
siteId: siteId,
|
||||||
|
relativePath: relativePath,
|
||||||
|
),
|
||||||
|
from: siteFileContentRawProvider,
|
||||||
|
name: r'siteFileContentRawProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$siteFileContentRawHash,
|
||||||
|
dependencies: SiteFileContentRawFamily._dependencies,
|
||||||
|
allTransitiveDependencies:
|
||||||
|
SiteFileContentRawFamily._allTransitiveDependencies,
|
||||||
|
siteId: siteId,
|
||||||
|
relativePath: relativePath,
|
||||||
|
);
|
||||||
|
|
||||||
|
SiteFileContentRawProvider._internal(
|
||||||
|
super._createNotifier, {
|
||||||
|
required super.name,
|
||||||
|
required super.dependencies,
|
||||||
|
required super.allTransitiveDependencies,
|
||||||
|
required super.debugGetCreateSourceHash,
|
||||||
|
required super.from,
|
||||||
|
required this.siteId,
|
||||||
|
required this.relativePath,
|
||||||
|
}) : super.internal();
|
||||||
|
|
||||||
|
final String siteId;
|
||||||
|
final String relativePath;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Override overrideWith(
|
||||||
|
FutureOr<String> Function(SiteFileContentRawRef provider) create,
|
||||||
|
) {
|
||||||
|
return ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
override: SiteFileContentRawProvider._internal(
|
||||||
|
(ref) => create(ref as SiteFileContentRawRef),
|
||||||
|
from: from,
|
||||||
|
name: null,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
debugGetCreateSourceHash: null,
|
||||||
|
siteId: siteId,
|
||||||
|
relativePath: relativePath,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AutoDisposeFutureProviderElement<String> createElement() {
|
||||||
|
return _SiteFileContentRawProviderElement(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is SiteFileContentRawProvider &&
|
||||||
|
other.siteId == siteId &&
|
||||||
|
other.relativePath == relativePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, siteId.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, relativePath.hashCode);
|
||||||
|
|
||||||
|
return _SystemHash.finish(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
mixin SiteFileContentRawRef on AutoDisposeFutureProviderRef<String> {
|
||||||
|
/// The parameter `siteId` of this provider.
|
||||||
|
String get siteId;
|
||||||
|
|
||||||
|
/// The parameter `relativePath` of this provider.
|
||||||
|
String get relativePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SiteFileContentRawProviderElement
|
||||||
|
extends AutoDisposeFutureProviderElement<String>
|
||||||
|
with SiteFileContentRawRef {
|
||||||
|
_SiteFileContentRawProviderElement(super.provider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get siteId => (origin as SiteFileContentRawProvider).siteId;
|
||||||
|
@override
|
||||||
|
String get relativePath =>
|
||||||
|
(origin as SiteFileContentRawProvider).relativePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
116
lib/pods/site_pages.dart
Normal file
116
lib/pods/site_pages.dart
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:island/models/publication_site.dart';
|
||||||
|
import 'package:island/pods/network.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
|
part 'site_pages.g.dart';
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
Future<List<SnPublicationPage>> sitePages(
|
||||||
|
Ref ref,
|
||||||
|
String pubName,
|
||||||
|
String siteSlug,
|
||||||
|
) async {
|
||||||
|
final apiClient = ref.watch(apiClientProvider);
|
||||||
|
final resp = await apiClient.get('/zone/sites/$pubName/$siteSlug/pages');
|
||||||
|
final data = resp.data as List<dynamic>;
|
||||||
|
return data.map((json) => SnPublicationPage.fromJson(json)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
Future<SnPublicationPage> sitePage(Ref ref, String pageId) async {
|
||||||
|
final apiClient = ref.watch(apiClientProvider);
|
||||||
|
final resp = await apiClient.get('/zone/sites/pages/$pageId');
|
||||||
|
return SnPublicationPage.fromJson(resp.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
class SitePagesNotifier
|
||||||
|
extends
|
||||||
|
AutoDisposeFamilyAsyncNotifier<
|
||||||
|
List<SnPublicationPage>,
|
||||||
|
({String pubName, String siteSlug})
|
||||||
|
> {
|
||||||
|
@override
|
||||||
|
Future<List<SnPublicationPage>> build(
|
||||||
|
({String pubName, String siteSlug}) arg,
|
||||||
|
) async {
|
||||||
|
return fetchPages();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<SnPublicationPage>> fetchPages() async {
|
||||||
|
try {
|
||||||
|
final apiClient = ref.read(apiClientProvider);
|
||||||
|
final resp = await apiClient.get(
|
||||||
|
'/zone/sites/${arg.pubName}/${arg.siteSlug}/pages',
|
||||||
|
);
|
||||||
|
final data = resp.data as List<dynamic>;
|
||||||
|
return data.map((json) => SnPublicationPage.fromJson(json)).toList();
|
||||||
|
} catch (e) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<SnPublicationPage?> createPage(Map<String, dynamic> pageData) async {
|
||||||
|
state = const AsyncValue.loading();
|
||||||
|
try {
|
||||||
|
final apiClient = ref.read(apiClientProvider);
|
||||||
|
final resp = await apiClient.post(
|
||||||
|
'/zone/sites/${arg.pubName}/${arg.siteSlug}/pages',
|
||||||
|
data: pageData,
|
||||||
|
);
|
||||||
|
final newPage = SnPublicationPage.fromJson(resp.data);
|
||||||
|
|
||||||
|
// Refresh the pages list
|
||||||
|
ref.invalidate(sitePagesProvider(arg.pubName, arg.siteSlug));
|
||||||
|
|
||||||
|
return newPage;
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
state = AsyncValue.error(error, stackTrace);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<SnPublicationPage?> updatePage(
|
||||||
|
String pageId,
|
||||||
|
Map<String, dynamic> pageData,
|
||||||
|
) async {
|
||||||
|
state = const AsyncValue.loading();
|
||||||
|
try {
|
||||||
|
final apiClient = ref.read(apiClientProvider);
|
||||||
|
final resp = await apiClient.patch(
|
||||||
|
'/zone/sites/pages/$pageId',
|
||||||
|
data: pageData,
|
||||||
|
);
|
||||||
|
final updatedPage = SnPublicationPage.fromJson(resp.data);
|
||||||
|
|
||||||
|
// Refresh the pages list
|
||||||
|
ref.invalidate(sitePagesProvider(arg.pubName, arg.siteSlug));
|
||||||
|
|
||||||
|
return updatedPage;
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
state = AsyncValue.error(error, stackTrace);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deletePage(String pageId) async {
|
||||||
|
state = const AsyncValue.loading();
|
||||||
|
try {
|
||||||
|
final apiClient = ref.read(apiClientProvider);
|
||||||
|
await apiClient.delete('/zone/sites/pages/$pageId');
|
||||||
|
|
||||||
|
// Refresh the pages list
|
||||||
|
ref.invalidate(sitePagesProvider(arg.pubName, arg.siteSlug));
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
state = AsyncValue.error(error, stackTrace);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final sitePagesNotifierProvider = AsyncNotifierProvider.autoDispose.family<
|
||||||
|
SitePagesNotifier,
|
||||||
|
List<SnPublicationPage>,
|
||||||
|
({String pubName, String siteSlug})
|
||||||
|
>(SitePagesNotifier.new);
|
||||||
280
lib/pods/site_pages.g.dart
Normal file
280
lib/pods/site_pages.g.dart
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'site_pages.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
String _$sitePagesHash() => r'5e084e9694ad665e9b238c6a747c6c6e99c5eb03';
|
||||||
|
|
||||||
|
/// Copied from Dart SDK
|
||||||
|
class _SystemHash {
|
||||||
|
_SystemHash._();
|
||||||
|
|
||||||
|
static int combine(int hash, int value) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + value);
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||||
|
return hash ^ (hash >> 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int finish(int hash) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = hash ^ (hash >> 11);
|
||||||
|
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [sitePages].
|
||||||
|
@ProviderFor(sitePages)
|
||||||
|
const sitePagesProvider = SitePagesFamily();
|
||||||
|
|
||||||
|
/// See also [sitePages].
|
||||||
|
class SitePagesFamily extends Family<AsyncValue<List<SnPublicationPage>>> {
|
||||||
|
/// See also [sitePages].
|
||||||
|
const SitePagesFamily();
|
||||||
|
|
||||||
|
/// See also [sitePages].
|
||||||
|
SitePagesProvider call(String pubName, String siteSlug) {
|
||||||
|
return SitePagesProvider(pubName, siteSlug);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
SitePagesProvider getProviderOverride(covariant SitePagesProvider provider) {
|
||||||
|
return call(provider.pubName, provider.siteSlug);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||||
|
_allTransitiveDependencies;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get name => r'sitePagesProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [sitePages].
|
||||||
|
class SitePagesProvider
|
||||||
|
extends AutoDisposeFutureProvider<List<SnPublicationPage>> {
|
||||||
|
/// See also [sitePages].
|
||||||
|
SitePagesProvider(String pubName, String siteSlug)
|
||||||
|
: this._internal(
|
||||||
|
(ref) => sitePages(ref as SitePagesRef, pubName, siteSlug),
|
||||||
|
from: sitePagesProvider,
|
||||||
|
name: r'sitePagesProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$sitePagesHash,
|
||||||
|
dependencies: SitePagesFamily._dependencies,
|
||||||
|
allTransitiveDependencies: SitePagesFamily._allTransitiveDependencies,
|
||||||
|
pubName: pubName,
|
||||||
|
siteSlug: siteSlug,
|
||||||
|
);
|
||||||
|
|
||||||
|
SitePagesProvider._internal(
|
||||||
|
super._createNotifier, {
|
||||||
|
required super.name,
|
||||||
|
required super.dependencies,
|
||||||
|
required super.allTransitiveDependencies,
|
||||||
|
required super.debugGetCreateSourceHash,
|
||||||
|
required super.from,
|
||||||
|
required this.pubName,
|
||||||
|
required this.siteSlug,
|
||||||
|
}) : super.internal();
|
||||||
|
|
||||||
|
final String pubName;
|
||||||
|
final String siteSlug;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Override overrideWith(
|
||||||
|
FutureOr<List<SnPublicationPage>> Function(SitePagesRef provider) create,
|
||||||
|
) {
|
||||||
|
return ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
override: SitePagesProvider._internal(
|
||||||
|
(ref) => create(ref as SitePagesRef),
|
||||||
|
from: from,
|
||||||
|
name: null,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
debugGetCreateSourceHash: null,
|
||||||
|
pubName: pubName,
|
||||||
|
siteSlug: siteSlug,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AutoDisposeFutureProviderElement<List<SnPublicationPage>> createElement() {
|
||||||
|
return _SitePagesProviderElement(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is SitePagesProvider &&
|
||||||
|
other.pubName == pubName &&
|
||||||
|
other.siteSlug == siteSlug;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, pubName.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, siteSlug.hashCode);
|
||||||
|
|
||||||
|
return _SystemHash.finish(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
mixin SitePagesRef on AutoDisposeFutureProviderRef<List<SnPublicationPage>> {
|
||||||
|
/// The parameter `pubName` of this provider.
|
||||||
|
String get pubName;
|
||||||
|
|
||||||
|
/// The parameter `siteSlug` of this provider.
|
||||||
|
String get siteSlug;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SitePagesProviderElement
|
||||||
|
extends AutoDisposeFutureProviderElement<List<SnPublicationPage>>
|
||||||
|
with SitePagesRef {
|
||||||
|
_SitePagesProviderElement(super.provider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pubName => (origin as SitePagesProvider).pubName;
|
||||||
|
@override
|
||||||
|
String get siteSlug => (origin as SitePagesProvider).siteSlug;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$sitePageHash() => r'542f70c5b103fe34d7cf7eb0821d52f017022efc';
|
||||||
|
|
||||||
|
/// See also [sitePage].
|
||||||
|
@ProviderFor(sitePage)
|
||||||
|
const sitePageProvider = SitePageFamily();
|
||||||
|
|
||||||
|
/// See also [sitePage].
|
||||||
|
class SitePageFamily extends Family<AsyncValue<SnPublicationPage>> {
|
||||||
|
/// See also [sitePage].
|
||||||
|
const SitePageFamily();
|
||||||
|
|
||||||
|
/// See also [sitePage].
|
||||||
|
SitePageProvider call(String pageId) {
|
||||||
|
return SitePageProvider(pageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
SitePageProvider getProviderOverride(covariant SitePageProvider provider) {
|
||||||
|
return call(provider.pageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||||
|
_allTransitiveDependencies;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get name => r'sitePageProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [sitePage].
|
||||||
|
class SitePageProvider extends AutoDisposeFutureProvider<SnPublicationPage> {
|
||||||
|
/// See also [sitePage].
|
||||||
|
SitePageProvider(String pageId)
|
||||||
|
: this._internal(
|
||||||
|
(ref) => sitePage(ref as SitePageRef, pageId),
|
||||||
|
from: sitePageProvider,
|
||||||
|
name: r'sitePageProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$sitePageHash,
|
||||||
|
dependencies: SitePageFamily._dependencies,
|
||||||
|
allTransitiveDependencies: SitePageFamily._allTransitiveDependencies,
|
||||||
|
pageId: pageId,
|
||||||
|
);
|
||||||
|
|
||||||
|
SitePageProvider._internal(
|
||||||
|
super._createNotifier, {
|
||||||
|
required super.name,
|
||||||
|
required super.dependencies,
|
||||||
|
required super.allTransitiveDependencies,
|
||||||
|
required super.debugGetCreateSourceHash,
|
||||||
|
required super.from,
|
||||||
|
required this.pageId,
|
||||||
|
}) : super.internal();
|
||||||
|
|
||||||
|
final String pageId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Override overrideWith(
|
||||||
|
FutureOr<SnPublicationPage> Function(SitePageRef provider) create,
|
||||||
|
) {
|
||||||
|
return ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
override: SitePageProvider._internal(
|
||||||
|
(ref) => create(ref as SitePageRef),
|
||||||
|
from: from,
|
||||||
|
name: null,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
debugGetCreateSourceHash: null,
|
||||||
|
pageId: pageId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AutoDisposeFutureProviderElement<SnPublicationPage> createElement() {
|
||||||
|
return _SitePageProviderElement(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is SitePageProvider && other.pageId == pageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, pageId.hashCode);
|
||||||
|
|
||||||
|
return _SystemHash.finish(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
mixin SitePageRef on AutoDisposeFutureProviderRef<SnPublicationPage> {
|
||||||
|
/// The parameter `pageId` of this provider.
|
||||||
|
String get pageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SitePageProviderElement
|
||||||
|
extends AutoDisposeFutureProviderElement<SnPublicationPage>
|
||||||
|
with SitePageRef {
|
||||||
|
_SitePageProviderElement(super.provider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pageId => (origin as SitePageProvider).pageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
88
lib/pods/sites.dart
Normal file
88
lib/pods/sites.dart
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:island/models/publication_site.dart';
|
||||||
|
import 'package:island/pods/network.dart';
|
||||||
|
|
||||||
|
class SiteNotifier
|
||||||
|
extends
|
||||||
|
AutoDisposeFamilyAsyncNotifier<
|
||||||
|
SnPublicationSite,
|
||||||
|
({String pubName, String? siteId})
|
||||||
|
> {
|
||||||
|
@override
|
||||||
|
FutureOr<SnPublicationSite> build(
|
||||||
|
({String pubName, String? siteId}) arg,
|
||||||
|
) async {
|
||||||
|
if (arg.siteId == null || arg.siteId!.isEmpty) {
|
||||||
|
return SnPublicationSite(
|
||||||
|
id: '',
|
||||||
|
slug: '',
|
||||||
|
name: '',
|
||||||
|
publisherId: arg.pubName,
|
||||||
|
accountId: '',
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
pages: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final client = ref.read(apiClientProvider);
|
||||||
|
final response = await client.get('/sphere/sites/${arg.siteId}');
|
||||||
|
return SnPublicationSite.fromJson(response.data);
|
||||||
|
} catch (e) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> saveSite(SnPublicationSite site) async {
|
||||||
|
state = const AsyncValue.loading();
|
||||||
|
try {
|
||||||
|
final client = ref.read(apiClientProvider);
|
||||||
|
final url = '/sphere/sites';
|
||||||
|
|
||||||
|
final response =
|
||||||
|
site.id.isEmpty
|
||||||
|
? await client.post(url, data: site.toJson())
|
||||||
|
: await client.patch('$url/${site.id}', data: site.toJson());
|
||||||
|
|
||||||
|
state = AsyncValue.data(SnPublicationSite.fromJson(response.data));
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
state = AsyncValue.error(error, stackTrace);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteSite() async {
|
||||||
|
final siteId = arg.siteId;
|
||||||
|
if (siteId == null || siteId.isEmpty) return;
|
||||||
|
|
||||||
|
state = const AsyncValue.loading();
|
||||||
|
try {
|
||||||
|
final client = ref.read(apiClientProvider);
|
||||||
|
await client.delete('/sphere/sites/$siteId');
|
||||||
|
state = AsyncValue.data(
|
||||||
|
SnPublicationSite(
|
||||||
|
id: '',
|
||||||
|
slug: '',
|
||||||
|
name: '',
|
||||||
|
publisherId: arg.pubName,
|
||||||
|
accountId: '',
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
pages: [],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
state = AsyncValue.error(error, stackTrace);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final siteNotifierProvider = AsyncNotifierProvider.autoDispose.family<
|
||||||
|
SiteNotifier,
|
||||||
|
SnPublicationSite,
|
||||||
|
({String pubName, String? siteId})
|
||||||
|
>(SiteNotifier.new);
|
||||||
@@ -258,6 +258,24 @@ class UploadTasksNotifier extends StateNotifier<List<DriveTask>> {
|
|||||||
}).toList();
|
}).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void updateDownloadProgress(
|
||||||
|
String taskId,
|
||||||
|
int downloadedBytes,
|
||||||
|
int totalBytes,
|
||||||
|
) {
|
||||||
|
state =
|
||||||
|
state.map((task) {
|
||||||
|
if (task.taskId == taskId) {
|
||||||
|
return task.copyWith(
|
||||||
|
fileSize: totalBytes,
|
||||||
|
uploadedBytes: downloadedBytes,
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return task;
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
void removeTask(String taskId) {
|
void removeTask(String taskId) {
|
||||||
state = state.where((task) => task.taskId != taskId).toList();
|
state = state.where((task) => task.taskId != taskId).toList();
|
||||||
}
|
}
|
||||||
@@ -275,6 +293,10 @@ class UploadTasksNotifier extends StateNotifier<List<DriveTask>> {
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void clearAllTasks() {
|
||||||
|
state = [];
|
||||||
|
}
|
||||||
|
|
||||||
DriveTask? getTask(String taskId) {
|
DriveTask? getTask(String taskId) {
|
||||||
return state.where((task) => task.taskId == taskId).firstOrNull;
|
return state.where((task) => task.taskId == taskId).firstOrNull;
|
||||||
}
|
}
|
||||||
@@ -291,6 +313,27 @@ class UploadTasksNotifier extends StateNotifier<List<DriveTask>> {
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String addLocalDownloadTask(SnCloudFile item) {
|
||||||
|
final taskId =
|
||||||
|
'download-${item.id}-${DateTime.now().millisecondsSinceEpoch}';
|
||||||
|
final task = DriveTask(
|
||||||
|
id: taskId,
|
||||||
|
taskId: taskId,
|
||||||
|
fileName: item.name,
|
||||||
|
contentType: item.mimeType ?? '',
|
||||||
|
fileSize: 0,
|
||||||
|
uploadedBytes: 0,
|
||||||
|
totalChunks: 1,
|
||||||
|
uploadedChunks: 0,
|
||||||
|
status: DriveTaskStatus.inProgress,
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
type: 'FileDownload',
|
||||||
|
);
|
||||||
|
state = [...state, task];
|
||||||
|
return taskId;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_websocketSubscription?.cancel();
|
_websocketSubscription?.cancel();
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import 'package:dio/dio.dart';
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:firebase_analytics/firebase_analytics.dart';
|
import 'package:firebase_analytics/firebase_analytics.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_platform_alert/flutter_platform_alert.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:island/widgets/alert.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/account.dart';
|
import 'package:island/models/account.dart';
|
||||||
@@ -36,41 +37,65 @@ class UserInfoNotifier extends StateNotifier<AsyncValue<SnAccount?>> {
|
|||||||
} catch (error, stackTrace) {
|
} catch (error, stackTrace) {
|
||||||
if (!kIsWeb) {
|
if (!kIsWeb) {
|
||||||
if (error is DioException) {
|
if (error is DioException) {
|
||||||
FlutterPlatformAlert.showCustomAlert(
|
showOverlayDialog<bool>(
|
||||||
windowTitle: 'failedToLoadUserInfo'.tr(),
|
builder:
|
||||||
text: [
|
(context, close) => AlertDialog(
|
||||||
(error.response?.statusCode == 401
|
title: Text('failedToLoadUserInfo'.tr()),
|
||||||
? 'failedToLoadUserInfoUnauthorized'
|
content: Text(
|
||||||
: 'failedToLoadUserInfoNetwork')
|
[
|
||||||
.tr()
|
(error.response?.statusCode == 401
|
||||||
.trim(),
|
? 'failedToLoadUserInfoUnauthorized'
|
||||||
'',
|
: 'failedToLoadUserInfoNetwork')
|
||||||
'${error.response?.statusCode ?? 'Network Error'}',
|
.tr()
|
||||||
if (error.response?.headers != null) error.response?.headers,
|
.trim(),
|
||||||
if (error.response?.data != null)
|
'',
|
||||||
jsonEncode(error.response?.data),
|
'${error.response?.statusCode ?? 'Network Error'}',
|
||||||
].join('\n'),
|
if (error.response?.headers != null)
|
||||||
iconStyle: IconStyle.error,
|
error.response?.headers,
|
||||||
neutralButtonTitle: 'retry'.tr(),
|
if (error.response?.data != null)
|
||||||
negativeButtonTitle: 'okay'.tr(),
|
jsonEncode(error.response?.data),
|
||||||
|
].join('\n'),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => close(false),
|
||||||
|
child: Text('okay'.tr()),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => close(true),
|
||||||
|
child: Text('retry'.tr()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
).then((value) {
|
).then((value) {
|
||||||
if (value == CustomButton.neutralButton) {
|
if (value == true) {
|
||||||
fetchUser();
|
fetchUser();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
FlutterPlatformAlert.showCustomAlert(
|
showOverlayDialog<bool>(
|
||||||
windowTitle: 'failedToLoadUserInfo'.tr(),
|
builder:
|
||||||
text:
|
(context, close) => AlertDialog(
|
||||||
[
|
title: Text('failedToLoadUserInfo'.tr()),
|
||||||
'failedToLoadUserInfoNetwork'.tr(),
|
content: Text(
|
||||||
error.toString(),
|
[
|
||||||
].join('\n\n').trim(),
|
'failedToLoadUserInfoNetwork'.tr(),
|
||||||
iconStyle: IconStyle.error,
|
error.toString(),
|
||||||
neutralButtonTitle: 'retry'.tr(),
|
].join('\n\n').trim(),
|
||||||
negativeButtonTitle: 'okay'.tr(),
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => close(false),
|
||||||
|
child: Text('okay'.tr()),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => close(true),
|
||||||
|
child: Text('retry'.tr()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
).then((value) {
|
).then((value) {
|
||||||
if (value == CustomButton.neutralButton) {
|
if (value == true) {
|
||||||
fetchUser();
|
fetchUser();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ import 'package:island/screens/account/me/account_settings.dart';
|
|||||||
import 'package:island/screens/chat/chat.dart';
|
import 'package:island/screens/chat/chat.dart';
|
||||||
import 'package:island/screens/chat/room.dart';
|
import 'package:island/screens/chat/room.dart';
|
||||||
import 'package:island/screens/chat/room_detail.dart';
|
import 'package:island/screens/chat/room_detail.dart';
|
||||||
import 'package:island/screens/chat/call.dart';
|
|
||||||
import 'package:island/screens/chat/search_messages.dart';
|
import 'package:island/screens/chat/search_messages.dart';
|
||||||
import 'package:island/screens/thought/think.dart';
|
import 'package:island/screens/thought/think.dart';
|
||||||
import 'package:island/screens/creators/hub.dart';
|
import 'package:island/screens/creators/hub.dart';
|
||||||
@@ -43,6 +42,8 @@ import 'package:island/screens/stickers/pack_detail.dart';
|
|||||||
import 'package:island/screens/discovery/feeds/feed_marketplace.dart';
|
import 'package:island/screens/discovery/feeds/feed_marketplace.dart';
|
||||||
import 'package:island/screens/discovery/feeds/feed_detail.dart';
|
import 'package:island/screens/discovery/feeds/feed_detail.dart';
|
||||||
import 'package:island/screens/creators/poll/poll_list.dart';
|
import 'package:island/screens/creators/poll/poll_list.dart';
|
||||||
|
import 'package:island/screens/creators/sites/site_detail.dart';
|
||||||
|
import 'package:island/screens/creators/sites/site_list.dart';
|
||||||
import 'package:island/screens/creators/webfeed/webfeed_list.dart';
|
import 'package:island/screens/creators/webfeed/webfeed_list.dart';
|
||||||
import 'package:island/screens/posts/compose.dart';
|
import 'package:island/screens/posts/compose.dart';
|
||||||
import 'package:island/screens/posts/compose_article.dart';
|
import 'package:island/screens/posts/compose_article.dart';
|
||||||
@@ -117,14 +118,6 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
return ArticleEditScreen(id: id);
|
return ArticleEditScreen(id: id);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
GoRoute(
|
|
||||||
name: 'chatCall',
|
|
||||||
path: '/chat/:id/call',
|
|
||||||
builder: (context, state) {
|
|
||||||
final id = state.pathParameters['id']!;
|
|
||||||
return CallScreen(roomId: id);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
GoRoute(
|
GoRoute(
|
||||||
name: 'logs',
|
name: 'logs',
|
||||||
path: '/logs',
|
path: '/logs',
|
||||||
@@ -170,6 +163,22 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
builder: (context, state) => const AboutScreen(),
|
builder: (context, state) => const AboutScreen(),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
GoRoute(
|
||||||
|
name: 'fileDetail',
|
||||||
|
path: '/files/:id',
|
||||||
|
builder: (context, state) {
|
||||||
|
// For now, we'll need to pass the file object through extra
|
||||||
|
// This will be updated when we modify the file list navigation
|
||||||
|
final file = state.extra as SnCloudFile?;
|
||||||
|
if (file != null) {
|
||||||
|
return FileDetailScreen(item: file);
|
||||||
|
}
|
||||||
|
// Fallback - this shouldn't happen in normal flow
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
// Main tabs with TabsScreen shell
|
// Main tabs with TabsScreen shell
|
||||||
ShellRoute(
|
ShellRoute(
|
||||||
navigatorKey: _tabsShellKey,
|
navigatorKey: _tabsShellKey,
|
||||||
@@ -427,23 +436,6 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
name: 'files',
|
name: 'files',
|
||||||
path: '/files',
|
path: '/files',
|
||||||
builder: (context, state) => const FileListScreen(),
|
builder: (context, state) => const FileListScreen(),
|
||||||
routes: [
|
|
||||||
GoRoute(
|
|
||||||
name: 'fileDetail',
|
|
||||||
path: ':id',
|
|
||||||
builder: (context, state) {
|
|
||||||
// For now, we'll need to pass the file object through extra
|
|
||||||
// This will be updated when we modify the file list navigation
|
|
||||||
final file = state.extra as SnCloudFile?;
|
|
||||||
if (file != null) {
|
|
||||||
return FileDetailScreen(item: file);
|
|
||||||
}
|
|
||||||
// Fallback - this shouldn't happen in normal flow
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
|
|
||||||
// SN-chan tab
|
// SN-chan tab
|
||||||
@@ -485,6 +477,29 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
return CreatorPollListScreen(pubName: name);
|
return CreatorPollListScreen(pubName: name);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
// Site list route
|
||||||
|
GoRoute(
|
||||||
|
name: 'creatorSites',
|
||||||
|
path: ':name/sites',
|
||||||
|
builder: (context, state) {
|
||||||
|
final name = state.pathParameters['name']!;
|
||||||
|
return CreatorSiteListScreen(pubName: name);
|
||||||
|
},
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
name: 'creatorSiteDetail',
|
||||||
|
path: ':siteSlug',
|
||||||
|
builder: (context, state) {
|
||||||
|
final name = state.pathParameters['name']!;
|
||||||
|
final siteSlug = state.pathParameters['siteSlug']!;
|
||||||
|
return PublicationSiteDetailScreen(
|
||||||
|
siteSlug: siteSlug,
|
||||||
|
pubName: name,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
GoRoute(
|
GoRoute(
|
||||||
name: 'creatorStickers',
|
name: 'creatorStickers',
|
||||||
|
|||||||
@@ -384,9 +384,7 @@ class _AboutScreenState extends ConsumerState<AboutScreen> {
|
|||||||
icon: const Icon(Symbols.content_copy, size: 16),
|
icon: const Icon(Symbols.content_copy, size: 16),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Clipboard.setData(ClipboardData(text: value));
|
Clipboard.setData(ClipboardData(text: value));
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
showSnackBar('copiedToClipboard'.tr());
|
||||||
SnackBar(content: Text('copiedToClipboard'.tr())),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
constraints: const BoxConstraints(),
|
constraints: const BoxConstraints(),
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ class AccountSettingsScreen extends HookConsumerWidget {
|
|||||||
final confirm = await showConfirmAlert(
|
final confirm = await showConfirmAlert(
|
||||||
'accountDeletionHint'.tr(),
|
'accountDeletionHint'.tr(),
|
||||||
'accountDeletion'.tr(),
|
'accountDeletion'.tr(),
|
||||||
|
isDanger: true,
|
||||||
);
|
);
|
||||||
if (!confirm || !context.mounted) return;
|
if (!confirm || !context.mounted) return;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class AuthFactorSheet extends HookConsumerWidget {
|
|||||||
final confirm = await showConfirmAlert(
|
final confirm = await showConfirmAlert(
|
||||||
'authFactorDeleteHint'.tr(),
|
'authFactorDeleteHint'.tr(),
|
||||||
'authFactorDelete'.tr(),
|
'authFactorDelete'.tr(),
|
||||||
|
isDanger: true,
|
||||||
);
|
);
|
||||||
if (!confirm || !context.mounted) return;
|
if (!confirm || !context.mounted) return;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ class AccountConnectionSheet extends HookConsumerWidget {
|
|||||||
final confirm = await showConfirmAlert(
|
final confirm = await showConfirmAlert(
|
||||||
'accountConnectionDeleteHint'.tr(),
|
'accountConnectionDeleteHint'.tr(),
|
||||||
'accountConnectionDelete'.tr(),
|
'accountConnectionDelete'.tr(),
|
||||||
|
isDanger: true,
|
||||||
);
|
);
|
||||||
if (!confirm || !context.mounted) return;
|
if (!confirm || !context.mounted) return;
|
||||||
try {
|
try {
|
||||||
@@ -332,6 +333,7 @@ class AccountConnectionsSheet extends HookConsumerWidget {
|
|||||||
final confirm = await showConfirmAlert(
|
final confirm = await showConfirmAlert(
|
||||||
'accountConnectionDeleteHint'.tr(),
|
'accountConnectionDeleteHint'.tr(),
|
||||||
'accountConnectionDelete'.tr(),
|
'accountConnectionDelete'.tr(),
|
||||||
|
isDanger: true,
|
||||||
);
|
);
|
||||||
if (confirm && context.mounted) {
|
if (confirm && context.mounted) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class ContactMethodSheet extends HookConsumerWidget {
|
|||||||
final confirm = await showConfirmAlert(
|
final confirm = await showConfirmAlert(
|
||||||
'contactMethodDeleteHint'.tr(),
|
'contactMethodDeleteHint'.tr(),
|
||||||
'contactMethodDelete'.tr(),
|
'contactMethodDelete'.tr(),
|
||||||
|
isDanger: true,
|
||||||
);
|
);
|
||||||
if (!confirm || !context.mounted) return;
|
if (!confirm || !context.mounted) return;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ import 'package:island/widgets/content/sheet.dart';
|
|||||||
|
|
||||||
class CaptchaScreen extends ConsumerWidget {
|
class CaptchaScreen extends ConsumerWidget {
|
||||||
static Future<String?> show(BuildContext context) {
|
static Future<String?> show(BuildContext context) {
|
||||||
return showModalBottomSheet<String>(
|
return Navigator.push<String>(
|
||||||
context: context,
|
context,
|
||||||
isScrollControlled: true,
|
MaterialPageRoute(
|
||||||
builder: (context) => const CaptchaScreen(),
|
builder: (context) => const CaptchaScreen(),
|
||||||
|
fullscreenDialog: true,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// ignore_for_file: invalid_runtime_check_with_js_interop_types
|
|
||||||
|
|
||||||
import 'dart:ui_web' as ui;
|
import 'dart:ui_web' as ui;
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/pods/config.dart';
|
import 'package:island/pods/config.dart';
|
||||||
@@ -10,10 +8,12 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
class CaptchaScreen extends ConsumerStatefulWidget {
|
class CaptchaScreen extends ConsumerStatefulWidget {
|
||||||
static Future<String?> show(BuildContext context) {
|
static Future<String?> show(BuildContext context) {
|
||||||
return showModalBottomSheet<String>(
|
return Navigator.push<String>(
|
||||||
context: context,
|
context,
|
||||||
isScrollControlled: true,
|
MaterialPageRoute(
|
||||||
builder: (context) => const CaptchaScreen(),
|
builder: (context) => const CaptchaScreen(),
|
||||||
|
fullscreenDialog: true,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,7 +28,9 @@ class _CaptchaScreenState extends ConsumerState<CaptchaScreen> {
|
|||||||
|
|
||||||
void _setupWebListener(String serverUrl) async {
|
void _setupWebListener(String serverUrl) async {
|
||||||
web.window.onMessage.listen((event) {
|
web.window.onMessage.listen((event) {
|
||||||
|
// ignore: invalid_runtime_check_with_js_interop_types
|
||||||
if (event.data != null && event.data is String) {
|
if (event.data != null && event.data is String) {
|
||||||
|
// ignore: invalid_runtime_check_with_js_interop_types
|
||||||
final message = event.data as String;
|
final message = event.data as String;
|
||||||
if (message.startsWith("captcha_tk=")) {
|
if (message.startsWith("captcha_tk=")) {
|
||||||
String token = message.replaceFirst("captcha_tk=", "");
|
String token = message.replaceFirst("captcha_tk=", "");
|
||||||
|
|||||||
@@ -3,30 +3,31 @@ import 'package:flutter/material.dart' hide ConnectionState;
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:island/models/chat.dart';
|
||||||
import 'package:island/pods/chat/call.dart';
|
import 'package:island/pods/chat/call.dart';
|
||||||
import 'package:island/talker.dart';
|
import 'package:island/talker.dart';
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
import 'package:island/widgets/chat/call_button.dart';
|
import 'package:island/widgets/chat/call_button.dart';
|
||||||
|
import 'package:island/widgets/chat/call_content.dart';
|
||||||
import 'package:island/widgets/chat/call_overlay.dart';
|
import 'package:island/widgets/chat/call_overlay.dart';
|
||||||
import 'package:island/widgets/chat/call_participant_tile.dart';
|
import 'package:island/widgets/chat/call_participant_tile.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
import 'package:livekit_client/livekit_client.dart';
|
import 'package:livekit_client/livekit_client.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
|
||||||
|
|
||||||
class CallScreen extends HookConsumerWidget {
|
class CallScreen extends HookConsumerWidget {
|
||||||
final String roomId;
|
final SnChatRoom room;
|
||||||
const CallScreen({super.key, required this.roomId});
|
const CallScreen({super.key, required this.room});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final ongoingCall = ref.watch(ongoingCallProvider(roomId));
|
final ongoingCall = ref.watch(ongoingCallProvider(room.id));
|
||||||
final callState = ref.watch(callNotifierProvider);
|
final callState = ref.watch(callNotifierProvider);
|
||||||
final callNotifier = ref.watch(callNotifierProvider.notifier);
|
final callNotifier = ref.watch(callNotifierProvider.notifier);
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
talker.info('[Call] Joining the call...');
|
talker.info('[Call] Joining the call...');
|
||||||
callNotifier.joinRoom(roomId).catchError((_) {
|
callNotifier.joinRoom(room).catchError((_) {
|
||||||
showConfirmAlert(
|
showConfirmAlert(
|
||||||
'Seems there already has a call connected, do you want override it?',
|
'Seems there already has a call connected, do you want override it?',
|
||||||
'Call already connected',
|
'Call already connected',
|
||||||
@@ -35,7 +36,7 @@ class CallScreen extends HookConsumerWidget {
|
|||||||
talker.info('[Call] Joining the call... with overrides');
|
talker.info('[Call] Joining the call... with overrides');
|
||||||
callNotifier.disconnect();
|
callNotifier.disconnect();
|
||||||
callNotifier.dispose();
|
callNotifier.dispose();
|
||||||
callNotifier.joinRoom(roomId);
|
callNotifier.joinRoom(room);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return null;
|
return null;
|
||||||
@@ -110,7 +111,7 @@ class CallScreen extends HookConsumerWidget {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
callNotifier.disconnect();
|
callNotifier.disconnect();
|
||||||
callNotifier.dispose();
|
callNotifier.dispose();
|
||||||
callNotifier.joinRoom(roomId);
|
callNotifier.joinRoom(room);
|
||||||
},
|
},
|
||||||
child: Text('retry').tr(),
|
child: Text('retry').tr(),
|
||||||
),
|
),
|
||||||
@@ -120,72 +121,7 @@ class CallScreen extends HookConsumerWidget {
|
|||||||
)
|
)
|
||||||
: Column(
|
: Column(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(child: CallContent()),
|
||||||
child: Builder(
|
|
||||||
builder: (context) {
|
|
||||||
if (!callState.isConnected) {
|
|
||||||
return const Center(
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (callNotifier.participants.isEmpty) {
|
|
||||||
return const Center(
|
|
||||||
child: Text('No participants in call'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final participants = callNotifier.participants;
|
|
||||||
if (allAudioOnly) {
|
|
||||||
// Audio-only: show avatars in a compact row
|
|
||||||
return Center(
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
child: Wrap(
|
|
||||||
crossAxisAlignment: WrapCrossAlignment.center,
|
|
||||||
alignment: WrapAlignment.center,
|
|
||||||
spacing: 8,
|
|
||||||
runSpacing: 8,
|
|
||||||
children: [
|
|
||||||
for (final live in participants)
|
|
||||||
SpeakingRippleAvatar(
|
|
||||||
live: live,
|
|
||||||
size: 72,
|
|
||||||
).padding(horizontal: 4),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stage view: show main speaker(s) large, others in row
|
|
||||||
final mainSpeakers =
|
|
||||||
participants
|
|
||||||
.where(
|
|
||||||
(p) => p
|
|
||||||
.remoteParticipant
|
|
||||||
.trackPublications
|
|
||||||
.values
|
|
||||||
.any(
|
|
||||||
(pub) =>
|
|
||||||
pub.track != null &&
|
|
||||||
pub.kind == TrackType.VIDEO,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
if (mainSpeakers.isEmpty && participants.isNotEmpty) {
|
|
||||||
mainSpeakers.add(participants.first);
|
|
||||||
}
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
for (final speaker in mainSpeakers)
|
|
||||||
Expanded(
|
|
||||||
child: CallParticipantTile(live: speaker),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
CallControlsBar(),
|
CallControlsBar(),
|
||||||
Gap(MediaQuery.of(context).padding.bottom + 16),
|
Gap(MediaQuery.of(context).padding.bottom + 16),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.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';
|
||||||
@@ -6,9 +8,12 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/chat.dart';
|
import 'package:island/models/chat.dart';
|
||||||
import 'package:island/pods/chat/call.dart';
|
import 'package:island/models/file.dart';
|
||||||
|
import 'package:island/models/account.dart';
|
||||||
|
import 'package:island/pods/database.dart';
|
||||||
import 'package:island/pods/chat/chat_summary.dart';
|
import 'package:island/pods/chat/chat_summary.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
|
import 'package:island/pods/userinfo.dart';
|
||||||
import 'package:island/screens/realm/realms.dart';
|
import 'package:island/screens/realm/realms.dart';
|
||||||
import 'package:island/services/event_bus.dart';
|
import 'package:island/services/event_bus.dart';
|
||||||
import 'package:island/services/responsive.dart';
|
import 'package:island/services/responsive.dart';
|
||||||
@@ -22,6 +27,7 @@ import 'package:material_symbols_icons/symbols.dart';
|
|||||||
import 'package:relative_time/relative_time.dart';
|
import 'package:relative_time/relative_time.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:super_sliver_list/super_sliver_list.dart';
|
||||||
|
|
||||||
part 'chat.g.dart';
|
part 'chat.g.dart';
|
||||||
|
|
||||||
@@ -47,6 +53,17 @@ class ChatRoomListTile extends HookConsumerWidget {
|
|||||||
.watch(chatSummaryProvider)
|
.watch(chatSummaryProvider)
|
||||||
.whenData((summaries) => summaries[room.id]);
|
.whenData((summaries) => summaries[room.id]);
|
||||||
|
|
||||||
|
var validMembers = room.members ?? [];
|
||||||
|
if (validMembers.isNotEmpty) {
|
||||||
|
final userInfo = ref.watch(userInfoProvider);
|
||||||
|
if (userInfo.value != null) {
|
||||||
|
validMembers =
|
||||||
|
validMembers
|
||||||
|
.where((e) => e.accountId != userInfo.value!.id)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Widget buildSubtitle() {
|
Widget buildSubtitle() {
|
||||||
if (subtitle != null) return subtitle!;
|
if (subtitle != null) return subtitle!;
|
||||||
|
|
||||||
@@ -55,7 +72,7 @@ class ChatRoomListTile extends HookConsumerWidget {
|
|||||||
if (data == null) {
|
if (data == null) {
|
||||||
return isDirect && room.description == null
|
return isDirect && room.description == null
|
||||||
? Text(
|
? Text(
|
||||||
room.members!.map((e) => '@${e.account.name}').join(', '),
|
validMembers.map((e) => '@${e.account.name}').join(', '),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
)
|
)
|
||||||
: Text(room.description ?? 'descriptionNone'.tr(), maxLines: 1);
|
: Text(room.description ?? 'descriptionNone'.tr(), maxLines: 1);
|
||||||
@@ -111,7 +128,7 @@ class ChatRoomListTile extends HookConsumerWidget {
|
|||||||
(_, _) =>
|
(_, _) =>
|
||||||
isDirect && room.description == null
|
isDirect && room.description == null
|
||||||
? Text(
|
? Text(
|
||||||
room.members!.map((e) => '@${e.account.name}').join(', '),
|
validMembers.map((e) => '@${e.account.name}').join(', '),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
)
|
)
|
||||||
: Text(
|
: Text(
|
||||||
@@ -121,6 +138,17 @@ class ChatRoomListTile extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String titleText;
|
||||||
|
if (isDirect && room.name == null) {
|
||||||
|
if (room.members?.isNotEmpty ?? false) {
|
||||||
|
titleText = validMembers.map((e) => e.account.nick).join(', ');
|
||||||
|
} else {
|
||||||
|
titleText = 'Direct Message';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
titleText = room.name ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: Badge(
|
leading: Badge(
|
||||||
isLabelVisible: summary.when(
|
isLabelVisible: summary.when(
|
||||||
@@ -132,7 +160,7 @@ class ChatRoomListTile extends HookConsumerWidget {
|
|||||||
(isDirect && room.picture?.id == null)
|
(isDirect && room.picture?.id == null)
|
||||||
? SplitAvatarWidget(
|
? SplitAvatarWidget(
|
||||||
filesId:
|
filesId:
|
||||||
room.members!
|
validMembers
|
||||||
.map((e) => e.account.profile.picture?.id)
|
.map((e) => e.account.profile.picture?.id)
|
||||||
.toList(),
|
.toList(),
|
||||||
)
|
)
|
||||||
@@ -140,11 +168,7 @@ class ChatRoomListTile extends HookConsumerWidget {
|
|||||||
? CircleAvatar(child: Text(room.name![0].toUpperCase()))
|
? CircleAvatar(child: Text(room.name![0].toUpperCase()))
|
||||||
: ProfilePictureWidget(fileId: room.picture?.id),
|
: ProfilePictureWidget(fileId: room.picture?.id),
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(titleText),
|
||||||
(isDirect && room.name == null)
|
|
||||||
? room.members!.map((e) => e.account.nick).join(', ')
|
|
||||||
: room.name ?? '',
|
|
||||||
),
|
|
||||||
subtitle: buildSubtitle(),
|
subtitle: buildSubtitle(),
|
||||||
trailing: trailing, // Add this line
|
trailing: trailing, // Add this line
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
@@ -162,12 +186,92 @@ class ChatRoomListTile extends HookConsumerWidget {
|
|||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
Future<List<SnChatRoom>> chatroomsJoined(Ref ref) async {
|
Future<List<SnChatRoom>> chatroomsJoined(Ref ref) async {
|
||||||
|
final db = ref.watch(databaseProvider);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final localRoomsData = await db.select(db.chatRooms).get();
|
||||||
|
if (localRoomsData.isNotEmpty) {
|
||||||
|
final localRooms = await Future.wait(
|
||||||
|
localRoomsData.map((row) async {
|
||||||
|
final membersRows =
|
||||||
|
await (db.select(db.chatMembers)
|
||||||
|
..where((m) => m.chatRoomId.equals(row.id))).get();
|
||||||
|
final members =
|
||||||
|
membersRows.map((mRow) {
|
||||||
|
final account = SnAccount.fromJson(mRow.account);
|
||||||
|
SnAccountStatus? status;
|
||||||
|
if (mRow.status != null) {
|
||||||
|
status = SnAccountStatus.fromJson(jsonDecode(mRow.status!));
|
||||||
|
}
|
||||||
|
return SnChatMember(
|
||||||
|
id: mRow.id,
|
||||||
|
chatRoomId: mRow.chatRoomId,
|
||||||
|
accountId: mRow.accountId,
|
||||||
|
account: account,
|
||||||
|
nick: mRow.nick,
|
||||||
|
role: mRow.role,
|
||||||
|
notify: mRow.notify,
|
||||||
|
joinedAt: mRow.joinedAt,
|
||||||
|
breakUntil: mRow.breakUntil,
|
||||||
|
timeoutUntil: mRow.timeoutUntil,
|
||||||
|
isBot: mRow.isBot,
|
||||||
|
status: status,
|
||||||
|
lastTyped: mRow.lastTyped,
|
||||||
|
createdAt: mRow.createdAt,
|
||||||
|
updatedAt: mRow.updatedAt,
|
||||||
|
deletedAt: mRow.deletedAt,
|
||||||
|
chatRoom: null,
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
return SnChatRoom(
|
||||||
|
id: row.id,
|
||||||
|
name: row.name,
|
||||||
|
description: row.description,
|
||||||
|
type: row.type,
|
||||||
|
isPublic: row.isPublic!,
|
||||||
|
isCommunity: row.isCommunity!,
|
||||||
|
picture:
|
||||||
|
row.picture != null ? SnCloudFile.fromJson(row.picture!) : null,
|
||||||
|
background:
|
||||||
|
row.background != null
|
||||||
|
? SnCloudFile.fromJson(row.background!)
|
||||||
|
: null,
|
||||||
|
realmId: row.realmId,
|
||||||
|
realm: null,
|
||||||
|
createdAt: row.createdAt,
|
||||||
|
updatedAt: row.updatedAt,
|
||||||
|
deletedAt: row.deletedAt,
|
||||||
|
members: members,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Background sync
|
||||||
|
Future(() async {
|
||||||
|
try {
|
||||||
|
final client = ref.read(apiClientProvider);
|
||||||
|
final resp = await client.get('/sphere/chat');
|
||||||
|
final remoteRooms =
|
||||||
|
resp.data
|
||||||
|
.map((e) => SnChatRoom.fromJson(e))
|
||||||
|
.cast<SnChatRoom>()
|
||||||
|
.toList();
|
||||||
|
await db.saveChatRooms(remoteRooms);
|
||||||
|
ref.invalidateSelf();
|
||||||
|
} catch (_) {}
|
||||||
|
}).ignore();
|
||||||
|
|
||||||
|
return localRooms;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
// Fallback to API
|
||||||
final client = ref.watch(apiClientProvider);
|
final client = ref.watch(apiClientProvider);
|
||||||
final resp = await client.get('/sphere/chat');
|
final resp = await client.get('/sphere/chat');
|
||||||
return resp.data
|
final rooms =
|
||||||
.map((e) => SnChatRoom.fromJson(e))
|
resp.data.map((e) => SnChatRoom.fromJson(e)).cast<SnChatRoom>().toList();
|
||||||
.cast<SnChatRoom>()
|
await db.saveChatRooms(rooms);
|
||||||
.toList();
|
return rooms;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ChatListBodyWidget extends HookConsumerWidget {
|
class ChatListBodyWidget extends HookConsumerWidget {
|
||||||
@@ -185,7 +289,6 @@ class ChatListBodyWidget extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final chats = ref.watch(chatroomsJoinedProvider);
|
final chats = ref.watch(chatroomsJoinedProvider);
|
||||||
final callState = ref.watch(callNotifierProvider);
|
|
||||||
|
|
||||||
Widget bodyWidget = Column(
|
Widget bodyWidget = Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -210,10 +313,8 @@ class ChatListBodyWidget extends HookConsumerWidget {
|
|||||||
() => Future.sync(() {
|
() => Future.sync(() {
|
||||||
ref.invalidate(chatroomsJoinedProvider);
|
ref.invalidate(chatroomsJoinedProvider);
|
||||||
}),
|
}),
|
||||||
child: ListView.builder(
|
child: SuperListView.builder(
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(bottom: 96),
|
||||||
bottom: callState.isConnected ? 96 : 0,
|
|
||||||
),
|
|
||||||
itemCount:
|
itemCount:
|
||||||
items
|
items
|
||||||
.where(
|
.where(
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ part of 'chat.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$chatroomsJoinedHash() => r'3bb6389af07e81007680484d04bf5fe6f6c10571';
|
String _$chatroomsJoinedHash() => r'9523efecd1869e7dd26adfc8ec87be48db19ee1c';
|
||||||
|
|
||||||
/// See also [chatroomsJoined].
|
/// See also [chatroomsJoined].
|
||||||
@ProviderFor(chatroomsJoined)
|
@ProviderFor(chatroomsJoined)
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import "package:island/widgets/chat/chat_input.dart";
|
|||||||
import "package:island/widgets/chat/chat_link_attachments.dart";
|
import "package:island/widgets/chat/chat_link_attachments.dart";
|
||||||
import "package:island/widgets/chat/public_room_preview.dart";
|
import "package:island/widgets/chat/public_room_preview.dart";
|
||||||
import "package:island/screens/thought/think_sheet.dart";
|
import "package:island/screens/thought/think_sheet.dart";
|
||||||
|
import "package:island/screens/chat/widgets/message_item_wrapper.dart";
|
||||||
|
|
||||||
class ChatRoomScreen extends HookConsumerWidget {
|
class ChatRoomScreen extends HookConsumerWidget {
|
||||||
final String id;
|
final String id;
|
||||||
@@ -148,9 +149,6 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
final inputKey = useMemoized(() => GlobalKey());
|
final inputKey = useMemoized(() => GlobalKey());
|
||||||
final inputHeight = useState<double>(80.0);
|
final inputHeight = useState<double>(80.0);
|
||||||
|
|
||||||
// Track previous height for smooth animations
|
|
||||||
final previousInputHeight = usePrevious<double>(inputHeight.value);
|
|
||||||
|
|
||||||
// Periodic height measurement for dynamic sizing
|
// Periodic height measurement for dynamic sizing
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
final timer = Timer.periodic(const Duration(milliseconds: 50), (_) {
|
final timer = Timer.periodic(const Duration(milliseconds: 50), (_) {
|
||||||
@@ -181,6 +179,38 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
final isSelectionMode = useState<bool>(false);
|
final isSelectionMode = useState<bool>(false);
|
||||||
final selectedMessages = useState<Set<String>>({});
|
final selectedMessages = useState<Set<String>>({});
|
||||||
|
|
||||||
|
final roomOpenTime = useMemoized(() => DateTime.now());
|
||||||
|
|
||||||
|
final onMessageAction = useCallback(
|
||||||
|
(String action, LocalChatMessage message) {
|
||||||
|
switch (action) {
|
||||||
|
case MessageItemAction.delete:
|
||||||
|
messagesNotifier.deleteMessage(message.id);
|
||||||
|
case MessageItemAction.edit:
|
||||||
|
messageEditingTo.value = message.toRemoteMessage();
|
||||||
|
messageController.text = messageEditingTo.value?.content ?? '';
|
||||||
|
attachments.value =
|
||||||
|
messageEditingTo.value!.attachments
|
||||||
|
.map((e) => UniversalFile.fromAttachment(e))
|
||||||
|
.toList();
|
||||||
|
case MessageItemAction.forward:
|
||||||
|
messageForwardingTo.value = message.toRemoteMessage();
|
||||||
|
case MessageItemAction.reply:
|
||||||
|
messageReplyingTo.value = message.toRemoteMessage();
|
||||||
|
case MessageItemAction.resend:
|
||||||
|
messagesNotifier.retryMessage(message.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
messagesNotifier,
|
||||||
|
messageEditingTo,
|
||||||
|
messageController,
|
||||||
|
attachments,
|
||||||
|
messageForwardingTo,
|
||||||
|
messageReplyingTo,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
var isLoading = false;
|
var isLoading = false;
|
||||||
var isScrollingToMessage = false; // Flag to prevent scroll conflicts
|
var isScrollingToMessage = false; // Flag to prevent scroll conflicts
|
||||||
|
|
||||||
@@ -624,428 +654,70 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget chatMessageListWidget(List<LocalChatMessage> messageList) =>
|
Widget chatMessageListWidget(
|
||||||
previousInputHeight != null && previousInputHeight != inputHeight.value
|
List<LocalChatMessage> messageList,
|
||||||
? TweenAnimationBuilder<double>(
|
) => AnimatedPadding(
|
||||||
tween: Tween<double>(
|
duration: const Duration(milliseconds: 200),
|
||||||
begin: previousInputHeight,
|
curve: Curves.easeOut,
|
||||||
end: inputHeight.value,
|
padding: EdgeInsets.only(
|
||||||
),
|
bottom: MediaQuery.of(context).padding.bottom + 8 + inputHeight.value,
|
||||||
duration: const Duration(milliseconds: 200),
|
),
|
||||||
curve: Curves.easeOut,
|
child: SuperListView.builder(
|
||||||
builder:
|
listController: listController,
|
||||||
(context, height, child) => SuperListView.builder(
|
controller: scrollController,
|
||||||
listController: listController,
|
reverse: true, // Show newest messages at the bottom
|
||||||
padding: EdgeInsets.only(
|
itemCount: messageList.length,
|
||||||
top: 16,
|
findChildIndexCallback: (key) {
|
||||||
bottom:
|
if (key is! ValueKey<String>) return null;
|
||||||
MediaQuery.of(context).padding.bottom + 8 + height,
|
final messageId = key.value.substring(messageKeyPrefix.length);
|
||||||
),
|
final index = messageList.indexWhere(
|
||||||
controller: scrollController,
|
(m) => (m.nonce ?? m.id) == messageId,
|
||||||
reverse: true, // Show newest messages at the bottom
|
);
|
||||||
itemCount: messageList.length,
|
return index >= 0 ? index : null;
|
||||||
findChildIndexCallback: (key) {
|
},
|
||||||
if (key is! ValueKey<String>) return null;
|
extentEstimation: (_, _) => 40,
|
||||||
final messageId = key.value.substring(
|
itemBuilder: (context, index) {
|
||||||
messageKeyPrefix.length,
|
final message = messageList[index];
|
||||||
);
|
final nextMessage =
|
||||||
final index = messageList.indexWhere(
|
index < messageList.length - 1 ? messageList[index + 1] : null;
|
||||||
(m) => (m.nonce ?? m.id) == messageId,
|
final isLastInGroup =
|
||||||
);
|
nextMessage == null ||
|
||||||
// Return null for invalid indices to let SuperListView handle it properly
|
nextMessage.senderId != message.senderId ||
|
||||||
return index >= 0 ? index : null;
|
nextMessage.createdAt
|
||||||
},
|
.difference(message.createdAt)
|
||||||
extentEstimation: (_, _) => 40,
|
.inMinutes
|
||||||
itemBuilder: (context, index) {
|
.abs() >
|
||||||
final message = messageList[index];
|
3;
|
||||||
final nextMessage =
|
|
||||||
index < messageList.length - 1
|
|
||||||
? messageList[index + 1]
|
|
||||||
: null;
|
|
||||||
final isLastInGroup =
|
|
||||||
nextMessage == null ||
|
|
||||||
nextMessage.senderId != message.senderId ||
|
|
||||||
nextMessage.createdAt
|
|
||||||
.difference(message.createdAt)
|
|
||||||
.inMinutes
|
|
||||||
.abs() >
|
|
||||||
3;
|
|
||||||
|
|
||||||
// Use a stable animation key that doesn't change during message lifecycle
|
final key = Key('$messageKeyPrefix${message.nonce ?? message.id}');
|
||||||
final key = Key(
|
|
||||||
'$messageKeyPrefix${message.nonce ?? message.id}',
|
|
||||||
);
|
|
||||||
|
|
||||||
final messageWidget = chatIdentity.when(
|
return MessageItemWrapper(
|
||||||
skipError: true,
|
key: key,
|
||||||
data:
|
message: message,
|
||||||
(identity) => GestureDetector(
|
index: index,
|
||||||
onLongPress: () {
|
isLastInGroup: isLastInGroup,
|
||||||
if (!isSelectionMode.value) {
|
isSelectionMode: isSelectionMode.value,
|
||||||
toggleSelectionMode();
|
selectedMessages: selectedMessages.value,
|
||||||
toggleMessageSelection(message.id);
|
chatIdentity: chatIdentity,
|
||||||
}
|
toggleSelectionMode: toggleSelectionMode,
|
||||||
},
|
toggleMessageSelection: toggleMessageSelection,
|
||||||
onTap: () {
|
onMessageAction: onMessageAction,
|
||||||
if (isSelectionMode.value) {
|
onJump:
|
||||||
toggleMessageSelection(message.id);
|
(messageId) => scrollToMessage(
|
||||||
}
|
messageId: messageId,
|
||||||
},
|
messageList: messageList,
|
||||||
child: Container(
|
messagesNotifier: messagesNotifier,
|
||||||
color:
|
listController: listController,
|
||||||
selectedMessages.value.contains(message.id)
|
scrollController: scrollController,
|
||||||
? Theme.of(context)
|
ref: ref,
|
||||||
.colorScheme
|
),
|
||||||
.primaryContainer
|
attachmentProgress: attachmentProgress.value,
|
||||||
.withOpacity(0.3)
|
disableAnimation: settings.disableAnimation,
|
||||||
: null,
|
roomOpenTime: roomOpenTime,
|
||||||
child: Stack(
|
);
|
||||||
children: [
|
},
|
||||||
MessageItem(
|
),
|
||||||
key:
|
);
|
||||||
settings.disableAnimation
|
|
||||||
? key
|
|
||||||
: null,
|
|
||||||
message: message,
|
|
||||||
isCurrentUser:
|
|
||||||
identity?.id == message.senderId,
|
|
||||||
onAction:
|
|
||||||
isSelectionMode.value
|
|
||||||
? null
|
|
||||||
: (action) {
|
|
||||||
switch (action) {
|
|
||||||
case MessageItemAction.delete:
|
|
||||||
messagesNotifier
|
|
||||||
.deleteMessage(
|
|
||||||
message.id,
|
|
||||||
);
|
|
||||||
case MessageItemAction.edit:
|
|
||||||
messageEditingTo.value =
|
|
||||||
message
|
|
||||||
.toRemoteMessage();
|
|
||||||
messageController.text =
|
|
||||||
messageEditingTo
|
|
||||||
.value
|
|
||||||
?.content ??
|
|
||||||
'';
|
|
||||||
attachments.value =
|
|
||||||
messageEditingTo
|
|
||||||
.value!
|
|
||||||
.attachments
|
|
||||||
.map(
|
|
||||||
(e) =>
|
|
||||||
UniversalFile.fromAttachment(
|
|
||||||
e,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
case MessageItemAction
|
|
||||||
.forward:
|
|
||||||
messageForwardingTo.value =
|
|
||||||
message
|
|
||||||
.toRemoteMessage();
|
|
||||||
case MessageItemAction.reply:
|
|
||||||
messageReplyingTo.value =
|
|
||||||
message
|
|
||||||
.toRemoteMessage();
|
|
||||||
case MessageItemAction.resend:
|
|
||||||
messagesNotifier
|
|
||||||
.retryMessage(
|
|
||||||
message.id,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onJump: (messageId) {
|
|
||||||
scrollToMessage(
|
|
||||||
messageId: messageId,
|
|
||||||
messageList: messageList,
|
|
||||||
messagesNotifier: messagesNotifier,
|
|
||||||
listController: listController,
|
|
||||||
scrollController: scrollController,
|
|
||||||
ref: ref,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
progress:
|
|
||||||
attachmentProgress.value[message.id],
|
|
||||||
showAvatar: isLastInGroup,
|
|
||||||
isSelectionMode: isSelectionMode.value,
|
|
||||||
isSelected: selectedMessages.value
|
|
||||||
.contains(message.id),
|
|
||||||
onToggleSelection: toggleMessageSelection,
|
|
||||||
onEnterSelectionMode: () {
|
|
||||||
if (!isSelectionMode.value) {
|
|
||||||
toggleSelectionMode();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (selectedMessages.value.contains(
|
|
||||||
message.id,
|
|
||||||
))
|
|
||||||
...([
|
|
||||||
Positioned(
|
|
||||||
top: 8,
|
|
||||||
right: 8,
|
|
||||||
child: Container(
|
|
||||||
width: 16,
|
|
||||||
height: 16,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color:
|
|
||||||
Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.primary,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
child: Icon(
|
|
||||||
Icons.check,
|
|
||||||
size: 12,
|
|
||||||
color:
|
|
||||||
Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.onPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
loading:
|
|
||||||
() => MessageItem(
|
|
||||||
message: message,
|
|
||||||
isCurrentUser: false,
|
|
||||||
onAction: null,
|
|
||||||
progress: null,
|
|
||||||
showAvatar: false,
|
|
||||||
onJump: (_) {},
|
|
||||||
),
|
|
||||||
error: (_, _) => const SizedBox.shrink(),
|
|
||||||
);
|
|
||||||
|
|
||||||
return settings.disableAnimation
|
|
||||||
? messageWidget
|
|
||||||
: TweenAnimationBuilder<double>(
|
|
||||||
key: key,
|
|
||||||
tween: Tween<double>(begin: 0.0, end: 1.0),
|
|
||||||
duration: Duration(
|
|
||||||
milliseconds: 400 + (index % 5) * 50,
|
|
||||||
), // Staggered delay
|
|
||||||
curve: Curves.easeOutCubic,
|
|
||||||
builder: (context, animationValue, child) {
|
|
||||||
return Transform.translate(
|
|
||||||
offset: Offset(
|
|
||||||
0,
|
|
||||||
20 * (1 - animationValue),
|
|
||||||
), // Slide up from bottom
|
|
||||||
child: Opacity(
|
|
||||||
opacity: animationValue,
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: messageWidget,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: SuperListView.builder(
|
|
||||||
listController: listController,
|
|
||||||
padding: EdgeInsets.only(
|
|
||||||
top: 16,
|
|
||||||
bottom:
|
|
||||||
MediaQuery.of(context).padding.bottom +
|
|
||||||
8 +
|
|
||||||
inputHeight.value,
|
|
||||||
),
|
|
||||||
controller: scrollController,
|
|
||||||
reverse: true, // Show newest messages at the bottom
|
|
||||||
itemCount: messageList.length,
|
|
||||||
findChildIndexCallback: (key) {
|
|
||||||
if (key is! ValueKey<String>) return null;
|
|
||||||
final messageId = key.value.substring(messageKeyPrefix.length);
|
|
||||||
final index = messageList.indexWhere(
|
|
||||||
(m) => (m.nonce ?? m.id) == messageId,
|
|
||||||
);
|
|
||||||
// Return null for invalid indices to let SuperListView handle it properly
|
|
||||||
return index >= 0 ? index : null;
|
|
||||||
},
|
|
||||||
extentEstimation: (_, _) => 40,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final message = messageList[index];
|
|
||||||
final nextMessage =
|
|
||||||
index < messageList.length - 1
|
|
||||||
? messageList[index + 1]
|
|
||||||
: null;
|
|
||||||
final isLastInGroup =
|
|
||||||
nextMessage == null ||
|
|
||||||
nextMessage.senderId != message.senderId ||
|
|
||||||
nextMessage.createdAt
|
|
||||||
.difference(message.createdAt)
|
|
||||||
.inMinutes
|
|
||||||
.abs() >
|
|
||||||
3;
|
|
||||||
|
|
||||||
// Use a stable animation key that doesn't change during message lifecycle
|
|
||||||
final key = Key(
|
|
||||||
'$messageKeyPrefix${message.nonce ?? message.id}',
|
|
||||||
);
|
|
||||||
|
|
||||||
final messageWidget = chatIdentity.when(
|
|
||||||
skipError: true,
|
|
||||||
data:
|
|
||||||
(identity) => GestureDetector(
|
|
||||||
onLongPress: () {
|
|
||||||
if (!isSelectionMode.value) {
|
|
||||||
toggleSelectionMode();
|
|
||||||
toggleMessageSelection(message.id);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onTap: () {
|
|
||||||
if (isSelectionMode.value) {
|
|
||||||
toggleMessageSelection(message.id);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Container(
|
|
||||||
color:
|
|
||||||
selectedMessages.value.contains(message.id)
|
|
||||||
? Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.primaryContainer
|
|
||||||
.withOpacity(0.3)
|
|
||||||
: null,
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
MessageItem(
|
|
||||||
key: settings.disableAnimation ? key : null,
|
|
||||||
message: message,
|
|
||||||
isCurrentUser: identity?.id == message.senderId,
|
|
||||||
onAction:
|
|
||||||
isSelectionMode.value
|
|
||||||
? null
|
|
||||||
: (action) {
|
|
||||||
switch (action) {
|
|
||||||
case MessageItemAction.delete:
|
|
||||||
messagesNotifier.deleteMessage(
|
|
||||||
message.id,
|
|
||||||
);
|
|
||||||
case MessageItemAction.edit:
|
|
||||||
messageEditingTo.value =
|
|
||||||
message.toRemoteMessage();
|
|
||||||
messageController.text =
|
|
||||||
messageEditingTo
|
|
||||||
.value
|
|
||||||
?.content ??
|
|
||||||
'';
|
|
||||||
attachments.value =
|
|
||||||
messageEditingTo
|
|
||||||
.value!
|
|
||||||
.attachments
|
|
||||||
.map(
|
|
||||||
(e) =>
|
|
||||||
UniversalFile.fromAttachment(
|
|
||||||
e,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
case MessageItemAction.forward:
|
|
||||||
messageForwardingTo.value =
|
|
||||||
message.toRemoteMessage();
|
|
||||||
case MessageItemAction.reply:
|
|
||||||
messageReplyingTo.value =
|
|
||||||
message.toRemoteMessage();
|
|
||||||
case MessageItemAction.resend:
|
|
||||||
messagesNotifier.retryMessage(
|
|
||||||
message.id,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onJump: (messageId) {
|
|
||||||
scrollToMessage(
|
|
||||||
messageId: messageId,
|
|
||||||
messageList: messageList,
|
|
||||||
messagesNotifier: messagesNotifier,
|
|
||||||
listController: listController,
|
|
||||||
scrollController: scrollController,
|
|
||||||
ref: ref,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
progress: attachmentProgress.value[message.id],
|
|
||||||
showAvatar: isLastInGroup,
|
|
||||||
isSelectionMode: isSelectionMode.value,
|
|
||||||
isSelected: selectedMessages.value.contains(
|
|
||||||
message.id,
|
|
||||||
),
|
|
||||||
onToggleSelection: toggleMessageSelection,
|
|
||||||
onEnterSelectionMode: () {
|
|
||||||
if (!isSelectionMode.value) {
|
|
||||||
toggleSelectionMode();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (selectedMessages.value.contains(message.id))
|
|
||||||
...([
|
|
||||||
Positioned(
|
|
||||||
top: 8,
|
|
||||||
right: 8,
|
|
||||||
child: Container(
|
|
||||||
width: 16,
|
|
||||||
height: 16,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color:
|
|
||||||
Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.primary,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
child: Icon(
|
|
||||||
Icons.check,
|
|
||||||
size: 12,
|
|
||||||
color:
|
|
||||||
Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.onPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
loading:
|
|
||||||
() => MessageItem(
|
|
||||||
message: message,
|
|
||||||
isCurrentUser: false,
|
|
||||||
onAction: null,
|
|
||||||
progress: null,
|
|
||||||
showAvatar: false,
|
|
||||||
onJump: (_) {},
|
|
||||||
),
|
|
||||||
error: (_, _) => const SizedBox.shrink(),
|
|
||||||
);
|
|
||||||
|
|
||||||
return settings.disableAnimation
|
|
||||||
? messageWidget
|
|
||||||
: TweenAnimationBuilder<double>(
|
|
||||||
key: key,
|
|
||||||
tween: Tween<double>(begin: 0.0, end: 1.0),
|
|
||||||
duration: Duration(
|
|
||||||
milliseconds: 400 + (index % 5) * 50,
|
|
||||||
), // Staggered delay
|
|
||||||
curve: Curves.easeOutCubic,
|
|
||||||
builder: (context, animationValue, child) {
|
|
||||||
return Transform.translate(
|
|
||||||
offset: Offset(
|
|
||||||
0,
|
|
||||||
20 * (1 - animationValue),
|
|
||||||
), // Slide up from bottom
|
|
||||||
child: Opacity(opacity: animationValue, child: child),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: messageWidget,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
@@ -1066,7 +738,11 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
AudioCallButton(roomId: id),
|
chatRoom.when(
|
||||||
|
data: (data) => AudioCallButton(room: data!),
|
||||||
|
error: (err, _) => const SizedBox.shrink(),
|
||||||
|
loading: () => const SizedBox.shrink(),
|
||||||
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.more_vert),
|
icon: const Icon(Icons.more_vert),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
@@ -1167,7 +843,14 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
top: 0,
|
top: 0,
|
||||||
child: CallOverlayBar().padding(horizontal: 8, top: 12),
|
child: chatRoom.when(
|
||||||
|
data:
|
||||||
|
(data) => CallOverlayBar(
|
||||||
|
room: data!,
|
||||||
|
).padding(horizontal: 8, top: 12),
|
||||||
|
error: (_, _) => const SizedBox.shrink(),
|
||||||
|
loading: () => const SizedBox.shrink(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (isSyncing)
|
if (isSyncing)
|
||||||
Positioned(
|
Positioned(
|
||||||
|
|||||||
@@ -487,6 +487,7 @@ class _ChatRoomActionMenu extends HookConsumerWidget {
|
|||||||
showConfirmAlert(
|
showConfirmAlert(
|
||||||
'deleteChatRoomHint'.tr(),
|
'deleteChatRoomHint'.tr(),
|
||||||
'deleteChatRoom'.tr(),
|
'deleteChatRoom'.tr(),
|
||||||
|
isDanger: true,
|
||||||
).then((confirm) async {
|
).then((confirm) async {
|
||||||
if (confirm) {
|
if (confirm) {
|
||||||
final client = ref.watch(apiClientProvider);
|
final client = ref.watch(apiClientProvider);
|
||||||
|
|||||||
169
lib/screens/chat/widgets/message_item_wrapper.dart
Normal file
169
lib/screens/chat/widgets/message_item_wrapper.dart
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
|
import 'package:island/database/message.dart';
|
||||||
|
import 'package:island/models/chat.dart';
|
||||||
|
import 'package:island/widgets/chat/message_item.dart';
|
||||||
|
|
||||||
|
// Provider to track animated messages to prevent replay
|
||||||
|
final animatedMessagesProvider = StateProvider<Set<String>>((ref) => {});
|
||||||
|
|
||||||
|
class MessageItemWrapper extends HookConsumerWidget {
|
||||||
|
final LocalChatMessage message;
|
||||||
|
final int index;
|
||||||
|
final bool isLastInGroup;
|
||||||
|
final bool isSelectionMode;
|
||||||
|
final Set<String> selectedMessages;
|
||||||
|
final AsyncValue<SnChatMember?> chatIdentity;
|
||||||
|
final VoidCallback toggleSelectionMode;
|
||||||
|
final Function(String) toggleMessageSelection;
|
||||||
|
final Function(String, LocalChatMessage) onMessageAction;
|
||||||
|
final Function(String) onJump;
|
||||||
|
final Map<String, Map<int, double?>> attachmentProgress;
|
||||||
|
final bool disableAnimation;
|
||||||
|
final DateTime roomOpenTime;
|
||||||
|
|
||||||
|
const MessageItemWrapper({
|
||||||
|
super.key,
|
||||||
|
required this.message,
|
||||||
|
required this.index,
|
||||||
|
required this.isLastInGroup,
|
||||||
|
required this.isSelectionMode,
|
||||||
|
required this.selectedMessages,
|
||||||
|
required this.chatIdentity,
|
||||||
|
required this.toggleSelectionMode,
|
||||||
|
required this.toggleMessageSelection,
|
||||||
|
required this.onMessageAction,
|
||||||
|
required this.onJump,
|
||||||
|
required this.attachmentProgress,
|
||||||
|
required this.disableAnimation,
|
||||||
|
required this.roomOpenTime,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
// Animation logic
|
||||||
|
final animatedMessages = ref.watch(animatedMessagesProvider);
|
||||||
|
final isNewMessage = message.createdAt.isAfter(roomOpenTime);
|
||||||
|
final hasAnimated = animatedMessages.contains(message.id);
|
||||||
|
|
||||||
|
// Only animate if:
|
||||||
|
// 1. Animation is enabled
|
||||||
|
// 2. Message is new (created after room open)
|
||||||
|
// 3. Has not animated yet
|
||||||
|
final shouldAnimate = !disableAnimation && isNewMessage && !hasAnimated;
|
||||||
|
|
||||||
|
final child = chatIdentity.when(
|
||||||
|
skipError: true,
|
||||||
|
data: (identity) => _buildContent(context, identity),
|
||||||
|
loading: () => _buildLoading(),
|
||||||
|
error: (_, _) => const SizedBox.shrink(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!shouldAnimate) {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TweenAnimationBuilder<double>(
|
||||||
|
key: ValueKey('anim-${message.id}'), // Ensure unique key for animation
|
||||||
|
tween: Tween<double>(begin: 0.0, end: 1.0),
|
||||||
|
duration: Duration(milliseconds: 400 + (index % 5) * 50),
|
||||||
|
curve: Curves.easeOutCubic,
|
||||||
|
builder: (context, value, child) {
|
||||||
|
return Transform.translate(
|
||||||
|
offset: Offset(0, 20 * (1 - value)),
|
||||||
|
child: Opacity(opacity: value, child: child),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onEnd: () {
|
||||||
|
// Mark as animated
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
ref
|
||||||
|
.read(animatedMessagesProvider.notifier)
|
||||||
|
.update((state) => {...state, message.id});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildContent(BuildContext context, SnChatMember? identity) {
|
||||||
|
final isSelected = selectedMessages.contains(message.id);
|
||||||
|
final isCurrentUser = identity?.id == message.senderId;
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onLongPress: () {
|
||||||
|
if (!isSelectionMode) {
|
||||||
|
toggleSelectionMode();
|
||||||
|
toggleMessageSelection(message.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onTap: () {
|
||||||
|
if (isSelectionMode) {
|
||||||
|
toggleMessageSelection(message.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
color:
|
||||||
|
isSelected
|
||||||
|
? Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.primaryContainer.withOpacity(0.3)
|
||||||
|
: null,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
MessageItem(
|
||||||
|
// If animation is disabled, we might want to pass a key to maintain state?
|
||||||
|
// But here we are inside the wrapper.
|
||||||
|
key: ValueKey('item-${message.id}'),
|
||||||
|
message: message,
|
||||||
|
isCurrentUser: isCurrentUser,
|
||||||
|
onAction:
|
||||||
|
isSelectionMode
|
||||||
|
? null
|
||||||
|
: (action) => onMessageAction(action, message),
|
||||||
|
onJump: onJump,
|
||||||
|
progress: attachmentProgress[message.id],
|
||||||
|
showAvatar: isLastInGroup,
|
||||||
|
isSelectionMode: isSelectionMode,
|
||||||
|
isSelected: isSelected,
|
||||||
|
onToggleSelection: toggleMessageSelection,
|
||||||
|
onEnterSelectionMode: () {
|
||||||
|
if (!isSelectionMode) toggleSelectionMode();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (isSelected)
|
||||||
|
Positioned(
|
||||||
|
top: 8,
|
||||||
|
right: 8,
|
||||||
|
child: Container(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.check,
|
||||||
|
size: 12,
|
||||||
|
color: Theme.of(context).colorScheme.onPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLoading() {
|
||||||
|
return MessageItem(
|
||||||
|
message: message,
|
||||||
|
isCurrentUser: false,
|
||||||
|
onAction: null,
|
||||||
|
progress: null,
|
||||||
|
showAvatar: false,
|
||||||
|
onJump: (_) {},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -304,16 +304,18 @@ class CreatorHubScreen extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void deletePublisher() {
|
void deletePublisher() {
|
||||||
showConfirmAlert('deletePublisherHint'.tr(), 'deletePublisher'.tr()).then(
|
showConfirmAlert(
|
||||||
(confirm) {
|
'deletePublisherHint'.tr(),
|
||||||
if (confirm) {
|
'deletePublisher'.tr(),
|
||||||
final client = ref.watch(apiClientProvider);
|
isDanger: true,
|
||||||
client.delete('/sphere/publishers/${currentPublisher.value!.name}');
|
).then((confirm) {
|
||||||
ref.invalidate(publishersManagedProvider);
|
if (confirm) {
|
||||||
currentPublisher.value = null;
|
final client = ref.watch(apiClientProvider);
|
||||||
}
|
client.delete('/sphere/publishers/${currentPublisher.value!.name}');
|
||||||
},
|
ref.invalidate(publishersManagedProvider);
|
||||||
);
|
currentPublisher.value = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<DropdownMenuItem<SnPublisher>> publishersMenu = publishers.when(
|
final List<DropdownMenuItem<SnPublisher>> publishersMenu = publishers.when(
|
||||||
@@ -403,6 +405,21 @@ class CreatorHubScreen extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
ListTile(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
),
|
||||||
|
minTileHeight: 48,
|
||||||
|
title: Text('publicationSites').tr(),
|
||||||
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
|
leading: const Icon(Symbols.web),
|
||||||
|
onTap: () {
|
||||||
|
context.pushNamed(
|
||||||
|
'creatorSites',
|
||||||
|
pathParameters: {'name': currentPublisher.value!.name},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
@@ -585,7 +602,7 @@ class CreatorHubScreen extends HookConsumerWidget {
|
|||||||
).padding(horizontal: 12),
|
).padding(horizontal: 12),
|
||||||
buildNavigationWidget(true),
|
buildNavigationWidget(true),
|
||||||
],
|
],
|
||||||
)
|
).padding(vertical: 24)
|
||||||
: Column(
|
: Column(
|
||||||
spacing: 12,
|
spacing: 12,
|
||||||
children: [
|
children: [
|
||||||
@@ -831,7 +848,7 @@ class _PublisherMemberListSheet extends HookConsumerWidget {
|
|||||||
try {
|
try {
|
||||||
final apiClient = ref.watch(apiClientProvider);
|
final apiClient = ref.watch(apiClientProvider);
|
||||||
await apiClient.post(
|
await apiClient.post(
|
||||||
'/sphere/publishers/$publisherUname/invites',
|
'/sphere/publishers/invites/$publisherUname',
|
||||||
data: {'related_user_id': result.id, 'role': 0},
|
data: {'related_user_id': result.id, 'role': 0},
|
||||||
);
|
);
|
||||||
// Refresh both providers
|
// Refresh both providers
|
||||||
@@ -1119,7 +1136,7 @@ class _PublisherInviteSheet extends HookConsumerWidget {
|
|||||||
try {
|
try {
|
||||||
final client = ref.read(apiClientProvider);
|
final client = ref.read(apiClientProvider);
|
||||||
await client.post(
|
await client.post(
|
||||||
'/publishers/invites/${invite.publisher!.name}/accept',
|
'/sphere/publishers/invites/${invite.publisher!.name}/accept',
|
||||||
);
|
);
|
||||||
ref.invalidate(publisherInvitesProvider);
|
ref.invalidate(publisherInvitesProvider);
|
||||||
ref.invalidate(publishersManagedProvider);
|
ref.invalidate(publishersManagedProvider);
|
||||||
@@ -1132,7 +1149,7 @@ class _PublisherInviteSheet extends HookConsumerWidget {
|
|||||||
try {
|
try {
|
||||||
final client = ref.read(apiClientProvider);
|
final client = ref.read(apiClientProvider);
|
||||||
await client.post(
|
await client.post(
|
||||||
'/publishers/invites/${invite.publisher!.name}/decline',
|
'/sphere/publishers/invites/${invite.publisher!.name}/decline',
|
||||||
);
|
);
|
||||||
ref.invalidate(publisherInvitesProvider);
|
ref.invalidate(publisherInvitesProvider);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:island/models/poll.dart';
|
import 'package:island/models/poll.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
import 'package:island/screens/poll/poll_editor.dart';
|
import 'package:island/screens/poll/poll_editor.dart';
|
||||||
|
import 'package:island/widgets/alert.dart';
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
import 'package:island/widgets/poll/poll_feedback.dart';
|
import 'package:island/widgets/poll/poll_feedback.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
@@ -234,19 +235,9 @@ class _CreatorPollItem extends HookConsumerWidget {
|
|||||||
'/sphere/polls/${pollWithStats.id}',
|
'/sphere/polls/${pollWithStats.id}',
|
||||||
);
|
);
|
||||||
ref.invalidate(pollListNotifierProvider(pubName));
|
ref.invalidate(pollListNotifierProvider(pubName));
|
||||||
if (context.mounted) {
|
showSnackBar('Poll deleted successfully');
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text('Poll deleted successfully'),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (context.mounted) {
|
showErrorAlert(e);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(content: Text('Failed to delete poll')),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
161
lib/screens/creators/sites/site_detail.dart
Normal file
161
lib/screens/creators/sites/site_detail.dart
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:island/models/publication_site.dart';
|
||||||
|
import 'package:island/pods/network.dart';
|
||||||
|
import 'package:island/pods/site_pages.dart';
|
||||||
|
import 'package:island/widgets/sites/page_form.dart';
|
||||||
|
import 'package:island/widgets/sites/site_action_menu.dart';
|
||||||
|
import 'package:island/widgets/sites/site_detail_content.dart';
|
||||||
|
import 'package:island/widgets/sites/site_info_card.dart';
|
||||||
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
import 'package:island/widgets/sites/pages_section.dart';
|
||||||
|
import 'package:island/widgets/sites/file_management_section.dart';
|
||||||
|
import 'package:island/widgets/sites/file_management_action_section.dart';
|
||||||
|
import 'package:island/services/responsive.dart';
|
||||||
|
import 'package:island/widgets/extended_refresh_indicator.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
|
||||||
|
part 'site_detail.g.dart';
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
Future<SnPublicationSite> publicationSiteDetail(
|
||||||
|
Ref ref,
|
||||||
|
String pubName,
|
||||||
|
String siteSlug,
|
||||||
|
) async {
|
||||||
|
final apiClient = ref.watch(apiClientProvider);
|
||||||
|
final resp = await apiClient.get('/zone/sites/$pubName/$siteSlug');
|
||||||
|
return SnPublicationSite.fromJson(resp.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
class PublicationSiteDetailScreen extends HookConsumerWidget {
|
||||||
|
final String siteSlug;
|
||||||
|
final String pubName;
|
||||||
|
|
||||||
|
const PublicationSiteDetailScreen({
|
||||||
|
super.key,
|
||||||
|
required this.siteSlug,
|
||||||
|
required this.pubName,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final siteAsync = ref.watch(
|
||||||
|
publicationSiteDetailProvider(pubName, siteSlug),
|
||||||
|
);
|
||||||
|
|
||||||
|
return AppScaffold(
|
||||||
|
isNoBackground: false,
|
||||||
|
appBar: AppBar(
|
||||||
|
title: siteAsync.maybeWhen(
|
||||||
|
data: (site) => Text(site.name),
|
||||||
|
orElse: () => Text('siteDetails'.tr()),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
siteAsync.maybeWhen(
|
||||||
|
data: (site) => SiteActionMenu(site: site, pubName: pubName),
|
||||||
|
orElse: () => const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: siteAsync.when(
|
||||||
|
data: (site) {
|
||||||
|
if (isWideScreen(context)) {
|
||||||
|
return ExtendedRefreshIndicator(
|
||||||
|
onRefresh:
|
||||||
|
() async => ref.invalidate(
|
||||||
|
publicationSiteDetailProvider(pubName, site.slug),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
spacing: 8,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
flex: 3,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
PagesSection(site: site, pubName: pubName),
|
||||||
|
if (site.mode == 1) // Self-Managed only
|
||||||
|
FileManagementSection(site: site, pubName: pubName),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
flex: 2,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SiteInfoCard(site: site),
|
||||||
|
const Gap(8),
|
||||||
|
if (site.mode == 1) // Self-Managed only
|
||||||
|
FileManagementActionSection(
|
||||||
|
site: site,
|
||||||
|
pubName: pubName,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 12),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return SiteDetailContent(site: site, pubName: pubName);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error:
|
||||||
|
(error, stack) => Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'failedToLoadSite'.tr(),
|
||||||
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
|
),
|
||||||
|
const Gap(16),
|
||||||
|
Text(error.toString()),
|
||||||
|
const Gap(24),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed:
|
||||||
|
() => ref.invalidate(
|
||||||
|
publicationSiteDetailProvider(pubName, siteSlug),
|
||||||
|
),
|
||||||
|
child: Text('retry'.tr()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
|
),
|
||||||
|
floatingActionButton: siteAsync.maybeWhen(
|
||||||
|
data:
|
||||||
|
(site) => FloatingActionButton(
|
||||||
|
onPressed: () {
|
||||||
|
// Create new page
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
builder: (context) => PageForm(site: site, pubName: pubName),
|
||||||
|
).then((_) {
|
||||||
|
// Refresh pages after creation
|
||||||
|
ref.invalidate(sitePagesProvider(pubName, site.slug));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: const Icon(Symbols.add),
|
||||||
|
),
|
||||||
|
orElse: () => null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
173
lib/screens/creators/sites/site_detail.g.dart
Normal file
173
lib/screens/creators/sites/site_detail.g.dart
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'site_detail.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
String _$publicationSiteDetailHash() =>
|
||||||
|
r'e5d259ea39c4ba47e92d37e644fc3d84984927a9';
|
||||||
|
|
||||||
|
/// Copied from Dart SDK
|
||||||
|
class _SystemHash {
|
||||||
|
_SystemHash._();
|
||||||
|
|
||||||
|
static int combine(int hash, int value) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + value);
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||||
|
return hash ^ (hash >> 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int finish(int hash) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = hash ^ (hash >> 11);
|
||||||
|
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [publicationSiteDetail].
|
||||||
|
@ProviderFor(publicationSiteDetail)
|
||||||
|
const publicationSiteDetailProvider = PublicationSiteDetailFamily();
|
||||||
|
|
||||||
|
/// See also [publicationSiteDetail].
|
||||||
|
class PublicationSiteDetailFamily
|
||||||
|
extends Family<AsyncValue<SnPublicationSite>> {
|
||||||
|
/// See also [publicationSiteDetail].
|
||||||
|
const PublicationSiteDetailFamily();
|
||||||
|
|
||||||
|
/// See also [publicationSiteDetail].
|
||||||
|
PublicationSiteDetailProvider call(String pubName, String siteSlug) {
|
||||||
|
return PublicationSiteDetailProvider(pubName, siteSlug);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
PublicationSiteDetailProvider getProviderOverride(
|
||||||
|
covariant PublicationSiteDetailProvider provider,
|
||||||
|
) {
|
||||||
|
return call(provider.pubName, provider.siteSlug);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||||
|
_allTransitiveDependencies;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get name => r'publicationSiteDetailProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [publicationSiteDetail].
|
||||||
|
class PublicationSiteDetailProvider
|
||||||
|
extends AutoDisposeFutureProvider<SnPublicationSite> {
|
||||||
|
/// See also [publicationSiteDetail].
|
||||||
|
PublicationSiteDetailProvider(String pubName, String siteSlug)
|
||||||
|
: this._internal(
|
||||||
|
(ref) => publicationSiteDetail(
|
||||||
|
ref as PublicationSiteDetailRef,
|
||||||
|
pubName,
|
||||||
|
siteSlug,
|
||||||
|
),
|
||||||
|
from: publicationSiteDetailProvider,
|
||||||
|
name: r'publicationSiteDetailProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$publicationSiteDetailHash,
|
||||||
|
dependencies: PublicationSiteDetailFamily._dependencies,
|
||||||
|
allTransitiveDependencies:
|
||||||
|
PublicationSiteDetailFamily._allTransitiveDependencies,
|
||||||
|
pubName: pubName,
|
||||||
|
siteSlug: siteSlug,
|
||||||
|
);
|
||||||
|
|
||||||
|
PublicationSiteDetailProvider._internal(
|
||||||
|
super._createNotifier, {
|
||||||
|
required super.name,
|
||||||
|
required super.dependencies,
|
||||||
|
required super.allTransitiveDependencies,
|
||||||
|
required super.debugGetCreateSourceHash,
|
||||||
|
required super.from,
|
||||||
|
required this.pubName,
|
||||||
|
required this.siteSlug,
|
||||||
|
}) : super.internal();
|
||||||
|
|
||||||
|
final String pubName;
|
||||||
|
final String siteSlug;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Override overrideWith(
|
||||||
|
FutureOr<SnPublicationSite> Function(PublicationSiteDetailRef provider)
|
||||||
|
create,
|
||||||
|
) {
|
||||||
|
return ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
override: PublicationSiteDetailProvider._internal(
|
||||||
|
(ref) => create(ref as PublicationSiteDetailRef),
|
||||||
|
from: from,
|
||||||
|
name: null,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
debugGetCreateSourceHash: null,
|
||||||
|
pubName: pubName,
|
||||||
|
siteSlug: siteSlug,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AutoDisposeFutureProviderElement<SnPublicationSite> createElement() {
|
||||||
|
return _PublicationSiteDetailProviderElement(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is PublicationSiteDetailProvider &&
|
||||||
|
other.pubName == pubName &&
|
||||||
|
other.siteSlug == siteSlug;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, pubName.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, siteSlug.hashCode);
|
||||||
|
|
||||||
|
return _SystemHash.finish(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
mixin PublicationSiteDetailRef
|
||||||
|
on AutoDisposeFutureProviderRef<SnPublicationSite> {
|
||||||
|
/// The parameter `pubName` of this provider.
|
||||||
|
String get pubName;
|
||||||
|
|
||||||
|
/// The parameter `siteSlug` of this provider.
|
||||||
|
String get siteSlug;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PublicationSiteDetailProviderElement
|
||||||
|
extends AutoDisposeFutureProviderElement<SnPublicationSite>
|
||||||
|
with PublicationSiteDetailRef {
|
||||||
|
_PublicationSiteDetailProviderElement(super.provider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pubName => (origin as PublicationSiteDetailProvider).pubName;
|
||||||
|
@override
|
||||||
|
String get siteSlug => (origin as PublicationSiteDetailProvider).siteSlug;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
385
lib/screens/creators/sites/site_edit.dart
Normal file
385
lib/screens/creators/sites/site_edit.dart
Normal file
@@ -0,0 +1,385 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:island/pods/network.dart';
|
||||||
|
import 'package:island/screens/creators/sites/site_detail.dart';
|
||||||
|
import 'package:island/screens/creators/sites/site_list.dart';
|
||||||
|
import 'package:island/widgets/alert.dart';
|
||||||
|
import 'package:island/widgets/content/sheet.dart';
|
||||||
|
import 'package:island/widgets/response.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
|
||||||
|
class SiteForm extends HookConsumerWidget {
|
||||||
|
final String pubName;
|
||||||
|
final String? siteSlug;
|
||||||
|
|
||||||
|
const SiteForm({super.key, required this.pubName, this.siteSlug});
|
||||||
|
|
||||||
|
Widget _buildForm(
|
||||||
|
GlobalKey<FormState> formKey,
|
||||||
|
TextEditingController slugController,
|
||||||
|
TextEditingController nameController,
|
||||||
|
TextEditingController descriptionController,
|
||||||
|
ValueNotifier<int> modeController,
|
||||||
|
Function() saveSite,
|
||||||
|
Function() deleteSite,
|
||||||
|
String siteSlug,
|
||||||
|
) {
|
||||||
|
final formFields = Column(
|
||||||
|
children: [
|
||||||
|
TextFormField(
|
||||||
|
controller: slugController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'siteSlug'.tr(),
|
||||||
|
hintText: 'siteSlugHint'.tr(),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'siteSlugRequired'.tr();
|
||||||
|
}
|
||||||
|
final slugRegex = RegExp(r'^[a-z0-9]+(?:-[a-z0-9]+)*$');
|
||||||
|
if (!slugRegex.hasMatch(value)) {
|
||||||
|
return 'siteSlugInvalid'.tr();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
controller: nameController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'siteName'.tr(),
|
||||||
|
hintText: 'siteNameHint'.tr(),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'siteNameRequired'.tr();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
controller: descriptionController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'description'.tr(),
|
||||||
|
alignLabelWithHint: true,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
maxLines: 3,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
DropdownButtonFormField<int>(
|
||||||
|
value: modeController.value,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'siteMode'.tr(),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
items: [
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: 0,
|
||||||
|
child: Text('siteModeFullyManaged'.tr()),
|
||||||
|
),
|
||||||
|
DropdownMenuItem(value: 1, child: Text('siteModeSelfManaged'.tr())),
|
||||||
|
],
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value != null) {
|
||||||
|
modeController.value = value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(all: 20);
|
||||||
|
|
||||||
|
return SheetScaffold(
|
||||||
|
titleText: 'editPublicationSite'.tr(),
|
||||||
|
child: Builder(
|
||||||
|
builder:
|
||||||
|
(context) => SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Form(key: formKey, child: formFields),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: deleteSite,
|
||||||
|
icon: const Icon(Symbols.delete_forever),
|
||||||
|
label: Text('deletePublicationSite'.tr()),
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
foregroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
).alignment(Alignment.centerRight),
|
||||||
|
const Spacer(),
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: saveSite,
|
||||||
|
icon: const Icon(Symbols.save),
|
||||||
|
label: Text('saveChanges').tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 20, vertical: 12),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final formKey = useMemoized(() => GlobalKey<FormState>());
|
||||||
|
final slugController = useTextEditingController();
|
||||||
|
final nameController = useTextEditingController();
|
||||||
|
final descriptionController = useTextEditingController();
|
||||||
|
final modeController = useState<int>(0); // Default to fully managed (0)
|
||||||
|
final isLoading = useState(false);
|
||||||
|
|
||||||
|
final saveSite = useCallback(() async {
|
||||||
|
if (!formKey.currentState!.validate()) return;
|
||||||
|
|
||||||
|
isLoading.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final client = ref.read(apiClientProvider);
|
||||||
|
final url = '/zone/sites/$pubName';
|
||||||
|
final payload = <String, dynamic>{
|
||||||
|
'slug': slugController.text,
|
||||||
|
'name': nameController.text,
|
||||||
|
'mode': modeController.value,
|
||||||
|
if (descriptionController.text.isNotEmpty)
|
||||||
|
'description': descriptionController.text,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (siteSlug != null) {
|
||||||
|
await client.patch('$url/$siteSlug', data: payload);
|
||||||
|
} else {
|
||||||
|
await client.post(url, data: payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh the site list
|
||||||
|
ref.invalidate(siteListNotifierProvider(pubName));
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
showSnackBar('publicationSiteSavedSuccess'.tr());
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showErrorAlert(e);
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}, [pubName, siteSlug, context]);
|
||||||
|
|
||||||
|
final deleteSite = useCallback(() async {
|
||||||
|
if (siteSlug == null) return; // Shouldn't happen for editing
|
||||||
|
|
||||||
|
final confirmed = await showConfirmAlert(
|
||||||
|
'publicationSiteDeleteConfirm'.tr(),
|
||||||
|
'deletePublicationSite'.tr(),
|
||||||
|
isDanger: true,
|
||||||
|
);
|
||||||
|
if (confirmed != true) return;
|
||||||
|
|
||||||
|
isLoading.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final client = ref.read(apiClientProvider);
|
||||||
|
await client.delete('/zone/sites/$pubName/$siteSlug');
|
||||||
|
|
||||||
|
ref.invalidate(siteListNotifierProvider(pubName));
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
showSnackBar('publicationSiteDeletedSuccess'.tr());
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showErrorAlert(e);
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}, [pubName, siteSlug, context]);
|
||||||
|
|
||||||
|
// Use Riverpod provider for loading and error states for editing
|
||||||
|
if (siteSlug != null) {
|
||||||
|
final editingSiteSlug =
|
||||||
|
siteSlug!; // Assert non-null since we checked above
|
||||||
|
final siteAsync = ref.watch(
|
||||||
|
publicationSiteDetailProvider(pubName, editingSiteSlug),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initialize form fields when site data is loaded
|
||||||
|
useEffect(() {
|
||||||
|
if (siteAsync.value != null && nameController.text.isEmpty) {
|
||||||
|
final site = siteAsync.value!;
|
||||||
|
slugController.text = site.slug;
|
||||||
|
nameController.text = site.name;
|
||||||
|
descriptionController.text = site.description ?? '';
|
||||||
|
modeController.value = site.mode ?? 0;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [siteAsync]);
|
||||||
|
|
||||||
|
// Handle loading and error states for editing using AsyncValue
|
||||||
|
return siteAsync.when(
|
||||||
|
data:
|
||||||
|
(_) => _buildForm(
|
||||||
|
formKey,
|
||||||
|
slugController,
|
||||||
|
nameController,
|
||||||
|
descriptionController,
|
||||||
|
modeController,
|
||||||
|
saveSite,
|
||||||
|
deleteSite,
|
||||||
|
editingSiteSlug,
|
||||||
|
),
|
||||||
|
loading:
|
||||||
|
() => SheetScaffold(
|
||||||
|
titleText: 'editPublicationSite'.tr(),
|
||||||
|
child: Center(child: CircularProgressIndicator()),
|
||||||
|
),
|
||||||
|
error:
|
||||||
|
(error, _) => SheetScaffold(
|
||||||
|
titleText: 'editPublicationSite'.tr(),
|
||||||
|
child: ResponseErrorWidget(
|
||||||
|
error: error.toString(),
|
||||||
|
onRetry: () {
|
||||||
|
ref.invalidate(
|
||||||
|
publicationSiteDetailProvider(pubName, editingSiteSlug),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For new sites, directly show the form
|
||||||
|
|
||||||
|
final formFields = Column(
|
||||||
|
children: [
|
||||||
|
TextFormField(
|
||||||
|
controller: slugController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Slug',
|
||||||
|
hintText: 'my-site',
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Please enter a slug';
|
||||||
|
}
|
||||||
|
final slugRegex = RegExp(r'^[a-z0-9]+(?:-[a-z0-9]+)*$');
|
||||||
|
if (!slugRegex.hasMatch(value)) {
|
||||||
|
return 'Slug can only contain lowercase letters, numbers, and dashes';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
controller: nameController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Site Name',
|
||||||
|
hintText: 'My Publication Site',
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Please enter a site name';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
controller: descriptionController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Description',
|
||||||
|
alignLabelWithHint: true,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
maxLines: 3,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
DropdownButtonFormField<int>(
|
||||||
|
value: modeController.value,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Mode',
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
items: [
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: 0,
|
||||||
|
child: Text('siteModeFullyManaged'.tr()),
|
||||||
|
),
|
||||||
|
DropdownMenuItem(value: 1, child: Text('siteModeSelfManaged'.tr())),
|
||||||
|
],
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value != null) {
|
||||||
|
modeController.value = value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(all: 20);
|
||||||
|
|
||||||
|
final saveButton = TextButton.icon(
|
||||||
|
onPressed: isLoading.value ? null : saveSite,
|
||||||
|
icon: const Icon(Symbols.save),
|
||||||
|
label: Text('saveChanges').tr(),
|
||||||
|
).padding(horizontal: 20, vertical: 12);
|
||||||
|
|
||||||
|
return SheetScaffold(
|
||||||
|
titleText:
|
||||||
|
siteSlug == null
|
||||||
|
? 'newPublicationSite'.tr()
|
||||||
|
: 'editPublicationSite'.tr(),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Form(key: formKey, child: formFields),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
if (siteSlug != null) ...[
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: isLoading.value ? null : deleteSite,
|
||||||
|
icon: const Icon(Symbols.delete_forever),
|
||||||
|
label: Text('deletePublicationSite'.tr()),
|
||||||
|
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||||
|
).alignment(Alignment.centerRight),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
const Spacer(),
|
||||||
|
saveButton,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
243
lib/screens/creators/sites/site_list.dart
Normal file
243
lib/screens/creators/sites/site_list.dart
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
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:google_fonts/google_fonts.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:island/models/publication_site.dart';
|
||||||
|
import 'package:island/pods/network.dart';
|
||||||
|
import 'package:island/screens/creators/sites/site_edit.dart';
|
||||||
|
import 'package:island/widgets/alert.dart';
|
||||||
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
|
||||||
|
import 'package:island/widgets/extended_refresh_indicator.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
|
||||||
|
part 'site_list.g.dart';
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
class SiteListNotifier extends _$SiteListNotifier
|
||||||
|
with CursorPagingNotifierMixin<SnPublicationSite> {
|
||||||
|
static const int _pageSize = 20;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<CursorPagingData<SnPublicationSite>> build(String? pubName) {
|
||||||
|
// immediately load first page
|
||||||
|
return fetch(cursor: null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<CursorPagingData<SnPublicationSite>> fetch({
|
||||||
|
required String? cursor,
|
||||||
|
}) async {
|
||||||
|
final client = ref.read(apiClientProvider);
|
||||||
|
final offset = cursor == null ? 0 : int.parse(cursor);
|
||||||
|
|
||||||
|
// read the current family argument passed to provider
|
||||||
|
final queryParams = {'offset': offset, 'take': _pageSize};
|
||||||
|
|
||||||
|
final response = await client.get(
|
||||||
|
'/zone/sites/$pubName',
|
||||||
|
queryParameters: queryParams,
|
||||||
|
);
|
||||||
|
final total = int.parse(response.headers.value('X-Total') ?? '0');
|
||||||
|
final List<dynamic> data = response.data;
|
||||||
|
final items = data.map((json) => SnPublicationSite.fromJson(json)).toList();
|
||||||
|
|
||||||
|
final hasMore = offset + items.length < total;
|
||||||
|
final nextCursor = hasMore ? (offset + items.length).toString() : null;
|
||||||
|
|
||||||
|
return CursorPagingData(
|
||||||
|
items: items,
|
||||||
|
hasMore: hasMore,
|
||||||
|
nextCursor: nextCursor,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CreatorSiteListScreen extends HookConsumerWidget {
|
||||||
|
const CreatorSiteListScreen({super.key, required this.pubName});
|
||||||
|
|
||||||
|
final String pubName;
|
||||||
|
|
||||||
|
Future<void> _createSite(BuildContext context) async {
|
||||||
|
await showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
builder: (context) => SiteForm(pubName: pubName),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return AppScaffold(
|
||||||
|
isNoBackground: false,
|
||||||
|
appBar: AppBar(title: Text('publicationSites'.tr())),
|
||||||
|
floatingActionButton: FloatingActionButton(
|
||||||
|
onPressed: () => _createSite(context),
|
||||||
|
child: Icon(Icons.add),
|
||||||
|
),
|
||||||
|
body: ExtendedRefreshIndicator(
|
||||||
|
onRefresh: () => ref.refresh(siteListNotifierProvider(pubName).future),
|
||||||
|
child: CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
const SliverGap(8),
|
||||||
|
PagingHelperSliverView(
|
||||||
|
provider: siteListNotifierProvider(pubName),
|
||||||
|
futureRefreshable: siteListNotifierProvider(pubName).future,
|
||||||
|
notifierRefreshable: siteListNotifierProvider(pubName).notifier,
|
||||||
|
contentBuilder:
|
||||||
|
(data, widgetCount, endItemView) => SliverList.builder(
|
||||||
|
itemCount: widgetCount,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
if (index == widgetCount - 1) {
|
||||||
|
return endItemView;
|
||||||
|
}
|
||||||
|
final site = data.items[index];
|
||||||
|
return ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(maxWidth: 640),
|
||||||
|
child: _CreatorSiteItem(site: site, pubName: pubName),
|
||||||
|
).center();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CreatorSiteItem extends HookConsumerWidget {
|
||||||
|
final String pubName;
|
||||||
|
const _CreatorSiteItem({required this.site, required this.pubName});
|
||||||
|
|
||||||
|
final SnPublicationSite site;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () {
|
||||||
|
// Navigate to site detail screen
|
||||||
|
context.pushNamed(
|
||||||
|
'creatorSiteDetail',
|
||||||
|
pathParameters: {'name': pubName, 'siteSlug': site.slug},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
spacing: 2,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Symbols.globe,
|
||||||
|
size: 18,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
const Gap(6),
|
||||||
|
Text(site.name).bold(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (site.description != null &&
|
||||||
|
site.description!.isNotEmpty)
|
||||||
|
Text(
|
||||||
|
site.description!,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
const Divider(height: 8),
|
||||||
|
Text(
|
||||||
|
'${site.slug}.solian.page',
|
||||||
|
style: GoogleFonts.robotoMono(fontSize: 11),
|
||||||
|
).opacity(0.8),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
PopupMenuButton<String>(
|
||||||
|
itemBuilder:
|
||||||
|
(context) => [
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.edit),
|
||||||
|
const Gap(16),
|
||||||
|
Text('edit').tr(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
builder:
|
||||||
|
(context) => SiteForm(
|
||||||
|
pubName: pubName,
|
||||||
|
siteSlug: site.slug,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.delete, color: Colors.red),
|
||||||
|
const Gap(16),
|
||||||
|
Text('delete').tr().textColor(Colors.red),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () async {
|
||||||
|
final confirmed = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder:
|
||||||
|
(context) => AlertDialog(
|
||||||
|
title: Text('deleteSite'.tr()),
|
||||||
|
content: Text('deleteSiteConfirm'.tr()),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed:
|
||||||
|
() =>
|
||||||
|
Navigator.of(context).pop(false),
|
||||||
|
child: Text('cancel'.tr()),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed:
|
||||||
|
() => Navigator.of(context).pop(true),
|
||||||
|
child: Text('delete'.tr()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (confirmed == true) {
|
||||||
|
try {
|
||||||
|
final client = ref.read(apiClientProvider);
|
||||||
|
await client.delete(
|
||||||
|
'/zone/sites/$pubName/${site.slug}',
|
||||||
|
);
|
||||||
|
ref.invalidate(siteListNotifierProvider(pubName));
|
||||||
|
showSnackBar('siteDeletedSuccess'.tr());
|
||||||
|
} catch (e) {
|
||||||
|
showErrorAlert(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
183
lib/screens/creators/sites/site_list.g.dart
Normal file
183
lib/screens/creators/sites/site_list.g.dart
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'site_list.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
String _$siteListNotifierHash() => r'1670cadcc0c7ccbd98bc33bbf5b4af21e9cb166c';
|
||||||
|
|
||||||
|
/// Copied from Dart SDK
|
||||||
|
class _SystemHash {
|
||||||
|
_SystemHash._();
|
||||||
|
|
||||||
|
static int combine(int hash, int value) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + value);
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||||
|
return hash ^ (hash >> 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int finish(int hash) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = hash ^ (hash >> 11);
|
||||||
|
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _$SiteListNotifier
|
||||||
|
extends
|
||||||
|
BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnPublicationSite>> {
|
||||||
|
late final String? pubName;
|
||||||
|
|
||||||
|
FutureOr<CursorPagingData<SnPublicationSite>> build(String? pubName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [SiteListNotifier].
|
||||||
|
@ProviderFor(SiteListNotifier)
|
||||||
|
const siteListNotifierProvider = SiteListNotifierFamily();
|
||||||
|
|
||||||
|
/// See also [SiteListNotifier].
|
||||||
|
class SiteListNotifierFamily
|
||||||
|
extends Family<AsyncValue<CursorPagingData<SnPublicationSite>>> {
|
||||||
|
/// See also [SiteListNotifier].
|
||||||
|
const SiteListNotifierFamily();
|
||||||
|
|
||||||
|
/// See also [SiteListNotifier].
|
||||||
|
SiteListNotifierProvider call(String? pubName) {
|
||||||
|
return SiteListNotifierProvider(pubName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
SiteListNotifierProvider getProviderOverride(
|
||||||
|
covariant SiteListNotifierProvider provider,
|
||||||
|
) {
|
||||||
|
return call(provider.pubName);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||||
|
_allTransitiveDependencies;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get name => r'siteListNotifierProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [SiteListNotifier].
|
||||||
|
class SiteListNotifierProvider
|
||||||
|
extends
|
||||||
|
AutoDisposeAsyncNotifierProviderImpl<
|
||||||
|
SiteListNotifier,
|
||||||
|
CursorPagingData<SnPublicationSite>
|
||||||
|
> {
|
||||||
|
/// See also [SiteListNotifier].
|
||||||
|
SiteListNotifierProvider(String? pubName)
|
||||||
|
: this._internal(
|
||||||
|
() => SiteListNotifier()..pubName = pubName,
|
||||||
|
from: siteListNotifierProvider,
|
||||||
|
name: r'siteListNotifierProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$siteListNotifierHash,
|
||||||
|
dependencies: SiteListNotifierFamily._dependencies,
|
||||||
|
allTransitiveDependencies:
|
||||||
|
SiteListNotifierFamily._allTransitiveDependencies,
|
||||||
|
pubName: pubName,
|
||||||
|
);
|
||||||
|
|
||||||
|
SiteListNotifierProvider._internal(
|
||||||
|
super._createNotifier, {
|
||||||
|
required super.name,
|
||||||
|
required super.dependencies,
|
||||||
|
required super.allTransitiveDependencies,
|
||||||
|
required super.debugGetCreateSourceHash,
|
||||||
|
required super.from,
|
||||||
|
required this.pubName,
|
||||||
|
}) : super.internal();
|
||||||
|
|
||||||
|
final String? pubName;
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOr<CursorPagingData<SnPublicationSite>> runNotifierBuild(
|
||||||
|
covariant SiteListNotifier notifier,
|
||||||
|
) {
|
||||||
|
return notifier.build(pubName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Override overrideWith(SiteListNotifier Function() create) {
|
||||||
|
return ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
override: SiteListNotifierProvider._internal(
|
||||||
|
() => create()..pubName = pubName,
|
||||||
|
from: from,
|
||||||
|
name: null,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
debugGetCreateSourceHash: null,
|
||||||
|
pubName: pubName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AutoDisposeAsyncNotifierProviderElement<
|
||||||
|
SiteListNotifier,
|
||||||
|
CursorPagingData<SnPublicationSite>
|
||||||
|
>
|
||||||
|
createElement() {
|
||||||
|
return _SiteListNotifierProviderElement(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is SiteListNotifierProvider && other.pubName == pubName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, pubName.hashCode);
|
||||||
|
|
||||||
|
return _SystemHash.finish(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
mixin SiteListNotifierRef
|
||||||
|
on
|
||||||
|
AutoDisposeAsyncNotifierProviderRef<
|
||||||
|
CursorPagingData<SnPublicationSite>
|
||||||
|
> {
|
||||||
|
/// The parameter `pubName` of this provider.
|
||||||
|
String? get pubName;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SiteListNotifierProviderElement
|
||||||
|
extends
|
||||||
|
AutoDisposeAsyncNotifierProviderElement<
|
||||||
|
SiteListNotifier,
|
||||||
|
CursorPagingData<SnPublicationSite>
|
||||||
|
>
|
||||||
|
with SiteListNotifierRef {
|
||||||
|
_SiteListNotifierProviderElement(super.provider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get pubName => (origin as SiteListNotifierProvider).pubName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
@@ -288,6 +288,7 @@ class StickerPackActionMenu extends HookConsumerWidget {
|
|||||||
showConfirmAlert(
|
showConfirmAlert(
|
||||||
'deleteStickerPackHint'.tr(),
|
'deleteStickerPackHint'.tr(),
|
||||||
'deleteStickerPack'.tr(),
|
'deleteStickerPack'.tr(),
|
||||||
|
isDanger: true,
|
||||||
).then((confirm) {
|
).then((confirm) {
|
||||||
if (confirm) {
|
if (confirm) {
|
||||||
final client = ref.watch(apiClientProvider);
|
final client = ref.watch(apiClientProvider);
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ class WebfeedForm extends HookConsumerWidget {
|
|||||||
final confirmed = await showConfirmAlert(
|
final confirmed = await showConfirmAlert(
|
||||||
'Are you sure you want to delete this web feed? This action cannot be undone.',
|
'Are you sure you want to delete this web feed? This action cannot be undone.',
|
||||||
'Delete Web Feed',
|
'Delete Web Feed',
|
||||||
|
isDanger: true,
|
||||||
);
|
);
|
||||||
if (confirmed != true) return;
|
if (confirmed != true) return;
|
||||||
|
|
||||||
|
|||||||
@@ -211,6 +211,7 @@ class AppSecretsScreen extends HookConsumerWidget {
|
|||||||
showConfirmAlert(
|
showConfirmAlert(
|
||||||
'deleteSecretHint'.tr(),
|
'deleteSecretHint'.tr(),
|
||||||
'deleteSecret'.tr(),
|
'deleteSecret'.tr(),
|
||||||
|
isDanger: true,
|
||||||
).then((confirm) {
|
).then((confirm) {
|
||||||
if (confirm) {
|
if (confirm) {
|
||||||
final client = ref.read(apiClientProvider);
|
final client = ref.read(apiClientProvider);
|
||||||
|
|||||||
@@ -231,6 +231,7 @@ class CustomAppsScreen extends HookConsumerWidget {
|
|||||||
showConfirmAlert(
|
showConfirmAlert(
|
||||||
'deleteCustomAppHint'.tr(),
|
'deleteCustomAppHint'.tr(),
|
||||||
'deleteCustomApp'.tr(),
|
'deleteCustomApp'.tr(),
|
||||||
|
isDanger: true,
|
||||||
).then((confirm) {
|
).then((confirm) {
|
||||||
if (confirm) {
|
if (confirm) {
|
||||||
final client = ref.read(
|
final client = ref.read(
|
||||||
|
|||||||
@@ -159,9 +159,11 @@ class BotKeysScreen extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void revokeKey(String keyId) {
|
void revokeKey(String keyId) {
|
||||||
showConfirmAlert('revokeBotKeyHint'.tr(), 'revokeBotKey'.tr()).then((
|
showConfirmAlert(
|
||||||
confirm,
|
'revokeBotKeyHint'.tr(),
|
||||||
) {
|
'revokeBotKey'.tr(),
|
||||||
|
isDanger: true,
|
||||||
|
).then((confirm) {
|
||||||
if (confirm) {
|
if (confirm) {
|
||||||
final client = ref.read(apiClientProvider);
|
final client = ref.read(apiClientProvider);
|
||||||
client
|
client
|
||||||
|
|||||||
@@ -172,6 +172,7 @@ class BotsScreen extends HookConsumerWidget {
|
|||||||
showConfirmAlert(
|
showConfirmAlert(
|
||||||
'deleteBotHint'.tr(),
|
'deleteBotHint'.tr(),
|
||||||
'deleteBot'.tr(),
|
'deleteBot'.tr(),
|
||||||
|
isDanger: true,
|
||||||
).then((confirm) {
|
).then((confirm) {
|
||||||
if (confirm) {
|
if (confirm) {
|
||||||
final client = ref.read(apiClientProvider);
|
final client = ref.read(apiClientProvider);
|
||||||
|
|||||||
@@ -631,6 +631,7 @@ class _ProjectListTile extends HookConsumerWidget {
|
|||||||
showConfirmAlert(
|
showConfirmAlert(
|
||||||
'deleteProjectHint'.tr(),
|
'deleteProjectHint'.tr(),
|
||||||
'deleteProject'.tr(),
|
'deleteProject'.tr(),
|
||||||
|
isDanger: true,
|
||||||
).then((confirm) {
|
).then((confirm) {
|
||||||
if (confirm) {
|
if (confirm) {
|
||||||
final client = ref.read(apiClientProvider);
|
final client = ref.read(apiClientProvider);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:desktop_drop/desktop_drop.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -30,7 +31,9 @@ import 'package:island/widgets/publisher/publisher_card.dart';
|
|||||||
import 'package:island/widgets/web_article_card.dart';
|
import 'package:island/widgets/web_article_card.dart';
|
||||||
import 'package:island/widgets/extended_refresh_indicator.dart';
|
import 'package:island/widgets/extended_refresh_indicator.dart';
|
||||||
import 'package:island/services/event_bus.dart';
|
import 'package:island/services/event_bus.dart';
|
||||||
|
import 'package:island/widgets/share/share_sheet.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:super_sliver_list/super_sliver_list.dart';
|
||||||
|
|
||||||
part 'explore.g.dart';
|
part 'explore.g.dart';
|
||||||
|
|
||||||
@@ -239,23 +242,74 @@ class ExploreScreen extends HookConsumerWidget {
|
|||||||
|
|
||||||
final appBar = isWide ? null : _buildAppBar(tabController, context);
|
final appBar = isWide ? null : _buildAppBar(tabController, context);
|
||||||
|
|
||||||
return AppScaffold(
|
final dragging = useState(false);
|
||||||
isNoBackground: false,
|
|
||||||
appBar: appBar,
|
return DropTarget(
|
||||||
body:
|
onDragDone: (detail) {
|
||||||
isWide
|
dragging.value = false;
|
||||||
? _buildWideBody(
|
if (detail.files.isNotEmpty) {
|
||||||
context,
|
showModalBottomSheet(
|
||||||
ref,
|
context: context,
|
||||||
filterBar,
|
isScrollControlled: true,
|
||||||
user,
|
useRootNavigator: true,
|
||||||
notificationCount,
|
builder: (context) => ShareSheet.files(files: detail.files),
|
||||||
query,
|
);
|
||||||
events,
|
}
|
||||||
selectedDay,
|
},
|
||||||
currentFilter.value,
|
onDragEntered: (_) => dragging.value = true,
|
||||||
)
|
onDragExited: (_) => dragging.value = false,
|
||||||
: _buildNarrowBody(context, ref, currentFilter.value),
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
AppScaffold(
|
||||||
|
isNoBackground: false,
|
||||||
|
appBar: appBar,
|
||||||
|
body:
|
||||||
|
isWide
|
||||||
|
? _buildWideBody(
|
||||||
|
context,
|
||||||
|
ref,
|
||||||
|
filterBar,
|
||||||
|
user,
|
||||||
|
notificationCount,
|
||||||
|
query,
|
||||||
|
events,
|
||||||
|
selectedDay,
|
||||||
|
currentFilter.value,
|
||||||
|
)
|
||||||
|
: _buildNarrowBody(context, ref, currentFilter.value),
|
||||||
|
),
|
||||||
|
if (dragging.value)
|
||||||
|
Positioned.fill(
|
||||||
|
child: Container(
|
||||||
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.primaryContainer.withOpacity(0.9),
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Symbols.upload_file,
|
||||||
|
size: 64,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
const Gap(16),
|
||||||
|
Text(
|
||||||
|
'dropToShare'.tr(),
|
||||||
|
style: Theme.of(
|
||||||
|
context,
|
||||||
|
).textTheme.headlineMedium?.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -545,6 +599,7 @@ class ExploreScreen extends HookConsumerWidget {
|
|||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: FriendsOverviewWidget(
|
child: FriendsOverviewWidget(
|
||||||
padding: const EdgeInsets.only(bottom: 8),
|
padding: const EdgeInsets.only(bottom: 8),
|
||||||
|
hideWhenEmpty: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (notificationCount.value != null &&
|
if (notificationCount.value != null &&
|
||||||
@@ -581,7 +636,7 @@ class _DiscoveryActivityItem extends StatelessWidget {
|
|||||||
final height = type == 'post' ? 280.0 : 180.0;
|
final height = type == 'post' ? 280.0 : 180.0;
|
||||||
|
|
||||||
final contentWidget = switch (type) {
|
final contentWidget = switch (type) {
|
||||||
'post' => ListView.separated(
|
'post' => SuperListView.separated(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
itemCount: items.length,
|
itemCount: items.length,
|
||||||
separatorBuilder: (context, index) => const Gap(12),
|
separatorBuilder: (context, index) => const Gap(12),
|
||||||
|
|||||||
@@ -6,17 +6,24 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:gal/gal.dart';
|
import 'package:gal/gal.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/file.dart';
|
import 'package:island/models/file.dart';
|
||||||
import 'package:island/pods/config.dart';
|
import 'package:island/pods/config.dart';
|
||||||
|
import 'package:island/pods/file_references.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
|
import 'package:island/pods/upload_tasks.dart';
|
||||||
|
import 'package:island/models/drive_task.dart';
|
||||||
import 'package:island/services/responsive.dart';
|
import 'package:island/services/responsive.dart';
|
||||||
|
import 'package:island/services/time.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
import 'package:island/widgets/content/file_info_sheet.dart';
|
import 'package:island/widgets/content/file_info_sheet.dart';
|
||||||
import 'package:island/widgets/content/file_viewer_contents.dart';
|
import 'package:island/widgets/content/file_viewer_contents.dart';
|
||||||
|
import 'package:island/widgets/content/sheet.dart';
|
||||||
import 'package:path/path.dart' show extension;
|
import 'package:path/path.dart' show extension;
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
|
||||||
class FileDetailScreen extends HookConsumerWidget {
|
class FileDetailScreen extends HookConsumerWidget {
|
||||||
final SnCloudFile item;
|
final SnCloudFile item;
|
||||||
@@ -76,7 +83,7 @@ class FileDetailScreen extends HookConsumerWidget {
|
|||||||
}, [animationController]);
|
}, [animationController]);
|
||||||
|
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
isNoBackground: true,
|
isNoBackground: false,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
@@ -86,26 +93,47 @@ class FileDetailScreen extends HookConsumerWidget {
|
|||||||
title: Text(item.name.isEmpty ? 'File Details' : item.name),
|
title: Text(item.name.isEmpty ? 'File Details' : item.name),
|
||||||
actions: _buildAppBarActions(context, ref, showInfoSheet),
|
actions: _buildAppBarActions(context, ref, showInfoSheet),
|
||||||
),
|
),
|
||||||
body: AnimatedBuilder(
|
body: LayoutBuilder(
|
||||||
animation: animation,
|
builder: (context, constraints) {
|
||||||
builder: (context, child) {
|
return AnimatedBuilder(
|
||||||
return Row(
|
animation: animation,
|
||||||
children: [
|
builder: (context, child) {
|
||||||
// Main content area
|
return Stack(
|
||||||
Expanded(child: _buildContent(context, ref, serverUrl)),
|
children: [
|
||||||
// Animated drawer panel
|
// Main content area - resizes with animation
|
||||||
if (isWide)
|
Positioned(
|
||||||
SizedBox(
|
left: 0,
|
||||||
height: double.infinity,
|
top: 0,
|
||||||
width: animation.value * 400, // Max width of 400px
|
bottom: 0,
|
||||||
child: Container(
|
width: constraints.maxWidth - animation.value * 400,
|
||||||
child:
|
child: _buildContent(context, ref, serverUrl),
|
||||||
animation.value > 0.1
|
|
||||||
? FileInfoSheet(item: item, onClose: showInfoSheet)
|
|
||||||
: const SizedBox.shrink(),
|
|
||||||
),
|
),
|
||||||
),
|
// Animated drawer panel - overlays
|
||||||
],
|
if (isWide)
|
||||||
|
Positioned(
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
width: 400,
|
||||||
|
child: Transform.translate(
|
||||||
|
offset: Offset((1 - animation.value) * 400, 0),
|
||||||
|
child: SizedBox(
|
||||||
|
width: 400,
|
||||||
|
child: Material(
|
||||||
|
color:
|
||||||
|
Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
elevation: 8,
|
||||||
|
child: FileInfoSheet(
|
||||||
|
item: item,
|
||||||
|
onClose: showInfoSheet,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -144,6 +172,24 @@ class FileDetailScreen extends HookConsumerWidget {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add references button
|
||||||
|
actions.add(
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.link),
|
||||||
|
onPressed:
|
||||||
|
() => showModalBottomSheet(
|
||||||
|
useRootNavigator: true,
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
builder:
|
||||||
|
(context) => SheetScaffold(
|
||||||
|
titleText: 'File References',
|
||||||
|
child: ReferencesList(fileId: item.id),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
// Always add info button
|
// Always add info button
|
||||||
actions.add(
|
actions.add(
|
||||||
IconButton(icon: Icon(Icons.info_outline), onPressed: showInfoSheet),
|
IconButton(icon: Icon(Icons.info_outline), onPressed: showInfoSheet),
|
||||||
@@ -187,6 +233,8 @@ class FileDetailScreen extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _downloadFile(WidgetRef ref) async {
|
Future<void> _downloadFile(WidgetRef ref) async {
|
||||||
|
final taskNotifier = ref.read(uploadTasksProvider.notifier);
|
||||||
|
final taskId = taskNotifier.addLocalDownloadTask(item);
|
||||||
try {
|
try {
|
||||||
showSnackBar('Downloading file...');
|
showSnackBar('Downloading file...');
|
||||||
|
|
||||||
@@ -202,14 +250,26 @@ class FileDetailScreen extends HookConsumerWidget {
|
|||||||
'/drive/files/${item.id}',
|
'/drive/files/${item.id}',
|
||||||
filePath,
|
filePath,
|
||||||
queryParameters: {'original': true},
|
queryParameters: {'original': true},
|
||||||
|
onReceiveProgress: (count, total) {
|
||||||
|
if (total > 0) {
|
||||||
|
taskNotifier.updateDownloadProgress(taskId, count, total);
|
||||||
|
taskNotifier.updateTransmissionProgress(taskId, count / total);
|
||||||
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
await FileSaver.instance.saveFile(
|
await FileSaver.instance.saveFile(
|
||||||
name: item.name.isEmpty ? '${item.id}.$extName' : item.name,
|
name: item.name.isEmpty ? '${item.id}.$extName' : item.name,
|
||||||
file: File(filePath),
|
file: File(filePath),
|
||||||
);
|
);
|
||||||
|
taskNotifier.updateTaskStatus(taskId, DriveTaskStatus.completed);
|
||||||
showSnackBar('File saved to downloads');
|
showSnackBar('File saved to downloads');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
taskNotifier.updateTaskStatus(
|
||||||
|
taskId,
|
||||||
|
DriveTaskStatus.failed,
|
||||||
|
errorMessage: e.toString(),
|
||||||
|
);
|
||||||
showErrorAlert(e);
|
showErrorAlert(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -229,3 +289,54 @@ class FileDetailScreen extends HookConsumerWidget {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ReferencesList extends ConsumerWidget {
|
||||||
|
const ReferencesList({super.key, required this.fileId});
|
||||||
|
|
||||||
|
final String fileId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final asyncReferences = ref.watch(fileReferencesProvider(fileId));
|
||||||
|
|
||||||
|
return asyncReferences.when(
|
||||||
|
data:
|
||||||
|
(references) => ListView.builder(
|
||||||
|
itemCount: references.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final reference = references[index];
|
||||||
|
return ListTile(
|
||||||
|
leading: const Icon(Icons.link),
|
||||||
|
title: Row(
|
||||||
|
spacing: 6,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
reference.usage,
|
||||||
|
style: GoogleFonts.robotoMono(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 13,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
reference.id,
|
||||||
|
style: GoogleFonts.robotoMono(fontSize: 13),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
subtitle: Row(
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
Text(reference.createdAt.formatRelative(context)),
|
||||||
|
const VerticalDivider(width: 1, thickness: 1).height(12),
|
||||||
|
Text(reference.createdAt.formatSystem()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
|
error:
|
||||||
|
(error, _) => Center(child: Text('Error loading references: $error')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import 'package:cross_file/cross_file.dart';
|
import 'package:cross_file/cross_file.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/file.dart';
|
import 'package:island/models/file.dart';
|
||||||
|
import 'package:island/models/file_pool.dart';
|
||||||
import 'package:island/pods/file_list.dart';
|
import 'package:island/pods/file_list.dart';
|
||||||
import 'package:island/services/file_uploader.dart';
|
import 'package:island/services/file_uploader.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
@@ -23,6 +25,7 @@ class FileListScreen extends HookConsumerWidget {
|
|||||||
// Path navigation state
|
// Path navigation state
|
||||||
final currentPath = useState<String>('/');
|
final currentPath = useState<String>('/');
|
||||||
final mode = useState<FileListMode>(FileListMode.normal);
|
final mode = useState<FileListMode>(FileListMode.normal);
|
||||||
|
final selectedPool = useState<SnFilePool?>(null);
|
||||||
|
|
||||||
final usageAsync = ref.watch(billingUsageProvider);
|
final usageAsync = ref.watch(billingUsageProvider);
|
||||||
final quotaAsync = ref.watch(billingQuotaProvider);
|
final quotaAsync = ref.watch(billingQuotaProvider);
|
||||||
@@ -32,8 +35,8 @@ class FileListScreen extends HookConsumerWidget {
|
|||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
isNoBackground: false,
|
isNoBackground: false,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text('Files'),
|
title: Text('files').tr(),
|
||||||
leading: const PageBackButton(),
|
leading: const PageBackButton(backTo: '/account'),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Symbols.bar_chart),
|
icon: const Icon(Symbols.bar_chart),
|
||||||
@@ -55,8 +58,13 @@ class FileListScreen extends HookConsumerWidget {
|
|||||||
usage: usage,
|
usage: usage,
|
||||||
quota: quota,
|
quota: quota,
|
||||||
currentPath: currentPath,
|
currentPath: currentPath,
|
||||||
|
selectedPool: selectedPool,
|
||||||
onPickAndUpload:
|
onPickAndUpload:
|
||||||
() => _pickAndUploadFile(ref, currentPath.value),
|
() => _pickAndUploadFile(
|
||||||
|
ref,
|
||||||
|
currentPath.value,
|
||||||
|
selectedPool.value?.id,
|
||||||
|
),
|
||||||
onShowCreateDirectory: _showCreateDirectoryDialog,
|
onShowCreateDirectory: _showCreateDirectoryDialog,
|
||||||
mode: mode,
|
mode: mode,
|
||||||
viewMode: viewMode,
|
viewMode: viewMode,
|
||||||
@@ -70,7 +78,11 @@ class FileListScreen extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _pickAndUploadFile(WidgetRef ref, String currentPath) async {
|
Future<void> _pickAndUploadFile(
|
||||||
|
WidgetRef ref,
|
||||||
|
String currentPath,
|
||||||
|
String? poolId,
|
||||||
|
) async {
|
||||||
try {
|
try {
|
||||||
final result = await FilePicker.platform.pickFiles(
|
final result = await FilePicker.platform.pickFiles(
|
||||||
allowMultiple: true,
|
allowMultiple: true,
|
||||||
@@ -92,6 +104,7 @@ class FileListScreen extends HookConsumerWidget {
|
|||||||
fileData: universalFile,
|
fileData: universalFile,
|
||||||
ref: ref,
|
ref: ref,
|
||||||
path: currentPath,
|
path: currentPath,
|
||||||
|
poolId: poolId,
|
||||||
onProgress: (progress, _) {
|
onProgress: (progress, _) {
|
||||||
// Progress is handled by the upload tasks system
|
// Progress is handled by the upload tasks system
|
||||||
if (progress != null) {
|
if (progress != null) {
|
||||||
|
|||||||
@@ -534,7 +534,7 @@ class _LotteryPurchaseSheetState extends State<LotteryPurchaseSheet> {
|
|||||||
),
|
),
|
||||||
const Gap(4),
|
const Gap(4),
|
||||||
Text(
|
Text(
|
||||||
'The last selected number will be your special number.',
|
'lotteryLastNumberSpecial'.tr(),
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
color: Theme.of(context).colorScheme.secondary,
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
@@ -738,11 +738,11 @@ class _LotteryPurchaseSheetState extends State<LotteryPurchaseSheet> {
|
|||||||
},
|
},
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value == null || value.isEmpty) {
|
if (value == null || value.isEmpty) {
|
||||||
return 'Please enter a multiplier';
|
return 'lotteryMultiplierRequired'.tr();
|
||||||
}
|
}
|
||||||
final parsed = int.tryParse(value);
|
final parsed = int.tryParse(value);
|
||||||
if (parsed == null || parsed < 1 || parsed > 10) {
|
if (parsed == null || parsed < 1 || parsed > 10) {
|
||||||
return 'Multiplier must be between 1 and 10';
|
return 'lotteryMultiplierRange'.tr();
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import 'package:island/pods/network.dart';
|
|||||||
import 'package:island/pods/websocket.dart';
|
import 'package:island/pods/websocket.dart';
|
||||||
import 'package:island/route.dart';
|
import 'package:island/route.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
|
import 'package:island/widgets/content/cloud_files.dart';
|
||||||
import 'package:island/widgets/content/markdown.dart';
|
import 'package:island/widgets/content/markdown.dart';
|
||||||
import 'package:island/widgets/content/sheet.dart';
|
import 'package:island/widgets/content/sheet.dart';
|
||||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||||
@@ -68,6 +69,16 @@ class NotificationUnreadCountNotifier
|
|||||||
void clear() async {
|
void clear() async {
|
||||||
state = AsyncData(0);
|
state = AsyncData(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> refresh() async {
|
||||||
|
try {
|
||||||
|
final client = ref.read(apiClientProvider);
|
||||||
|
final response = await client.get('/ring/notifications/count');
|
||||||
|
state = AsyncData((response.data as num).toInt());
|
||||||
|
} catch (_) {
|
||||||
|
// Keep the current state if refresh fails
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
@@ -115,8 +126,36 @@ class NotificationListNotifier extends _$NotificationListNotifier
|
|||||||
class NotificationSheet extends HookConsumerWidget {
|
class NotificationSheet extends HookConsumerWidget {
|
||||||
const NotificationSheet({super.key});
|
const NotificationSheet({super.key});
|
||||||
|
|
||||||
|
IconData _getNotificationIcon(String topic) {
|
||||||
|
switch (topic) {
|
||||||
|
case 'post.replies':
|
||||||
|
return Symbols.reply;
|
||||||
|
case 'wallet.transactions':
|
||||||
|
return Symbols.account_balance_wallet;
|
||||||
|
case 'relationships.friends.request':
|
||||||
|
return Symbols.person_add;
|
||||||
|
case 'invites.chat':
|
||||||
|
return Symbols.chat;
|
||||||
|
case 'invites.realm':
|
||||||
|
return Symbols.domain;
|
||||||
|
case 'auth.login':
|
||||||
|
return Symbols.login;
|
||||||
|
case 'posts.new':
|
||||||
|
return Symbols.post_add;
|
||||||
|
case 'wallet.orders.paid':
|
||||||
|
return Symbols.shopping_bag;
|
||||||
|
case 'posts.reactions.new':
|
||||||
|
return Symbols.add_reaction;
|
||||||
|
default:
|
||||||
|
return Symbols.notifications;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
// Refresh unread count when sheet opens to sync across devices
|
||||||
|
ref.read(notificationUnreadCountNotifierProvider.notifier).refresh();
|
||||||
|
|
||||||
Future<void> markAllRead() async {
|
Future<void> markAllRead() async {
|
||||||
showLoadingModal(context);
|
showLoadingModal(context);
|
||||||
final apiClient = ref.watch(apiClientProvider);
|
final apiClient = ref.watch(apiClientProvider);
|
||||||
@@ -149,12 +188,30 @@ class NotificationSheet extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final notification = data.items[index];
|
final notification = data.items[index];
|
||||||
|
final pfp = notification.meta['pfp'] as String?;
|
||||||
|
final images = notification.meta['images'] as List?;
|
||||||
|
final imageIds = images?.cast<String>() ?? [];
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
isThreeLine: true,
|
isThreeLine: true,
|
||||||
contentPadding: EdgeInsets.symmetric(
|
contentPadding: EdgeInsets.symmetric(
|
||||||
horizontal: 16,
|
horizontal: 16,
|
||||||
vertical: 8,
|
vertical: 8,
|
||||||
),
|
),
|
||||||
|
leading:
|
||||||
|
pfp != null
|
||||||
|
? ProfilePictureWidget(fileId: pfp, radius: 20)
|
||||||
|
: CircleAvatar(
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).colorScheme.primaryContainer,
|
||||||
|
child: Icon(
|
||||||
|
_getNotificationIcon(notification.topic),
|
||||||
|
color:
|
||||||
|
Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.onPrimaryContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
title: Text(notification.title),
|
title: Text(notification.title),
|
||||||
subtitle: Column(
|
subtitle: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
@@ -187,6 +244,29 @@ class NotificationSheet extends HookConsumerWidget {
|
|||||||
).colorScheme.onSurface.withOpacity(0.8),
|
).colorScheme.onSurface.withOpacity(0.8),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (imageIds.isNotEmpty)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8),
|
||||||
|
child: Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 8,
|
||||||
|
children:
|
||||||
|
imageIds.map((imageId) {
|
||||||
|
return SizedBox(
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: CloudImageWidget(
|
||||||
|
fileId: imageId,
|
||||||
|
aspectRatio: 1,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
trailing:
|
trailing:
|
||||||
|
|||||||
@@ -451,7 +451,7 @@ class PollEditorScreen extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (confirmed == true) {
|
if (confirmed == true) {
|
||||||
Navigator.of(context).pop();
|
if (context.mounted) Navigator.of(context).pop();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|||||||
@@ -256,21 +256,25 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
ComposeFormFields(
|
Expanded(
|
||||||
state: state,
|
child: SingleChildScrollView(
|
||||||
showPublisherAvatar: false,
|
child: ComposeFormFields(
|
||||||
onPublisherTap: () {
|
state: state,
|
||||||
showModalBottomSheet(
|
showPublisherAvatar: false,
|
||||||
isScrollControlled: true,
|
onPublisherTap: () {
|
||||||
context: context,
|
showModalBottomSheet(
|
||||||
builder: (context) => const PublisherModal(),
|
isScrollControlled: true,
|
||||||
).then((value) {
|
context: context,
|
||||||
if (value != null) {
|
builder: (context) => const PublisherModal(),
|
||||||
state.currentPublisher.value = value;
|
).then((value) {
|
||||||
}
|
if (value != null) {
|
||||||
});
|
state.currentPublisher.value = value;
|
||||||
},
|
}
|
||||||
).padding(top: 16),
|
});
|
||||||
|
},
|
||||||
|
).padding(top: 16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
// Attachments preview
|
// Attachments preview
|
||||||
ValueListenableBuilder<List<UniversalFile>>(
|
ValueListenableBuilder<List<UniversalFile>>(
|
||||||
|
|||||||
@@ -145,9 +145,11 @@ class PostActionButtons extends HookConsumerWidget {
|
|||||||
message: 'delete'.tr(),
|
message: 'delete'.tr(),
|
||||||
child: FilledButton.tonal(
|
child: FilledButton.tonal(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
showConfirmAlert('deletePostHint'.tr(), 'deletePost'.tr()).then((
|
showConfirmAlert(
|
||||||
confirm,
|
'deletePostHint'.tr(),
|
||||||
) {
|
'deletePost'.tr(),
|
||||||
|
isDanger: true,
|
||||||
|
).then((confirm) {
|
||||||
if (confirm) {
|
if (confirm) {
|
||||||
final client = ref.watch(apiClientProvider);
|
final client = ref.watch(apiClientProvider);
|
||||||
client
|
client
|
||||||
|
|||||||
@@ -427,6 +427,7 @@ class _RealmActionMenu extends HookConsumerWidget {
|
|||||||
showConfirmAlert(
|
showConfirmAlert(
|
||||||
'deleteRealmHint'.tr(),
|
'deleteRealmHint'.tr(),
|
||||||
'deleteRealm'.tr(),
|
'deleteRealm'.tr(),
|
||||||
|
isDanger: true,
|
||||||
).then((confirm) {
|
).then((confirm) {
|
||||||
if (confirm) {
|
if (confirm) {
|
||||||
final client = ref.watch(apiClientProvider);
|
final client = ref.watch(apiClientProvider);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import 'package:island/widgets/navigation/fab_menu.dart';
|
|||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:island/pods/config.dart';
|
import 'package:island/pods/config.dart';
|
||||||
|
import 'package:island/pods/chat/chat_summary.dart';
|
||||||
|
|
||||||
final currentRouteProvider = StateProvider<String?>((ref) => null);
|
final currentRouteProvider = StateProvider<String?>((ref) => null);
|
||||||
|
|
||||||
@@ -50,6 +51,8 @@ class TabsScreen extends HookConsumerWidget {
|
|||||||
notificationUnreadCountNotifierProvider,
|
notificationUnreadCountNotifierProvider,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final chatUnreadCount = ref.watch(chatUnreadCountNotifierProvider);
|
||||||
|
|
||||||
final wideScreen = isWideScreen(context);
|
final wideScreen = isWideScreen(context);
|
||||||
|
|
||||||
final destinations = [
|
final destinations = [
|
||||||
@@ -59,7 +62,11 @@ class TabsScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
NavigationDestination(
|
NavigationDestination(
|
||||||
label: 'chat'.tr(),
|
label: 'chat'.tr(),
|
||||||
icon: const Icon(Symbols.forum_rounded),
|
icon: Badge.count(
|
||||||
|
count: chatUnreadCount.value ?? 0,
|
||||||
|
isLabelVisible: (chatUnreadCount.value ?? 0) > 0,
|
||||||
|
child: const Icon(Symbols.forum_rounded),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
NavigationDestination(
|
NavigationDestination(
|
||||||
label: 'realms'.tr(),
|
label: 'realms'.tr(),
|
||||||
|
|||||||
@@ -82,12 +82,32 @@ class _ParsedVersion implements Comparable<_ParsedVersion> {
|
|||||||
return _ParsedVersion(major, minor, patch, build);
|
return _ParsedVersion(major, minor, patch, build);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Normalize Android build numbers by removing architecture-based offsets
|
||||||
|
/// Android adds 1000 for x86, 2000 for ARMv7, 4000 for ARMv8
|
||||||
|
int get normalizedBuild {
|
||||||
|
// Check if build number has an architecture offset
|
||||||
|
// We detect this by checking if the build % 1000 is the base build
|
||||||
|
if (build >= 4000) {
|
||||||
|
// Likely ARMv8 (arm64-v8a) with +4000 offset
|
||||||
|
return build % 4000;
|
||||||
|
} else if (build >= 2000) {
|
||||||
|
// Likely ARMv7 (armeabi-v7a) with +2000 offset
|
||||||
|
return build % 2000;
|
||||||
|
} else if (build >= 1000) {
|
||||||
|
// Likely x86/x86_64 with +1000 offset
|
||||||
|
return build % 1000;
|
||||||
|
}
|
||||||
|
// No offset, return as-is
|
||||||
|
return build;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int compareTo(_ParsedVersion other) {
|
int compareTo(_ParsedVersion other) {
|
||||||
if (major != other.major) return major.compareTo(other.major);
|
if (major != other.major) return major.compareTo(other.major);
|
||||||
if (minor != other.minor) return minor.compareTo(other.minor);
|
if (minor != other.minor) return minor.compareTo(other.minor);
|
||||||
if (patch != other.patch) return patch.compareTo(other.patch);
|
if (patch != other.patch) return patch.compareTo(other.patch);
|
||||||
return build.compareTo(other.build);
|
// Use normalized build numbers for comparison to handle Android arch offsets
|
||||||
|
return normalizedBuild.compareTo(other.normalizedBuild);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -244,13 +264,14 @@ class UpdateService {
|
|||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
builder: (context) => _WindowsUpdateDialog(
|
builder:
|
||||||
updateUrl: url,
|
(context) => _WindowsUpdateDialog(
|
||||||
onComplete: () {
|
updateUrl: url,
|
||||||
// Close the update sheet
|
onComplete: () {
|
||||||
Navigator.of(context).pop();
|
// Close the update sheet
|
||||||
},
|
Navigator.of(context).pop();
|
||||||
),
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,7 +342,9 @@ class _WindowsUpdateDialog extends StatefulWidget {
|
|||||||
|
|
||||||
class _WindowsUpdateDialogState extends State<_WindowsUpdateDialog> {
|
class _WindowsUpdateDialogState extends State<_WindowsUpdateDialog> {
|
||||||
final ValueNotifier<double?> progressNotifier = ValueNotifier<double?>(null);
|
final ValueNotifier<double?> progressNotifier = ValueNotifier<double?>(null);
|
||||||
final ValueNotifier<String> messageNotifier = ValueNotifier<String>('Downloading installer...');
|
final ValueNotifier<String> messageNotifier = ValueNotifier<String>(
|
||||||
|
'Downloading installer...',
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -392,16 +415,17 @@ class _WindowsUpdateDialogState extends State<_WindowsUpdateDialog> {
|
|||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder:
|
||||||
title: const Text('Update Failed'),
|
(context) => AlertDialog(
|
||||||
content: Text(message),
|
title: const Text('Update Failed'),
|
||||||
actions: [
|
content: Text(message),
|
||||||
TextButton(
|
actions: [
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
TextButton(
|
||||||
child: const Text('OK'),
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: const Text('OK'),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -458,7 +482,9 @@ class _WindowsUpdateDialogState extends State<_WindowsUpdateDialog> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
talker.info('[Update] Windows installer downloaded successfully to: $filePath');
|
talker.info(
|
||||||
|
'[Update] Windows installer downloaded successfully to: $filePath',
|
||||||
|
);
|
||||||
return filePath;
|
return filePath;
|
||||||
} else {
|
} else {
|
||||||
talker.error(
|
talker.error(
|
||||||
@@ -500,7 +526,9 @@ class _WindowsUpdateDialogState extends State<_WindowsUpdateDialog> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
talker.info('[Update] Windows installer extracted successfully to: $extractDir');
|
talker.info(
|
||||||
|
'[Update] Windows installer extracted successfully to: $extractDir',
|
||||||
|
);
|
||||||
return extractDir;
|
return extractDir;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
talker.error('[Update] Error extracting Windows installer: $e');
|
talker.error('[Update] Error extracting Windows installer: $e');
|
||||||
@@ -514,10 +542,11 @@ class _WindowsUpdateDialogState extends State<_WindowsUpdateDialog> {
|
|||||||
talker.info('[Update] Running Windows installer from: $extractDir');
|
talker.info('[Update] Running Windows installer from: $extractDir');
|
||||||
|
|
||||||
final dir = Directory(extractDir);
|
final dir = Directory(extractDir);
|
||||||
final exeFiles = dir
|
final exeFiles =
|
||||||
.listSync()
|
dir
|
||||||
.where((f) => f is File && f.path.endsWith('.exe'))
|
.listSync()
|
||||||
.toList();
|
.where((f) => f is File && f.path.endsWith('.exe'))
|
||||||
|
.toList();
|
||||||
|
|
||||||
if (exeFiles.isEmpty) {
|
if (exeFiles.isEmpty) {
|
||||||
talker.info('[Update] No .exe file found in extracted directory');
|
talker.info('[Update] No .exe file found in extracted directory');
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ class AccountSessionSheet extends HookConsumerWidget {
|
|||||||
final confirm = await showConfirmAlert(
|
final confirm = await showConfirmAlert(
|
||||||
'authDeviceLogoutHint'.tr(),
|
'authDeviceLogoutHint'.tr(),
|
||||||
'authDeviceLogout'.tr(),
|
'authDeviceLogout'.tr(),
|
||||||
|
isDanger: true,
|
||||||
);
|
);
|
||||||
if (!confirm || !context.mounted) return;
|
if (!confirm || !context.mounted) return;
|
||||||
try {
|
try {
|
||||||
@@ -276,6 +277,7 @@ class AccountSessionSheet extends HookConsumerWidget {
|
|||||||
final confirm = await showConfirmAlert(
|
final confirm = await showConfirmAlert(
|
||||||
'authDeviceLogoutHint'.tr(),
|
'authDeviceLogoutHint'.tr(),
|
||||||
'authDeviceLogout'.tr(),
|
'authDeviceLogout'.tr(),
|
||||||
|
isDanger: true,
|
||||||
);
|
);
|
||||||
if (confirm && context.mounted) {
|
if (confirm && context.mounted) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -25,12 +25,24 @@ const Map<String, Color> kUsernamePlainColors = {
|
|||||||
'white': Colors.white,
|
'white': Colors.white,
|
||||||
};
|
};
|
||||||
|
|
||||||
const kVerificationMarkColors = [
|
const List<IconData> kVerificationMarkIcons = [
|
||||||
|
Symbols.build_circle,
|
||||||
|
Symbols.verified,
|
||||||
|
Symbols.verified,
|
||||||
|
Symbols.account_balance,
|
||||||
|
Symbols.palette,
|
||||||
|
Symbols.code,
|
||||||
|
Symbols.masks,
|
||||||
|
];
|
||||||
|
|
||||||
|
const List<Color> kVerificationMarkColors = [
|
||||||
Colors.teal,
|
Colors.teal,
|
||||||
Colors.blue,
|
|
||||||
Colors.amber,
|
|
||||||
Colors.blueGrey,
|
|
||||||
Colors.lightBlue,
|
Colors.lightBlue,
|
||||||
|
Colors.indigo,
|
||||||
|
Colors.red,
|
||||||
|
Colors.orange,
|
||||||
|
Colors.blue,
|
||||||
|
Colors.blueAccent,
|
||||||
];
|
];
|
||||||
|
|
||||||
class AccountName extends StatelessWidget {
|
class AccountName extends StatelessWidget {
|
||||||
@@ -39,6 +51,7 @@ class AccountName extends StatelessWidget {
|
|||||||
final String? textOverride;
|
final String? textOverride;
|
||||||
final bool ignorePermissions;
|
final bool ignorePermissions;
|
||||||
final bool hideVerificationMark;
|
final bool hideVerificationMark;
|
||||||
|
final bool hideOverlay;
|
||||||
const AccountName({
|
const AccountName({
|
||||||
super.key,
|
super.key,
|
||||||
required this.account,
|
required this.account,
|
||||||
@@ -46,6 +59,7 @@ class AccountName extends StatelessWidget {
|
|||||||
this.textOverride,
|
this.textOverride,
|
||||||
this.ignorePermissions = false,
|
this.ignorePermissions = false,
|
||||||
this.hideVerificationMark = false,
|
this.hideVerificationMark = false,
|
||||||
|
this.hideOverlay = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
Alignment _parseGradientDirection(String direction) {
|
Alignment _parseGradientDirection(String direction) {
|
||||||
@@ -189,20 +203,33 @@ class AccountName extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (account.perkSubscription != null)
|
if (account.perkSubscription != null)
|
||||||
StellarMembershipMark(membership: account.perkSubscription!),
|
StellarMembershipMark(
|
||||||
|
membership: account.perkSubscription!,
|
||||||
|
hideOverlay: hideOverlay,
|
||||||
|
),
|
||||||
if (account.profile.verification != null &&
|
if (account.profile.verification != null &&
|
||||||
!hideVerificationMark)
|
!hideVerificationMark)
|
||||||
VerificationMark(mark: account.profile.verification!),
|
VerificationMark(
|
||||||
if (account.automatedId != null)
|
mark: account.profile.verification!,
|
||||||
Tooltip(
|
hideOverlay: hideOverlay,
|
||||||
message: 'accountAutomated'.tr(),
|
|
||||||
child: Icon(
|
|
||||||
Symbols.smart_toy,
|
|
||||||
size: 16,
|
|
||||||
color: nameStyle.color,
|
|
||||||
fill: 1,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
if (account.automatedId != null)
|
||||||
|
hideOverlay
|
||||||
|
? Icon(
|
||||||
|
Symbols.smart_toy,
|
||||||
|
size: 16,
|
||||||
|
color: nameStyle.color,
|
||||||
|
fill: 1,
|
||||||
|
)
|
||||||
|
: Tooltip(
|
||||||
|
message: 'accountAutomated'.tr(),
|
||||||
|
child: Icon(
|
||||||
|
Symbols.smart_toy,
|
||||||
|
size: 16,
|
||||||
|
color: nameStyle.color,
|
||||||
|
fill: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -226,26 +253,39 @@ class AccountName extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Text(
|
child: Text(
|
||||||
account.nick,
|
textOverride ?? account.nick,
|
||||||
style: nameStyle,
|
style: nameStyle,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (account.perkSubscription != null)
|
if (account.perkSubscription != null)
|
||||||
StellarMembershipMark(membership: account.perkSubscription!),
|
StellarMembershipMark(
|
||||||
if (account.profile.verification != null)
|
membership: account.perkSubscription!,
|
||||||
VerificationMark(mark: account.profile.verification!),
|
hideOverlay: hideOverlay,
|
||||||
if (account.automatedId != null)
|
|
||||||
Tooltip(
|
|
||||||
message: 'accountAutomated'.tr(),
|
|
||||||
child: Icon(
|
|
||||||
Symbols.smart_toy,
|
|
||||||
size: 16,
|
|
||||||
color: nameStyle.color,
|
|
||||||
fill: 1,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
if (account.profile.verification != null)
|
||||||
|
VerificationMark(
|
||||||
|
mark: account.profile.verification!,
|
||||||
|
hideOverlay: hideOverlay,
|
||||||
|
),
|
||||||
|
if (account.automatedId != null)
|
||||||
|
hideOverlay
|
||||||
|
? Icon(
|
||||||
|
Symbols.smart_toy,
|
||||||
|
size: 16,
|
||||||
|
color: nameStyle.color,
|
||||||
|
fill: 1,
|
||||||
|
)
|
||||||
|
: Tooltip(
|
||||||
|
message: 'accountAutomated'.tr(),
|
||||||
|
child: Icon(
|
||||||
|
Symbols.smart_toy,
|
||||||
|
size: 16,
|
||||||
|
color: nameStyle.color,
|
||||||
|
fill: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -253,39 +293,54 @@ class AccountName extends StatelessWidget {
|
|||||||
|
|
||||||
class VerificationMark extends StatelessWidget {
|
class VerificationMark extends StatelessWidget {
|
||||||
final SnVerificationMark mark;
|
final SnVerificationMark mark;
|
||||||
const VerificationMark({super.key, required this.mark});
|
final bool hideOverlay;
|
||||||
|
const VerificationMark({
|
||||||
|
super.key,
|
||||||
|
required this.mark,
|
||||||
|
this.hideOverlay = false,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Tooltip(
|
final icon = Icon(
|
||||||
richMessage: TextSpan(
|
(kVerificationMarkIcons.length > mark.type && mark.type >= 0)
|
||||||
text: mark.title ?? 'No title',
|
? kVerificationMarkIcons[mark.type]
|
||||||
children: [
|
: Symbols.verified,
|
||||||
TextSpan(text: '\n'),
|
size: 16,
|
||||||
TextSpan(
|
color:
|
||||||
text: mark.description ?? 'descriptionNone'.tr(),
|
(kVerificationMarkColors.length > mark.type && mark.type >= 0)
|
||||||
style: TextStyle(fontWeight: FontWeight.normal),
|
? kVerificationMarkColors[mark.type]
|
||||||
),
|
: Colors.blue,
|
||||||
],
|
fill: 1,
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
child: Icon(
|
|
||||||
mark.type == 4
|
|
||||||
? Symbols.play_circle
|
|
||||||
: mark.type == 0
|
|
||||||
? Symbols.build_circle
|
|
||||||
: Symbols.verified,
|
|
||||||
size: 16,
|
|
||||||
color: kVerificationMarkColors[mark.type],
|
|
||||||
fill: 1,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return hideOverlay
|
||||||
|
? icon
|
||||||
|
: Tooltip(
|
||||||
|
richMessage: TextSpan(
|
||||||
|
text: mark.title ?? 'No title',
|
||||||
|
children: [
|
||||||
|
TextSpan(text: '\n'),
|
||||||
|
TextSpan(
|
||||||
|
text: mark.description ?? 'descriptionNone'.tr(),
|
||||||
|
style: TextStyle(fontWeight: FontWeight.normal),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
child: icon,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class StellarMembershipMark extends StatelessWidget {
|
class StellarMembershipMark extends StatelessWidget {
|
||||||
final SnWalletSubscriptionRef membership;
|
final SnWalletSubscriptionRef membership;
|
||||||
const StellarMembershipMark({super.key, required this.membership});
|
final bool hideOverlay;
|
||||||
|
const StellarMembershipMark({
|
||||||
|
super.key,
|
||||||
|
required this.membership,
|
||||||
|
this.hideOverlay = false,
|
||||||
|
});
|
||||||
|
|
||||||
String _getMembershipTierName(String identifier) {
|
String _getMembershipTierName(String identifier) {
|
||||||
switch (identifier) {
|
switch (identifier) {
|
||||||
@@ -321,20 +376,24 @@ class StellarMembershipMark extends StatelessWidget {
|
|||||||
final tierColor = _getMembershipTierColor(membership.identifier);
|
final tierColor = _getMembershipTierColor(membership.identifier);
|
||||||
final tierIcon = Symbols.kid_star;
|
final tierIcon = Symbols.kid_star;
|
||||||
|
|
||||||
return Tooltip(
|
final icon = Icon(tierIcon, size: 16, color: tierColor, fill: 1);
|
||||||
richMessage: TextSpan(
|
|
||||||
text: 'stellarMembership'.tr(),
|
return hideOverlay
|
||||||
children: [
|
? icon
|
||||||
TextSpan(text: '\n'),
|
: Tooltip(
|
||||||
TextSpan(
|
richMessage: TextSpan(
|
||||||
text: 'currentMembershipMember'.tr(args: [tierName]),
|
text: 'stellarMembership'.tr(),
|
||||||
style: TextStyle(fontWeight: FontWeight.normal),
|
children: [
|
||||||
|
TextSpan(text: '\n'),
|
||||||
|
TextSpan(
|
||||||
|
text: 'currentMembershipMember'.tr(args: [tierName]),
|
||||||
|
style: TextStyle(fontWeight: FontWeight.normal),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
],
|
child: icon,
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
);
|
||||||
),
|
|
||||||
child: Icon(tierIcon, size: 16, color: tierColor, fill: 1),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -348,13 +407,14 @@ class VerificationStatusCard extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
mark.type == 4
|
(kVerificationMarkIcons.length > mark.type && mark.type >= 0)
|
||||||
? Symbols.play_circle
|
? kVerificationMarkIcons[mark.type]
|
||||||
: mark.type == 0
|
|
||||||
? Symbols.build_circle
|
|
||||||
: Symbols.verified,
|
: Symbols.verified,
|
||||||
size: 32,
|
size: 32,
|
||||||
color: kVerificationMarkColors[mark.type],
|
color:
|
||||||
|
(kVerificationMarkColors.length > mark.type && mark.type >= 0)
|
||||||
|
? kVerificationMarkColors[mark.type]
|
||||||
|
: Colors.blue,
|
||||||
fill: 1,
|
fill: 1,
|
||||||
).alignment(Alignment.centerLeft),
|
).alignment(Alignment.centerLeft),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
|
|||||||
@@ -1,11 +1,24 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:fl_heatmap/fl_heatmap.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/heatmap.dart';
|
import 'package:island/models/heatmap.dart';
|
||||||
import '../services/responsive.dart';
|
import 'package:island/services/responsive.dart';
|
||||||
|
|
||||||
|
/// Custom data class for selected heatmap item
|
||||||
|
class SelectedHeatmapItem {
|
||||||
|
final double value;
|
||||||
|
final String unit;
|
||||||
|
final String dateString;
|
||||||
|
final String dayLabel;
|
||||||
|
|
||||||
|
SelectedHeatmapItem({
|
||||||
|
required this.value,
|
||||||
|
required this.unit,
|
||||||
|
required this.dateString,
|
||||||
|
required this.dayLabel,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// A reusable heatmap widget for displaying activity data in GitHub-style layout.
|
/// A reusable heatmap widget for displaying activity data in GitHub-style layout.
|
||||||
/// Shows exactly 365 days (wide screen) or 90 days (non-wide screen) of data ending at the current date.
|
/// Shows exactly 365 days (wide screen) or 90 days (non-wide screen) of data ending at the current date.
|
||||||
@@ -21,7 +34,7 @@ class ActivityHeatmapWidget extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final selectedItem = useState<HeatmapItem?>(null);
|
final selectedItem = useState<SelectedHeatmapItem?>(null);
|
||||||
|
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
|
|
||||||
@@ -101,48 +114,18 @@ class ActivityHeatmapWidget extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final heatmapData = HeatmapData(
|
// Find maximum value for color scaling
|
||||||
rows: [
|
final maxValue =
|
||||||
'Mon',
|
dataMap.values.isNotEmpty
|
||||||
'Tue',
|
? dataMap.values.reduce((a, b) => a > b ? a : b)
|
||||||
'Wed',
|
: 1.0;
|
||||||
'Thu',
|
|
||||||
'Fri',
|
// Helper function to get color based on activity level
|
||||||
'Sat',
|
Color getActivityColor(double value) {
|
||||||
'Sun',
|
if (value == 0) return Colors.grey.withOpacity(0.1);
|
||||||
], // Days of week vertically
|
final intensity = value / maxValue;
|
||||||
columns:
|
return Colors.green.withOpacity(0.2 + (intensity * 0.8));
|
||||||
weeks
|
}
|
||||||
.map(
|
|
||||||
(w) =>
|
|
||||||
'${w.year}-${w.month.toString().padLeft(2, '0')}-${w.day.toString().padLeft(2, '0')}',
|
|
||||||
)
|
|
||||||
.toList(), // Weeks horizontally
|
|
||||||
items: [
|
|
||||||
for (int day = 0; day < 7; day++) // For each day of week (Mon-Sun)
|
|
||||||
for (final week in weeks) // For each week
|
|
||||||
HeatmapItem(
|
|
||||||
value: dataMap[week.add(Duration(days: day))] ?? 0.0,
|
|
||||||
unit: heatmap.unit,
|
|
||||||
xAxisLabel:
|
|
||||||
'${week.year}-${week.month.toString().padLeft(2, '0')}-${week.day.toString().padLeft(2, '0')}',
|
|
||||||
yAxisLabel:
|
|
||||||
day == 0
|
|
||||||
? 'Mon'
|
|
||||||
: day == 1
|
|
||||||
? 'Tue'
|
|
||||||
: day == 2
|
|
||||||
? 'Wed'
|
|
||||||
: day == 3
|
|
||||||
? 'Thu'
|
|
||||||
: day == 4
|
|
||||||
? 'Fri'
|
|
||||||
: day == 5
|
|
||||||
? 'Sat'
|
|
||||||
: 'Sun',
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
@@ -151,39 +134,103 @@ class ActivityHeatmapWidget extends HookConsumerWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
// Month labels row - aligned with month start positions
|
||||||
'activityHeatmap',
|
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
|
||||||
).tr(),
|
|
||||||
const Gap(8),
|
|
||||||
// Month labels row
|
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(width: 30), // Space for day labels
|
const SizedBox(width: 30), // Space for day labels
|
||||||
...monthLabels.asMap().entries.map((entry) {
|
...List.generate(weeks.length, (weekIndex) {
|
||||||
final month = entry.value;
|
// Check if this week is the start of a month
|
||||||
|
final monthIndex = monthPositions.indexOf(weekIndex);
|
||||||
|
final monthText =
|
||||||
|
monthIndex != -1 ? monthLabels[monthIndex] : null;
|
||||||
|
|
||||||
return Expanded(
|
return monthText != null
|
||||||
child: Container(
|
? Expanded(
|
||||||
alignment: Alignment.center,
|
child: Text(
|
||||||
child: Text(
|
monthText,
|
||||||
month,
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
textAlign: TextAlign.center,
|
||||||
textAlign: TextAlign.center,
|
),
|
||||||
),
|
)
|
||||||
),
|
: SizedBox.shrink();
|
||||||
);
|
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const Gap(4),
|
const Gap(4),
|
||||||
Heatmap(
|
// Custom heatmap grid
|
||||||
heatmapData: heatmapData,
|
Column(
|
||||||
rowsVisible: 7,
|
children: List.generate(7, (dayIndex) {
|
||||||
showXAxisLabels: false,
|
final dayLabels = [
|
||||||
onItemSelectedListener: (item) {
|
'Mon',
|
||||||
selectedItem.value = item;
|
'Tue',
|
||||||
},
|
'Wed',
|
||||||
|
'Thu',
|
||||||
|
'Fri',
|
||||||
|
'Sat',
|
||||||
|
'Sun',
|
||||||
|
];
|
||||||
|
final dayLabel = dayLabels[dayIndex];
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
// Day label
|
||||||
|
SizedBox(
|
||||||
|
width: 30,
|
||||||
|
child: Text(
|
||||||
|
dayLabel,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Activity squares for each week - evenly distributed
|
||||||
|
Expanded(
|
||||||
|
child: Row(
|
||||||
|
children: List.generate(weeks.length, (weekIndex) {
|
||||||
|
final week = weeks[weekIndex];
|
||||||
|
final date = week.add(Duration(days: dayIndex));
|
||||||
|
final value = dataMap[date] ?? 0.0;
|
||||||
|
final dateString =
|
||||||
|
'${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
||||||
|
|
||||||
|
return Expanded(
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
selectedItem.value = SelectedHeatmapItem(
|
||||||
|
value: value,
|
||||||
|
unit: heatmap.unit,
|
||||||
|
dateString: dateString,
|
||||||
|
dayLabel: dayLabel,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
height: 12,
|
||||||
|
margin: const EdgeInsets.all(1),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: getActivityColor(value),
|
||||||
|
borderRadius: BorderRadius.circular(2),
|
||||||
|
border:
|
||||||
|
selectedItem.value != null &&
|
||||||
|
selectedItem.value!.dateString ==
|
||||||
|
dateString &&
|
||||||
|
selectedItem.value!.dayLabel ==
|
||||||
|
dayLabel
|
||||||
|
? Border.all(
|
||||||
|
color: Colors.blue,
|
||||||
|
width: 1,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
// Legend
|
// Legend
|
||||||
@@ -203,9 +250,7 @@ class ActivityHeatmapWidget extends HookConsumerWidget {
|
|||||||
style: Theme.of(context).textTheme.bodySmall,
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
),
|
),
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: _formatDate(
|
text: _formatDate(selectedItem.value!.dateString),
|
||||||
selectedItem.value!.xAxisLabel ?? '',
|
|
||||||
),
|
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:dio/dio.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:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:island/main.dart';
|
import 'package:island/main.dart';
|
||||||
|
import 'package:island/talker.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:top_snackbar_flutter/top_snack_bar.dart';
|
import 'package:top_snackbar_flutter/top_snack_bar.dart';
|
||||||
|
|
||||||
export 'content/alert.native.dart'
|
|
||||||
if (dart.library.html) 'content/alert.web.dart';
|
|
||||||
|
|
||||||
void showSnackBar(String message, {SnackBarAction? action}) {
|
void showSnackBar(String message, {SnackBarAction? action}) {
|
||||||
final context = globalOverlay.currentState!.context;
|
final context = globalOverlay.currentState!.context;
|
||||||
final screenWidth = MediaQuery.of(context).size.width;
|
final screenWidth = MediaQuery.of(context).size.width;
|
||||||
@@ -29,43 +31,60 @@ void showSnackBar(String message, {SnackBarAction? action}) {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
snackBarPosition: SnackBarPosition.bottom,
|
snackBarPosition: SnackBarPosition.bottom,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void clearSnackBar(BuildContext context) {
|
|
||||||
ScaffoldMessenger.of(context).clearSnackBars();
|
|
||||||
}
|
|
||||||
|
|
||||||
OverlayEntry? _loadingOverlay;
|
OverlayEntry? _loadingOverlay;
|
||||||
GlobalKey<_FadeOverlayState> _loadingOverlayKey = GlobalKey();
|
GlobalKey<_FadeOverlayState> _loadingOverlayKey = GlobalKey();
|
||||||
|
|
||||||
class _FadeOverlay extends StatefulWidget {
|
class _FadeOverlay extends StatefulWidget {
|
||||||
const _FadeOverlay({super.key, required this.child});
|
const _FadeOverlay({
|
||||||
final Widget child;
|
super.key,
|
||||||
|
this.child,
|
||||||
|
this.builder,
|
||||||
|
this.duration = const Duration(milliseconds: 200),
|
||||||
|
this.curve = Curves.linear,
|
||||||
|
}) : assert(child != null || builder != null);
|
||||||
|
|
||||||
|
final Widget? child;
|
||||||
|
final Widget Function(BuildContext, Animation<double>)? builder;
|
||||||
|
final Duration duration;
|
||||||
|
final Curve curve;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_FadeOverlay> createState() => _FadeOverlayState();
|
State<_FadeOverlay> createState() => _FadeOverlayState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FadeOverlayState extends State<_FadeOverlay> {
|
class _FadeOverlayState extends State<_FadeOverlay>
|
||||||
bool _visible = false;
|
with SingleTickerProviderStateMixin {
|
||||||
|
late AnimationController _controller;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
_controller = AnimationController(vsync: this, duration: widget.duration);
|
||||||
setState(() => _visible = true);
|
_controller.forward();
|
||||||
});
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> animateOut() async {
|
||||||
|
await _controller.reverse();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AnimatedOpacity(
|
final animation = CurvedAnimation(parent: _controller, curve: widget.curve);
|
||||||
opacity: _visible ? 1.0 : 0.0,
|
if (widget.builder != null) {
|
||||||
duration: const Duration(milliseconds: 200),
|
return widget.builder!(context, animation);
|
||||||
child: widget.child,
|
}
|
||||||
);
|
return FadeTransition(opacity: animation, child: widget.child);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,10 +128,250 @@ void hideLoadingModal(BuildContext context) async {
|
|||||||
final state = entry.mounted ? _loadingOverlayKey.currentState : null;
|
final state = entry.mounted ? _loadingOverlayKey.currentState : null;
|
||||||
|
|
||||||
if (state != null) {
|
if (state != null) {
|
||||||
// ignore: invalid_use_of_protected_member
|
await state.animateOut();
|
||||||
state.setState(() => state._visible = false);
|
|
||||||
await Future.delayed(const Duration(milliseconds: 200));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
entry.remove();
|
entry.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _parseRemoteError(DioException err) {
|
||||||
|
String? message;
|
||||||
|
if (err.response?.data is String) {
|
||||||
|
message = err.response?.data;
|
||||||
|
} else if (err.response?.data?['message'] != null) {
|
||||||
|
message = <String?>[
|
||||||
|
err.response?.data?['message']?.toString(),
|
||||||
|
err.response?.data?['detail']?.toString(),
|
||||||
|
].where((e) => e != null).cast<String>().map((e) => e.trim()).join('\n');
|
||||||
|
} else if (err.response?.data?['errors'] != null) {
|
||||||
|
final errors = err.response?.data['errors'] as Map<String, dynamic>;
|
||||||
|
message = errors.values
|
||||||
|
.map(
|
||||||
|
(ele) =>
|
||||||
|
(ele as List<dynamic>).map((ele) => ele.toString()).join('\n'),
|
||||||
|
)
|
||||||
|
.join('\n');
|
||||||
|
}
|
||||||
|
if (message == null || message.isEmpty) message = err.response?.statusMessage;
|
||||||
|
message ??= err.message;
|
||||||
|
return message ?? err.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track active overlay dialogs for dismissal
|
||||||
|
final List<void Function()> _activeOverlayDialogs = [];
|
||||||
|
|
||||||
|
Future<T?> showOverlayDialog<T>({
|
||||||
|
required Widget Function(BuildContext context, void Function(T? result) close)
|
||||||
|
builder,
|
||||||
|
bool barrierDismissible = true,
|
||||||
|
}) {
|
||||||
|
final completer = Completer<T?>();
|
||||||
|
final key = GlobalKey<_FadeOverlayState>();
|
||||||
|
late OverlayEntry entry;
|
||||||
|
|
||||||
|
void close(T? result) async {
|
||||||
|
if (completer.isCompleted) return;
|
||||||
|
|
||||||
|
final state = key.currentState;
|
||||||
|
if (state != null) {
|
||||||
|
await state.animateOut();
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.remove();
|
||||||
|
_activeOverlayDialogs.remove(close);
|
||||||
|
completer.complete(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
entry = OverlayEntry(
|
||||||
|
builder:
|
||||||
|
(context) => _FadeOverlay(
|
||||||
|
key: key,
|
||||||
|
duration: const Duration(milliseconds: 150),
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
builder: (context, animation) {
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
Positioned.fill(
|
||||||
|
child: FadeTransition(
|
||||||
|
opacity: animation,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: barrierDismissible ? () => close(null) : null,
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
child: const ColoredBox(color: Colors.black54),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Center(
|
||||||
|
child: SlideTransition(
|
||||||
|
position: Tween<Offset>(
|
||||||
|
begin: const Offset(0, 0.05),
|
||||||
|
end: Offset.zero,
|
||||||
|
).animate(animation),
|
||||||
|
child: FadeTransition(
|
||||||
|
opacity: animation,
|
||||||
|
child: builder(context, close),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
_activeOverlayDialogs.add(() => close(null));
|
||||||
|
globalOverlay.currentState!.insert(entry);
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the topmost overlay dialog if any exists
|
||||||
|
bool closeTopmostOverlayDialog() {
|
||||||
|
if (_activeOverlayDialogs.isNotEmpty) {
|
||||||
|
final closeFunc = _activeOverlayDialogs.last;
|
||||||
|
closeFunc();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const kDialogMaxWidth = 480.0;
|
||||||
|
|
||||||
|
void showErrorAlert(dynamic err, {IconData? icon}) {
|
||||||
|
if (err is Error) {
|
||||||
|
talker.error('Something went wrong...', err, err.stackTrace);
|
||||||
|
}
|
||||||
|
final text = switch (err) {
|
||||||
|
String _ => err,
|
||||||
|
DioException _ => _parseRemoteError(err),
|
||||||
|
Exception _ => err.toString(),
|
||||||
|
_ => err.toString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
showOverlayDialog<void>(
|
||||||
|
builder:
|
||||||
|
(context, close) => ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: kDialogMaxWidth),
|
||||||
|
child: AlertDialog(
|
||||||
|
title: null,
|
||||||
|
titlePadding: EdgeInsets.zero,
|
||||||
|
contentPadding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
icon ?? Icons.error_outline_rounded,
|
||||||
|
size: 48,
|
||||||
|
color: Theme.of(context).colorScheme.error,
|
||||||
|
),
|
||||||
|
const Gap(16),
|
||||||
|
Text(
|
||||||
|
'somethingWentWrong'.tr(),
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
Text(text),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => close(null),
|
||||||
|
child: Text(MaterialLocalizations.of(context).okButtonLabel),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void showInfoAlert(String message, String title, {IconData? icon}) {
|
||||||
|
showOverlayDialog<void>(
|
||||||
|
builder:
|
||||||
|
(context, close) => ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: kDialogMaxWidth),
|
||||||
|
child: AlertDialog(
|
||||||
|
title: null,
|
||||||
|
titlePadding: EdgeInsets.zero,
|
||||||
|
contentPadding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
icon ?? Symbols.info_rounded,
|
||||||
|
fill: 1,
|
||||||
|
size: 48,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
const Gap(16),
|
||||||
|
Text(title, style: Theme.of(context).textTheme.titleLarge),
|
||||||
|
const Gap(8),
|
||||||
|
Text(message),
|
||||||
|
const Gap(8),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => close(null),
|
||||||
|
child: Text(MaterialLocalizations.of(context).okButtonLabel),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> showConfirmAlert(
|
||||||
|
String message,
|
||||||
|
String title, {
|
||||||
|
IconData? icon,
|
||||||
|
bool isDanger = false,
|
||||||
|
}) async {
|
||||||
|
final result = await showOverlayDialog<bool>(
|
||||||
|
builder:
|
||||||
|
(context, close) => ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: kDialogMaxWidth),
|
||||||
|
child: AlertDialog(
|
||||||
|
title: null,
|
||||||
|
titlePadding: EdgeInsets.zero,
|
||||||
|
contentPadding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
icon ?? Symbols.help_rounded,
|
||||||
|
size: 48,
|
||||||
|
fill: 1,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
const Gap(16),
|
||||||
|
Text(title, style: Theme.of(context).textTheme.titleLarge),
|
||||||
|
const Gap(8),
|
||||||
|
Text(message),
|
||||||
|
const Gap(8),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => close(false),
|
||||||
|
child: Text(
|
||||||
|
MaterialLocalizations.of(context).cancelButtonLabel,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => close(true),
|
||||||
|
style:
|
||||||
|
isDanger
|
||||||
|
? TextButton.styleFrom(
|
||||||
|
foregroundColor: Theme.of(context).colorScheme.error,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
child: Text(MaterialLocalizations.of(context).okButtonLabel),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return result ?? false;
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import 'package:island/route.dart';
|
|||||||
import 'package:island/pods/userinfo.dart';
|
import 'package:island/pods/userinfo.dart';
|
||||||
import 'package:island/pods/websocket.dart';
|
import 'package:island/pods/websocket.dart';
|
||||||
import 'package:island/services/responsive.dart';
|
import 'package:island/services/responsive.dart';
|
||||||
|
import 'package:island/widgets/alert.dart';
|
||||||
import 'package:island/widgets/upload_overlay.dart';
|
import 'package:island/widgets/upload_overlay.dart';
|
||||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
@@ -364,6 +365,12 @@ class PopAction extends Action<PopIntent> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void invoke(PopIntent intent) {
|
void invoke(PopIntent intent) {
|
||||||
|
// First, try to close any overlay dialogs
|
||||||
|
if (closeTopmostOverlayDialog()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no overlay to close, pop the route
|
||||||
if (ref.watch(routerProvider).canPop()) {
|
if (ref.watch(routerProvider).canPop()) {
|
||||||
ref.read(routerProvider).pop();
|
ref.read(routerProvider).pop();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,6 +130,17 @@ class _AppWrapperState extends ConsumerState<AppWrapper>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Special handling for share intent deep links
|
||||||
|
// Share intents are handled by SharingIntentService showing a modal,
|
||||||
|
// not by routing to a page
|
||||||
|
if (path == '/share') {
|
||||||
|
if (!kIsWeb &&
|
||||||
|
(Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
|
||||||
|
windowManager.show();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final router = ref.read(routerProvider);
|
final router = ref.read(routerProvider);
|
||||||
if (uri.queryParameters.isNotEmpty) {
|
if (uri.queryParameters.isNotEmpty) {
|
||||||
path =
|
path =
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/chat.dart';
|
import 'package:island/models/chat.dart';
|
||||||
@@ -28,12 +28,12 @@ Future<SnRealtimeCall?> ongoingCall(Ref ref, String roomId) async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class AudioCallButton extends HookConsumerWidget {
|
class AudioCallButton extends HookConsumerWidget {
|
||||||
final String roomId;
|
final SnChatRoom room;
|
||||||
const AudioCallButton({super.key, required this.roomId});
|
const AudioCallButton({super.key, required this.room});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final ongoingCall = ref.watch(ongoingCallProvider(roomId));
|
final ongoingCall = ref.watch(ongoingCallProvider(room.id));
|
||||||
final callState = ref.watch(callNotifierProvider);
|
final callState = ref.watch(callNotifierProvider);
|
||||||
final callNotifier = ref.read(callNotifierProvider.notifier);
|
final callNotifier = ref.read(callNotifierProvider.notifier);
|
||||||
final isLoading = useState(false);
|
final isLoading = useState(false);
|
||||||
@@ -42,10 +42,9 @@ class AudioCallButton extends HookConsumerWidget {
|
|||||||
Future<void> handleJoin() async {
|
Future<void> handleJoin() async {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
try {
|
try {
|
||||||
await apiClient.post('/sphere/chat/realtime/$roomId');
|
await apiClient.post('/sphere/chat/realtime/${room.id}');
|
||||||
if (context.mounted) {
|
// Just join the room, the overlay will handle the UI
|
||||||
context.pushNamed('chatCall', pathParameters: {'id': roomId});
|
await callNotifier.joinRoom(room);
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showErrorAlert(e);
|
showErrorAlert(e);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -56,7 +55,7 @@ class AudioCallButton extends HookConsumerWidget {
|
|||||||
Future<void> handleEnd() async {
|
Future<void> handleEnd() async {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
try {
|
try {
|
||||||
await apiClient.delete('/sphere/chat/realtime/$roomId');
|
await apiClient.delete('/sphere/chat/realtime/${room.id}');
|
||||||
callNotifier.dispose(); // Clean up call resources
|
callNotifier.dispose(); // Clean up call resources
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showErrorAlert(e);
|
showErrorAlert(e);
|
||||||
@@ -94,9 +93,14 @@ class AudioCallButton extends HookConsumerWidget {
|
|||||||
return IconButton(
|
return IconButton(
|
||||||
icon: const Icon(Icons.call),
|
icon: const Icon(Icons.call),
|
||||||
tooltip: 'Join Ongoing Call',
|
tooltip: 'Join Ongoing Call',
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
if (context.mounted) {
|
isLoading.value = true;
|
||||||
context.pushNamed('chatCall', pathParameters: {'id': roomId});
|
try {
|
||||||
|
await callNotifier.joinRoom(room);
|
||||||
|
} catch (e) {
|
||||||
|
showErrorAlert(e);
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -105,7 +109,7 @@ class AudioCallButton extends HookConsumerWidget {
|
|||||||
// Show join/start call button
|
// Show join/start call button
|
||||||
return IconButton(
|
return IconButton(
|
||||||
icon: const Icon(Icons.call),
|
icon: const Icon(Icons.call),
|
||||||
tooltip: 'Start/Join Call',
|
tooltip: 'Start Call',
|
||||||
onPressed: handleJoin,
|
onPressed: handleJoin,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
80
lib/widgets/chat/call_content.dart
Normal file
80
lib/widgets/chat/call_content.dart
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:island/pods/chat/call.dart';
|
||||||
|
import 'package:island/widgets/chat/call_participant_tile.dart';
|
||||||
|
import 'package:livekit_client/livekit_client.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
|
||||||
|
class CallContent extends HookConsumerWidget {
|
||||||
|
const CallContent({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final callState = ref.watch(callNotifierProvider);
|
||||||
|
final callNotifier = ref.watch(callNotifierProvider.notifier);
|
||||||
|
|
||||||
|
if (!callState.isConnected) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
if (callNotifier.participants.isEmpty) {
|
||||||
|
return const Center(child: Text('No participants in call'));
|
||||||
|
}
|
||||||
|
|
||||||
|
final participants = callNotifier.participants;
|
||||||
|
final allAudioOnly = participants.every(
|
||||||
|
(p) =>
|
||||||
|
!(p.hasVideo &&
|
||||||
|
p.remoteParticipant.trackPublications.values.any(
|
||||||
|
(pub) =>
|
||||||
|
pub.track != null &&
|
||||||
|
pub.kind == TrackType.VIDEO &&
|
||||||
|
!pub.muted &&
|
||||||
|
!pub.isDisposed,
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (allAudioOnly) {
|
||||||
|
// Audio-only: show avatars in a compact row
|
||||||
|
return Center(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: Wrap(
|
||||||
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
|
alignment: WrapAlignment.center,
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 8,
|
||||||
|
children: [
|
||||||
|
for (final live in participants)
|
||||||
|
SpeakingRippleAvatar(
|
||||||
|
live: live,
|
||||||
|
size: 72,
|
||||||
|
).padding(horizontal: 4),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show all participants in a responsive grid
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
// Calculate width for responsive 2-column layout
|
||||||
|
final itemWidth = (constraints.maxWidth / 2) - 16;
|
||||||
|
|
||||||
|
return Wrap(
|
||||||
|
alignment: WrapAlignment.center,
|
||||||
|
runAlignment: WrapAlignment.center,
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 8,
|
||||||
|
children: [
|
||||||
|
for (final participant in participants)
|
||||||
|
SizedBox(
|
||||||
|
width: itemWidth,
|
||||||
|
child: CallParticipantTile(live: participant),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,18 @@
|
|||||||
|
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:go_router/go_router.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:island/models/account.dart';
|
||||||
|
import 'package:island/models/chat.dart';
|
||||||
import 'package:island/pods/chat/call.dart';
|
import 'package:island/pods/chat/call.dart';
|
||||||
|
import 'package:island/pods/userinfo.dart';
|
||||||
|
import 'package:island/screens/chat/call.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
|
import 'package:island/widgets/chat/call_button.dart';
|
||||||
|
import 'package:island/widgets/chat/call_content.dart';
|
||||||
import 'package:island/widgets/chat/call_participant_tile.dart';
|
import 'package:island/widgets/chat/call_participant_tile.dart';
|
||||||
import 'package:island/widgets/content/sheet.dart';
|
import 'package:island/widgets/content/sheet.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
@@ -13,7 +20,8 @@ import 'package:styled_widget/styled_widget.dart';
|
|||||||
import 'package:livekit_client/livekit_client.dart';
|
import 'package:livekit_client/livekit_client.dart';
|
||||||
|
|
||||||
class CallControlsBar extends HookConsumerWidget {
|
class CallControlsBar extends HookConsumerWidget {
|
||||||
const CallControlsBar({super.key});
|
final bool isCompact;
|
||||||
|
const CallControlsBar({super.key, this.isCompact = false});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@@ -21,11 +29,14 @@ class CallControlsBar extends HookConsumerWidget {
|
|||||||
final callNotifier = ref.read(callNotifierProvider.notifier);
|
final callNotifier = ref.read(callNotifierProvider.notifier);
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: isCompact ? 12 : 20,
|
||||||
|
vertical: isCompact ? 8 : 16,
|
||||||
|
),
|
||||||
child: Wrap(
|
child: Wrap(
|
||||||
alignment: WrapAlignment.center,
|
alignment: WrapAlignment.center,
|
||||||
runSpacing: 16,
|
runSpacing: isCompact ? 12 : 16,
|
||||||
spacing: 16,
|
spacing: isCompact ? 12 : 16,
|
||||||
children: [
|
children: [
|
||||||
_buildCircularButtonWithDropdown(
|
_buildCircularButtonWithDropdown(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -73,12 +84,15 @@ class CallControlsBar extends HookConsumerWidget {
|
|||||||
(innerContext) => Column(
|
(innerContext) => Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
|
const Gap(24),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Symbols.logout, fill: 1),
|
leading: const Icon(Symbols.logout, fill: 1),
|
||||||
title: Text('callLeave').tr(),
|
title: Text('callLeave').tr(),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
callNotifier.disconnect();
|
callNotifier.disconnect();
|
||||||
Navigator.of(context).pop();
|
if (Navigator.of(context).canPop()) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
Navigator.of(innerContext).pop();
|
Navigator.of(innerContext).pop();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -96,7 +110,9 @@ class CallControlsBar extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
callNotifier.dispose();
|
callNotifier.dispose();
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
Navigator.of(context).pop();
|
if (Navigator.of(context).canPop()) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
Navigator.of(innerContext).pop();
|
Navigator.of(innerContext).pop();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -124,12 +140,14 @@ class CallControlsBar extends HookConsumerWidget {
|
|||||||
required Color backgroundColor,
|
required Color backgroundColor,
|
||||||
Color? iconColor,
|
Color? iconColor,
|
||||||
}) {
|
}) {
|
||||||
|
final size = isCompact ? 40.0 : 56.0;
|
||||||
|
final iconSize = isCompact ? 20.0 : 24.0;
|
||||||
return Container(
|
return Container(
|
||||||
width: 56,
|
width: size,
|
||||||
height: 56,
|
height: size,
|
||||||
decoration: BoxDecoration(color: backgroundColor, shape: BoxShape.circle),
|
decoration: BoxDecoration(color: backgroundColor, shape: BoxShape.circle),
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
icon: Icon(icon, color: iconColor ?? Colors.white, size: 24),
|
icon: Icon(icon, color: iconColor ?? Colors.white, size: iconSize),
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -145,41 +163,51 @@ class CallControlsBar extends HookConsumerWidget {
|
|||||||
Color? iconColor,
|
Color? iconColor,
|
||||||
String? deviceType, // 'videoinput' or 'audioinput'
|
String? deviceType, // 'videoinput' or 'audioinput'
|
||||||
}) {
|
}) {
|
||||||
|
final size = isCompact ? 40.0 : 56.0;
|
||||||
|
final iconSize = isCompact ? 20.0 : 24.0;
|
||||||
return Stack(
|
return Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
width: 56,
|
width: size,
|
||||||
height: 56,
|
height: size,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: backgroundColor,
|
color: backgroundColor,
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
icon: Icon(icon, color: iconColor ?? Colors.white, size: 24),
|
icon: Icon(icon, color: iconColor ?? Colors.white, size: iconSize),
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (hasDropdown && deviceType != null)
|
if (hasDropdown && deviceType != null)
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 4,
|
bottom: 0,
|
||||||
right: 4,
|
right: isCompact ? 0 : -4,
|
||||||
child: GestureDetector(
|
child: Material(
|
||||||
onTap: () => _showDeviceSelectionDialog(context, ref, deviceType),
|
color:
|
||||||
child: Container(
|
Colors
|
||||||
width: 16,
|
.transparent, // Make Material transparent to show underlying color
|
||||||
height: 16,
|
child: InkWell(
|
||||||
decoration: BoxDecoration(
|
onTap:
|
||||||
color: backgroundColor.withOpacity(0.8),
|
() => _showDeviceSelectionDialog(context, ref, deviceType),
|
||||||
shape: BoxShape.circle,
|
borderRadius: BorderRadius.circular((isCompact ? 16 : 24) / 2),
|
||||||
border: Border.all(
|
child: Container(
|
||||||
color: Colors.white.withOpacity(0.3),
|
width: isCompact ? 16 : 24,
|
||||||
width: 0.5,
|
height: isCompact ? 16 : 24,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: backgroundColor.withOpacity(0.8),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(
|
||||||
|
color: Colors.white.withOpacity(0.3),
|
||||||
|
width: 0.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.arrow_drop_down,
|
||||||
|
color: Colors.white,
|
||||||
|
size: isCompact ? 12 : 20,
|
||||||
),
|
),
|
||||||
),
|
|
||||||
child: Icon(
|
|
||||||
Icons.arrow_drop_down,
|
|
||||||
color: Colors.white,
|
|
||||||
size: 12,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -279,34 +307,150 @@ class CallControlsBar extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class CallOverlayBar extends HookConsumerWidget {
|
class CallOverlayBar extends HookConsumerWidget {
|
||||||
const CallOverlayBar({super.key});
|
final SnChatRoom room;
|
||||||
|
const CallOverlayBar({super.key, required this.room});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final callState = ref.watch(callNotifierProvider);
|
final callState = ref.watch(callNotifierProvider);
|
||||||
final callNotifier = ref.read(callNotifierProvider.notifier);
|
final callNotifier = ref.read(callNotifierProvider.notifier);
|
||||||
// Only show if connected and not on the call screen
|
final ongoingCall = ref.watch(ongoingCallProvider(room.id));
|
||||||
if (!callState.isConnected) return const SizedBox.shrink();
|
|
||||||
|
|
||||||
|
// State for overlay mode: compact or preview
|
||||||
|
// Default to true (preview mode) so user sees video immediately after joining
|
||||||
|
final isExpanded = useState(true);
|
||||||
|
|
||||||
|
Widget child;
|
||||||
|
if (callState.isConnected) {
|
||||||
|
child = _buildActiveCallOverlay(
|
||||||
|
context,
|
||||||
|
ref,
|
||||||
|
callState,
|
||||||
|
callNotifier,
|
||||||
|
isExpanded,
|
||||||
|
);
|
||||||
|
} else if (ongoingCall.value != null) {
|
||||||
|
child = _buildJoinPrompt(context, ref);
|
||||||
|
} else {
|
||||||
|
child = const SizedBox.shrink(key: ValueKey('empty'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return AnimatedSize(
|
||||||
|
duration: const Duration(milliseconds: 150),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
child: AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 150),
|
||||||
|
layoutBuilder: (currentChild, previousChildren) {
|
||||||
|
return Stack(
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
children: <Widget>[
|
||||||
|
...previousChildren,
|
||||||
|
if (currentChild != null) currentChild,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildJoinPrompt(BuildContext context, WidgetRef ref) {
|
||||||
|
final isLoading = useState(false);
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
key: const ValueKey('join_prompt'),
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.videocam,
|
||||||
|
color: Theme.of(context).colorScheme.onPrimary,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(12),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text('Call in progress').bold(),
|
||||||
|
Text('Tap to join', style: Theme.of(context).textTheme.bodySmall),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
if (isLoading.value)
|
||||||
|
const SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
).padding(right: 8)
|
||||||
|
else
|
||||||
|
FilledButton.icon(
|
||||||
|
onPressed: () async {
|
||||||
|
isLoading.value = true;
|
||||||
|
try {
|
||||||
|
// Just join the room, don't navigate
|
||||||
|
await ref.read(callNotifierProvider.notifier).joinRoom(room);
|
||||||
|
} catch (e) {
|
||||||
|
showErrorAlert(e);
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.call, size: 18),
|
||||||
|
label: const Text('Join'),
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(all: 12),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getChatRoomName(SnChatRoom? room, SnAccount currentUser) {
|
||||||
|
if (room == null) return 'unnamed'.tr();
|
||||||
|
return room.name ??
|
||||||
|
(room.members ?? [])
|
||||||
|
.where((element) => element.id != currentUser.id)
|
||||||
|
.map((element) => element.account.nick)
|
||||||
|
.first;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildActiveCallOverlay(
|
||||||
|
BuildContext context,
|
||||||
|
WidgetRef ref,
|
||||||
|
CallState callState,
|
||||||
|
CallNotifier callNotifier,
|
||||||
|
ValueNotifier<bool> isExpanded,
|
||||||
|
) {
|
||||||
final lastSpeaker =
|
final lastSpeaker =
|
||||||
callNotifier.participants
|
callNotifier.participants
|
||||||
.where(
|
.where(
|
||||||
(element) => element.remoteParticipant.lastSpokeAt != null,
|
(element) => element.remoteParticipant.lastSpokeAt != null,
|
||||||
)
|
)
|
||||||
.isEmpty
|
.isEmpty
|
||||||
? callNotifier.participants.first
|
? callNotifier.participants.firstOrNull
|
||||||
: callNotifier.participants
|
: callNotifier.participants
|
||||||
.where(
|
.where(
|
||||||
(element) => element.remoteParticipant.lastSpokeAt != null,
|
(element) => element.remoteParticipant.lastSpokeAt != null,
|
||||||
)
|
)
|
||||||
.fold(
|
.fold(
|
||||||
callNotifier.participants.first,
|
callNotifier.participants.firstOrNull,
|
||||||
(value, element) =>
|
(value, element) =>
|
||||||
element.remoteParticipant.lastSpokeAt != null &&
|
element.remoteParticipant.lastSpokeAt != null &&
|
||||||
(value.remoteParticipant.lastSpokeAt == null ||
|
(value?.remoteParticipant.lastSpokeAt == null ||
|
||||||
element.remoteParticipant.lastSpokeAt!
|
element.remoteParticipant.lastSpokeAt!
|
||||||
.compareTo(
|
.compareTo(
|
||||||
value
|
value!
|
||||||
.remoteParticipant
|
.remoteParticipant
|
||||||
.lastSpokeAt!,
|
.lastSpokeAt!,
|
||||||
) >
|
) >
|
||||||
@@ -315,11 +459,76 @@ class CallOverlayBar extends HookConsumerWidget {
|
|||||||
: value,
|
: value,
|
||||||
);
|
);
|
||||||
|
|
||||||
final actionButtonStyle = ButtonStyle(
|
if (lastSpeaker == null) {
|
||||||
minimumSize: const MaterialStatePropertyAll(Size(24, 24)),
|
return const SizedBox.shrink(key: ValueKey('active_waiting'));
|
||||||
);
|
}
|
||||||
|
|
||||||
|
final userInfo = ref.watch(userInfoProvider).value!;
|
||||||
|
|
||||||
|
// Preview Mode (Expanded)
|
||||||
|
if (isExpanded.value) {
|
||||||
|
return Card(
|
||||||
|
key: const ValueKey('active_expanded'),
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
// Header
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Gap(4),
|
||||||
|
Text(_getChatRoomName(callNotifier.chatRoom, userInfo)),
|
||||||
|
const Gap(4),
|
||||||
|
Text(formatDuration(callState.duration)).bold(),
|
||||||
|
const Spacer(),
|
||||||
|
OpenContainer(
|
||||||
|
closedElevation: 0,
|
||||||
|
closedColor: Colors.transparent,
|
||||||
|
openColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
middleColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
openBuilder: (context, action) => CallScreen(room: room),
|
||||||
|
closedBuilder:
|
||||||
|
(context, openContainer) => IconButton(
|
||||||
|
visualDensity: const VisualDensity(
|
||||||
|
horizontal: -4,
|
||||||
|
vertical: -4,
|
||||||
|
),
|
||||||
|
icon: const Icon(Icons.fullscreen),
|
||||||
|
onPressed: openContainer,
|
||||||
|
tooltip: 'Full Screen',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
visualDensity: const VisualDensity(
|
||||||
|
horizontal: -4,
|
||||||
|
vertical: -4,
|
||||||
|
),
|
||||||
|
icon: const Icon(Icons.expand_less),
|
||||||
|
onPressed: () => isExpanded.value = false,
|
||||||
|
tooltip: 'Collapse',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 12, vertical: 8),
|
||||||
|
// Video Preview
|
||||||
|
Container(
|
||||||
|
height: 200,
|
||||||
|
width: double.infinity,
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||||
|
child: const CallContent(),
|
||||||
|
),
|
||||||
|
const CallControlsBar(
|
||||||
|
isCompact: true,
|
||||||
|
).padding(vertical: 8, horizontal: 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compact Mode
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
|
key: const ValueKey('active_collapsed'),
|
||||||
|
onTap: () => isExpanded.value = true,
|
||||||
child: Card(
|
child: Card(
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -328,30 +537,32 @@ class CallOverlayBar extends HookConsumerWidget {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Builder(
|
SizedBox(
|
||||||
builder: (context) {
|
width: 40,
|
||||||
if (callNotifier.localParticipant == null) {
|
height: 40,
|
||||||
return CircularProgressIndicator().center();
|
child:
|
||||||
}
|
SpeakingRippleAvatar(
|
||||||
return SizedBox(
|
live: lastSpeaker,
|
||||||
width: 40,
|
size: 36,
|
||||||
height: 40,
|
).center(),
|
||||||
child:
|
|
||||||
SpeakingRippleAvatar(
|
|
||||||
live: lastSpeaker,
|
|
||||||
size: 36,
|
|
||||||
).center(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('@${lastSpeaker.participant.identity}').bold(),
|
Text('@${lastSpeaker.participant.identity}').bold(),
|
||||||
Text(
|
Row(
|
||||||
formatDuration(callState.duration),
|
spacing: 4,
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
children: [
|
||||||
|
Text(
|
||||||
|
_getChatRoomName(callNotifier.chatRoom, userInfo),
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
formatDuration(callState.duration),
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -361,41 +572,20 @@ class CallOverlayBar extends HookConsumerWidget {
|
|||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
callState.isMicrophoneEnabled ? Icons.mic : Icons.mic_off,
|
callState.isMicrophoneEnabled ? Icons.mic : Icons.mic_off,
|
||||||
|
size: 20,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
callNotifier.toggleMicrophone();
|
callNotifier.toggleMicrophone();
|
||||||
},
|
},
|
||||||
style: actionButtonStyle,
|
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(
|
icon: const Icon(Icons.expand_more),
|
||||||
callState.isCameraEnabled ? Icons.videocam : Icons.videocam_off,
|
onPressed: () => isExpanded.value = true,
|
||||||
),
|
tooltip: 'Expand',
|
||||||
onPressed: () {
|
|
||||||
callNotifier.toggleCamera();
|
|
||||||
},
|
|
||||||
style: actionButtonStyle,
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(
|
|
||||||
callState.isScreenSharing
|
|
||||||
? Icons.stop_screen_share
|
|
||||||
: Icons.screen_share,
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
callNotifier.toggleScreenShare(context);
|
|
||||||
},
|
|
||||||
style: actionButtonStyle,
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
).padding(all: 16),
|
).padding(all: 12),
|
||||||
),
|
),
|
||||||
onTap: () {
|
|
||||||
context.pushNamed(
|
|
||||||
'chatCall',
|
|
||||||
pathParameters: {'id': callNotifier.roomId!},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,59 @@ import 'package:livekit_client/livekit_client.dart';
|
|||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
|
||||||
|
class SpeakingRipple extends StatelessWidget {
|
||||||
|
final double size;
|
||||||
|
final double audioLevel;
|
||||||
|
final bool isSpeaking;
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const SpeakingRipple({
|
||||||
|
super.key,
|
||||||
|
required this.size,
|
||||||
|
required this.audioLevel,
|
||||||
|
required this.isSpeaking,
|
||||||
|
required this.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final avatarRadius = size / 2;
|
||||||
|
final clampedLevel = audioLevel.clamp(0.0, 1.0);
|
||||||
|
final rippleRadius = avatarRadius + clampedLevel * (size * 0.333);
|
||||||
|
|
||||||
|
return SizedBox(
|
||||||
|
width: size + 8,
|
||||||
|
height: size + 8,
|
||||||
|
child: TweenAnimationBuilder<double>(
|
||||||
|
tween: Tween<double>(
|
||||||
|
begin: avatarRadius,
|
||||||
|
end: isSpeaking ? rippleRadius : avatarRadius,
|
||||||
|
),
|
||||||
|
duration: const Duration(milliseconds: 250),
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
builder: (context, animatedRadius, child) {
|
||||||
|
return Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
if (isSpeaking)
|
||||||
|
Container(
|
||||||
|
width: animatedRadius * 2,
|
||||||
|
height: animatedRadius * 2,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: Colors.green.withOpacity(0.75 + 0.25 * clampedLevel),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child!,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: SizedBox(width: size, height: size, child: child),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class SpeakingRippleAvatar extends HookConsumerWidget {
|
class SpeakingRippleAvatar extends HookConsumerWidget {
|
||||||
final CallParticipantLive live;
|
final CallParticipantLive live;
|
||||||
final double size;
|
final double size;
|
||||||
@@ -18,79 +71,58 @@ class SpeakingRippleAvatar extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final account = ref.watch(accountProvider(live.participant.identity));
|
final account = ref.watch(accountProvider(live.participant.identity));
|
||||||
|
|
||||||
final avatarRadius = size / 2;
|
return SpeakingRipple(
|
||||||
final clampedLevel = live.remoteParticipant.audioLevel.clamp(0.0, 1.0);
|
size: size,
|
||||||
final rippleRadius = avatarRadius + clampedLevel * (size * 0.333);
|
audioLevel: live.remoteParticipant.audioLevel,
|
||||||
return SizedBox(
|
isSpeaking: live.remoteParticipant.isSpeaking,
|
||||||
width: size + 8,
|
child: Stack(
|
||||||
height: size + 8,
|
children: [
|
||||||
child: TweenAnimationBuilder<double>(
|
Container(
|
||||||
tween: Tween<double>(
|
width: size,
|
||||||
begin: avatarRadius,
|
height: size,
|
||||||
end: live.remoteParticipant.isSpeaking ? rippleRadius : avatarRadius,
|
|
||||||
),
|
|
||||||
duration: const Duration(milliseconds: 250),
|
|
||||||
curve: Curves.easeOut,
|
|
||||||
builder: (context, animatedRadius, child) {
|
|
||||||
return Stack(
|
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
children: [
|
decoration: const BoxDecoration(shape: BoxShape.circle),
|
||||||
if (live.remoteParticipant.isSpeaking)
|
child: account.when(
|
||||||
Container(
|
data:
|
||||||
width: animatedRadius * 2,
|
(value) => CallParticipantGestureDetector(
|
||||||
height: animatedRadius * 2,
|
participant: live,
|
||||||
decoration: BoxDecoration(
|
child: ProfilePictureWidget(
|
||||||
shape: BoxShape.circle,
|
file: value.profile.picture,
|
||||||
color: Colors.green.withOpacity(0.75 + 0.25 * clampedLevel),
|
radius: size / 2,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
error:
|
||||||
|
(_, _) => CircleAvatar(
|
||||||
|
radius: size / 2,
|
||||||
|
child: const Icon(Symbols.person_remove),
|
||||||
|
),
|
||||||
|
loading:
|
||||||
|
() => CircleAvatar(
|
||||||
|
radius: size / 2,
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (live.remoteParticipant.isMuted)
|
||||||
|
Positioned(
|
||||||
|
bottom: 0,
|
||||||
|
right: 0,
|
||||||
|
child: Container(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.red,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: Colors.white, width: 2),
|
||||||
),
|
),
|
||||||
Container(
|
child: const Icon(
|
||||||
width: size,
|
Symbols.mic_off,
|
||||||
height: size,
|
size: 14,
|
||||||
alignment: Alignment.center,
|
color: Colors.white,
|
||||||
decoration: BoxDecoration(shape: BoxShape.circle),
|
|
||||||
child: account.when(
|
|
||||||
data:
|
|
||||||
(value) => CallParticipantGestureDetector(
|
|
||||||
participant: live,
|
|
||||||
child: ProfilePictureWidget(
|
|
||||||
file: value.profile.picture,
|
|
||||||
radius: size / 2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
error:
|
|
||||||
(_, _) => CircleAvatar(
|
|
||||||
radius: size / 2,
|
|
||||||
child: const Icon(Symbols.person_remove),
|
|
||||||
),
|
|
||||||
loading:
|
|
||||||
() => CircleAvatar(
|
|
||||||
radius: size / 2,
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (live.remoteParticipant.isMuted)
|
),
|
||||||
Positioned(
|
],
|
||||||
bottom: 4,
|
|
||||||
right: 4,
|
|
||||||
child: Container(
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.red,
|
|
||||||
borderRadius: BorderRadius.all(Radius.circular(10)),
|
|
||||||
),
|
|
||||||
child: const Icon(
|
|
||||||
Symbols.mic_off,
|
|
||||||
size: 14,
|
|
||||||
fill: 1,
|
|
||||||
).padding(left: 1.5, top: 1.5),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -103,6 +135,8 @@ class CallParticipantTile extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final userInfo = ref.watch(accountProvider(live.participant.name));
|
||||||
|
|
||||||
final hasVideo =
|
final hasVideo =
|
||||||
live.hasVideo &&
|
live.hasVideo &&
|
||||||
live.remoteParticipant.trackPublications.values
|
live.remoteParticipant.trackPublications.values
|
||||||
@@ -110,42 +144,92 @@ class CallParticipantTile extends HookConsumerWidget {
|
|||||||
.isNotEmpty;
|
.isNotEmpty;
|
||||||
|
|
||||||
if (hasVideo) {
|
if (hasVideo) {
|
||||||
return Stack(
|
return Padding(
|
||||||
fit: StackFit.loose,
|
padding: const EdgeInsets.all(8),
|
||||||
children: [
|
child: LayoutBuilder(
|
||||||
AspectRatio(
|
builder: (context, constraints) {
|
||||||
aspectRatio: 16 / 9,
|
// Use the smaller dimension to determine the "size" for the ripple calculation
|
||||||
child: VideoTrackRenderer(
|
// effectively making the ripple relative to the tile size.
|
||||||
live.remoteParticipant.trackPublications.values
|
// However, for a rectangular video, we might want a different approach.
|
||||||
.where((track) => track.kind == TrackType.VIDEO)
|
// The user asked for "speaking ripple to the video as well".
|
||||||
.first
|
// If we use the extracted SpeakingRipple, it expects a size and assumes a circle.
|
||||||
.track
|
// We need to adapt it or create a rectangular version.
|
||||||
as VideoTrack,
|
// Given the "image" likely shows a rectangular video with rounded corners,
|
||||||
renderMode: VideoRenderMode.platformView,
|
// let's create a specific wrapper for the video tile that adds a border/glow when speaking.
|
||||||
),
|
|
||||||
),
|
final isSpeaking = live.remoteParticipant.isSpeaking;
|
||||||
Positioned(
|
final audioLevel = live.remoteParticipant.audioLevel;
|
||||||
left: 8,
|
|
||||||
right: 8,
|
return AnimatedContainer(
|
||||||
bottom: 8,
|
duration: const Duration(milliseconds: 200),
|
||||||
child: Text(
|
decoration: BoxDecoration(
|
||||||
'@${live.participant.name}',
|
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||||
textAlign: TextAlign.center,
|
borderRadius: BorderRadius.circular(16),
|
||||||
style: const TextStyle(
|
border: Border.all(
|
||||||
fontSize: 14,
|
color:
|
||||||
color: Colors.white,
|
isSpeaking
|
||||||
shadows: [
|
? Colors.green.withOpacity(
|
||||||
BoxShadow(
|
0.5 + 0.5 * audioLevel.clamp(0.0, 1.0),
|
||||||
color: Colors.black54,
|
)
|
||||||
offset: Offset(1, 1),
|
: Theme.of(context).colorScheme.outlineVariant,
|
||||||
spreadRadius: 8,
|
width: isSpeaking ? 4 : 1,
|
||||||
blurRadius: 8,
|
),
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
child: ClipRRect(
|
||||||
),
|
borderRadius: BorderRadius.circular(12),
|
||||||
],
|
child: AspectRatio(
|
||||||
|
aspectRatio: 16 / 9,
|
||||||
|
child: Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
VideoTrackRenderer(
|
||||||
|
live.remoteParticipant.trackPublications.values
|
||||||
|
.where((track) => track.kind == TrackType.VIDEO)
|
||||||
|
.first
|
||||||
|
.track
|
||||||
|
as VideoTrack,
|
||||||
|
renderMode: VideoRenderMode.platformView,
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
left: 8,
|
||||||
|
bottom: 8,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black.withOpacity(0.6),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (live.remoteParticipant.isMuted)
|
||||||
|
const Icon(
|
||||||
|
Symbols.mic_off,
|
||||||
|
size: 14,
|
||||||
|
color: Colors.redAccent,
|
||||||
|
).padding(right: 4),
|
||||||
|
Text(
|
||||||
|
userInfo.value?.nick ?? live.participant.name,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return SpeakingRippleAvatar(size: 84, live: live);
|
return SpeakingRippleAvatar(size: 84, live: live);
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ void _insertPlaceholder(TextEditingController controller, String placeholder) {
|
|||||||
|
|
||||||
const kInputDrawerExpandedHeight = 180.0;
|
const kInputDrawerExpandedHeight = 180.0;
|
||||||
|
|
||||||
|
const kExpandedSectionTabHeight = 32.0;
|
||||||
|
|
||||||
class _ExpandedSection extends StatelessWidget {
|
class _ExpandedSection extends StatelessWidget {
|
||||||
final TextEditingController messageController;
|
final TextEditingController messageController;
|
||||||
final SnPoll? selectedPoll;
|
final SnPoll? selectedPoll;
|
||||||
@@ -75,9 +77,23 @@ class _ExpandedSection extends StatelessWidget {
|
|||||||
length: 2,
|
length: 2,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
TabBar(
|
PreferredSize(
|
||||||
splashBorderRadius: const BorderRadius.all(Radius.circular(40)),
|
preferredSize: const Size.fromHeight(kExpandedSectionTabHeight),
|
||||||
tabs: [Tab(text: 'Features'), Tab(text: 'Stickers')],
|
child: TabBar(
|
||||||
|
splashBorderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(40),
|
||||||
|
),
|
||||||
|
tabs: [
|
||||||
|
Tab(
|
||||||
|
text: 'features'.tr(),
|
||||||
|
height: kExpandedSectionTabHeight,
|
||||||
|
),
|
||||||
|
Tab(
|
||||||
|
text: 'stickers'.tr(),
|
||||||
|
height: kExpandedSectionTabHeight,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: kInputDrawerExpandedHeight,
|
height: kInputDrawerExpandedHeight,
|
||||||
@@ -248,6 +264,7 @@ class ChatInput extends HookConsumerWidget {
|
|||||||
|
|
||||||
void send() {
|
void send() {
|
||||||
inputFocusNode.requestFocus();
|
inputFocusNode.requestFocus();
|
||||||
|
if (isExpanded.value) isExpanded.value = false;
|
||||||
onSend.call();
|
onSend.call();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -596,6 +596,7 @@ class MessageHoverActionMenu extends StatelessWidget {
|
|||||||
final confirmed = await showConfirmAlert(
|
final confirmed = await showConfirmAlert(
|
||||||
'deleteMessageConfirmation'.tr(),
|
'deleteMessageConfirmation'.tr(),
|
||||||
'deleteMessage'.tr(),
|
'deleteMessage'.tr(),
|
||||||
|
isDanger: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
|
|||||||
@@ -77,27 +77,12 @@ class MessageSenderInfo extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
AccountName(
|
||||||
children: [
|
account: sender.account,
|
||||||
AccountName(
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
account: sender.account,
|
color: textColor,
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
fontWeight: FontWeight.w500,
|
||||||
color: textColor,
|
),
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Badge(
|
|
||||||
label:
|
|
||||||
Text(
|
|
||||||
sender.role >= 100
|
|
||||||
? 'permissionOwner'
|
|
||||||
: sender.role >= 50
|
|
||||||
? 'permissionModerator'
|
|
||||||
: 'permissionMember',
|
|
||||||
).tr(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
timestamp,
|
timestamp,
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
import 'package:dio/dio.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flutter_platform_alert/flutter_platform_alert.dart';
|
|
||||||
import 'package:island/talker.dart';
|
|
||||||
|
|
||||||
String _parseRemoteError(DioException err) {
|
|
||||||
String? message;
|
|
||||||
if (err.response?.data is String) {
|
|
||||||
message = err.response?.data;
|
|
||||||
} else if (err.response?.data?['message'] != null) {
|
|
||||||
message = <String?>[
|
|
||||||
err.response?.data?['message']?.toString(),
|
|
||||||
err.response?.data?['detail']?.toString(),
|
|
||||||
].where((e) => e != null).cast<String>().map((e) => e.trim()).join('\n');
|
|
||||||
} else if (err.response?.data?['errors'] != null) {
|
|
||||||
final errors = err.response?.data['errors'] as Map<String, dynamic>;
|
|
||||||
message = errors.values
|
|
||||||
.map(
|
|
||||||
(ele) =>
|
|
||||||
(ele as List<dynamic>).map((ele) => ele.toString()).join('\n'),
|
|
||||||
)
|
|
||||||
.join('\n');
|
|
||||||
}
|
|
||||||
if (message == null || message.isEmpty) message = err.response?.statusMessage;
|
|
||||||
message ??= err.message;
|
|
||||||
return message ?? err.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
void showErrorAlert(dynamic err) async {
|
|
||||||
if (err is Error) {
|
|
||||||
talker.error('Something went wrong...', err, err.stackTrace);
|
|
||||||
}
|
|
||||||
final text = switch (err) {
|
|
||||||
String _ => err,
|
|
||||||
DioException _ => _parseRemoteError(err),
|
|
||||||
Exception _ => err.toString(),
|
|
||||||
_ => err.toString(),
|
|
||||||
};
|
|
||||||
FlutterPlatformAlert.showAlert(
|
|
||||||
windowTitle: 'somethingWentWrong'.tr(),
|
|
||||||
text: text,
|
|
||||||
alertStyle: AlertButtonStyle.ok,
|
|
||||||
iconStyle: IconStyle.error,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void showInfoAlert(String message, String title) async {
|
|
||||||
FlutterPlatformAlert.showAlert(
|
|
||||||
windowTitle: title,
|
|
||||||
text: message,
|
|
||||||
alertStyle: AlertButtonStyle.ok,
|
|
||||||
iconStyle: IconStyle.information,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> showConfirmAlert(String message, String title) async {
|
|
||||||
final result = await FlutterPlatformAlert.showAlert(
|
|
||||||
windowTitle: title,
|
|
||||||
text: message,
|
|
||||||
alertStyle: AlertButtonStyle.okCancel,
|
|
||||||
iconStyle: IconStyle.question,
|
|
||||||
);
|
|
||||||
return result == AlertButton.okButton;
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
// ignore_for_file: avoid_web_libraries_in_flutter
|
|
||||||
|
|
||||||
import 'dart:js' as js;
|
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
|
|
||||||
String _parseRemoteError(DioException err) {
|
|
||||||
String? message;
|
|
||||||
if (err.response?.data is String) {
|
|
||||||
message = err.response?.data;
|
|
||||||
} else if (err.response?.data?['message'] != null) {
|
|
||||||
message = <String?>[
|
|
||||||
err.response?.data?['message']?.toString(),
|
|
||||||
err.response?.data?['detail']?.toString(),
|
|
||||||
].where((e) => e != null).cast<String>().map((e) => e.trim()).join('\n');
|
|
||||||
} else if (err.response?.data?['errors'] != null) {
|
|
||||||
final errors = err.response?.data['errors'] as Map<String, dynamic>;
|
|
||||||
message = errors.values
|
|
||||||
.map(
|
|
||||||
(ele) =>
|
|
||||||
(ele as List<dynamic>).map((ele) => ele.toString()).join('\n'),
|
|
||||||
)
|
|
||||||
.join('\n');
|
|
||||||
}
|
|
||||||
if (message == null || message.isEmpty) message = err.response?.statusMessage;
|
|
||||||
message ??= err.message;
|
|
||||||
return message ?? err.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
void showErrorAlert(dynamic err) async {
|
|
||||||
final text = switch (err) {
|
|
||||||
String _ => err,
|
|
||||||
DioException _ => _parseRemoteError(err),
|
|
||||||
Exception _ => err.toString(),
|
|
||||||
_ => err.toString(),
|
|
||||||
};
|
|
||||||
js.context.callMethod('swal', ['somethingWentWrong'.tr(), text, 'error']);
|
|
||||||
}
|
|
||||||
|
|
||||||
void showInfoAlert(String message, String title) async {
|
|
||||||
js.context.callMethod('swal', [title, message, 'info']);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> showConfirmAlert(String message, String title) async {
|
|
||||||
final result = await js.context.callMethod('swal', [
|
|
||||||
title,
|
|
||||||
message,
|
|
||||||
'question',
|
|
||||||
{'buttons': true},
|
|
||||||
]);
|
|
||||||
return result == true;
|
|
||||||
}
|
|
||||||
@@ -215,6 +215,8 @@ class CloudFileList extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
if (files.length == 1) {
|
if (files.length == 1) {
|
||||||
final isImage = files.first.mimeType?.startsWith('image') ?? false;
|
final isImage = files.first.mimeType?.startsWith('image') ?? false;
|
||||||
|
final isAudio = files.first.mimeType?.startsWith('audio') ?? false;
|
||||||
|
final ratio = files.first.fileMeta?['ratio'] as num?;
|
||||||
final widgetItem = ClipRRect(
|
final widgetItem = ClipRRect(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
child: _CloudFileListEntry(
|
child: _CloudFileListEntry(
|
||||||
@@ -242,7 +244,15 @@ class CloudFileList extends HookConsumerWidget {
|
|||||||
minWidth: minWidth ?? 0,
|
minWidth: minWidth ?? 0,
|
||||||
maxWidth: files.length == 1 ? maxWidth : double.infinity,
|
maxWidth: files.length == 1 ? maxWidth : double.infinity,
|
||||||
),
|
),
|
||||||
child: IntrinsicWidth(child: IntrinsicHeight(child: widgetItem)),
|
child:
|
||||||
|
(ratio == null && isImage)
|
||||||
|
? IntrinsicWidth(child: IntrinsicHeight(child: widgetItem))
|
||||||
|
: (ratio == null && isAudio)
|
||||||
|
? IntrinsicHeight(child: widgetItem)
|
||||||
|
: AspectRatio(
|
||||||
|
aspectRatio: ratio?.toDouble() ?? 1,
|
||||||
|
child: widgetItem,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -408,8 +418,6 @@ class _CloudFileListEntry extends HookConsumerWidget {
|
|||||||
final lockedByMature = file.sensitiveMarks.isNotEmpty && !showMature.value;
|
final lockedByMature = file.sensitiveMarks.isNotEmpty && !showMature.value;
|
||||||
final meta = file.fileMeta is Map ? file.fileMeta as Map : const {};
|
final meta = file.fileMeta is Map ? file.fileMeta as Map : const {};
|
||||||
|
|
||||||
final ratio = meta['ratio'] as num?;
|
|
||||||
|
|
||||||
final fit = BoxFit.cover;
|
final fit = BoxFit.cover;
|
||||||
|
|
||||||
Widget bg = const SizedBox.shrink();
|
Widget bg = const SizedBox.shrink();
|
||||||
@@ -448,9 +456,7 @@ class _CloudFileListEntry extends HookConsumerWidget {
|
|||||||
fit: fit,
|
fit: fit,
|
||||||
useInternalGate: false,
|
useInternalGate: false,
|
||||||
))
|
))
|
||||||
: IntrinsicWidth(
|
: const SizedBox.shrink();
|
||||||
child: IntrinsicHeight(child: const SizedBox.shrink()),
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget overlays;
|
Widget overlays;
|
||||||
if (lockedByDS) {
|
if (lockedByDS) {
|
||||||
@@ -481,7 +487,7 @@ class _CloudFileListEntry extends HookConsumerWidget {
|
|||||||
onTap?.call();
|
onTap?.call();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: AspectRatio(aspectRatio: ratio?.toDouble() ?? 1, child: content),
|
child: content,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:gal/gal.dart';
|
import 'package:gal/gal.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/file.dart';
|
import 'package:island/models/file.dart';
|
||||||
import 'package:island/pods/config.dart';
|
import 'package:island/pods/config.dart';
|
||||||
@@ -171,6 +172,24 @@ class CloudFileLightbox extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
onPressed: showInfoSheet,
|
onPressed: showInfoSheet,
|
||||||
),
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
final router = GoRouter.of(context);
|
||||||
|
Navigator.of(context).pop(context);
|
||||||
|
Future(() {
|
||||||
|
router.pushNamed(
|
||||||
|
'fileDetail',
|
||||||
|
pathParameters: {'id': item.id},
|
||||||
|
extra: item,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: Icon(
|
||||||
|
Icons.more_horiz,
|
||||||
|
color: Colors.white,
|
||||||
|
shadows: shadow,
|
||||||
|
),
|
||||||
|
),
|
||||||
Spacer(),
|
Spacer(),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
|
|||||||
@@ -1,24 +1,17 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:file_saver/file_saver.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/file.dart';
|
import 'package:island/models/file.dart';
|
||||||
import 'package:island/pods/config.dart';
|
import 'package:island/pods/config.dart';
|
||||||
import 'package:island/pods/network.dart';
|
|
||||||
import 'package:island/services/time.dart';
|
import 'package:island/services/time.dart';
|
||||||
import 'package:island/utils/format.dart';
|
import 'package:island/utils/format.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:path/path.dart' show extension;
|
|
||||||
import 'package:path_provider/path_provider.dart';
|
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:island/widgets/data_saving_gate.dart';
|
import 'package:island/widgets/data_saving_gate.dart';
|
||||||
import 'package:island/widgets/content/file_info_sheet.dart';
|
|
||||||
|
|
||||||
import 'file_viewer_contents.dart';
|
import 'file_viewer_contents.dart';
|
||||||
import 'image.dart';
|
import 'image.dart';
|
||||||
@@ -66,34 +59,6 @@ class CloudFileWidget extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (item.mimeType == 'application/pdf') {
|
if (item.mimeType == 'application/pdf') {
|
||||||
Future<void> downloadFile() async {
|
|
||||||
try {
|
|
||||||
showSnackBar('Downloading file...');
|
|
||||||
|
|
||||||
final client = ref.read(apiClientProvider);
|
|
||||||
final tempDir = await getTemporaryDirectory();
|
|
||||||
var extName = extension(item.name).trim();
|
|
||||||
if (extName.isEmpty) {
|
|
||||||
extName = item.mimeType?.split('/').lastOrNull ?? 'pdf';
|
|
||||||
}
|
|
||||||
final filePath = '${tempDir.path}/${item.id}.$extName';
|
|
||||||
|
|
||||||
await client.download(
|
|
||||||
'/drive/files/${item.id}',
|
|
||||||
filePath,
|
|
||||||
queryParameters: {'original': true},
|
|
||||||
);
|
|
||||||
|
|
||||||
await FileSaver.instance.saveFile(
|
|
||||||
name: item.name.isEmpty ? '${item.id}.$extName' : item.name,
|
|
||||||
file: File(filePath),
|
|
||||||
);
|
|
||||||
showSnackBar('File saved to downloads');
|
|
||||||
} catch (e) {
|
|
||||||
showErrorAlert(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
height: 400,
|
height: 400,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -166,30 +131,20 @@ class CloudFileWidget extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Symbols.download,
|
Symbols.more_horiz,
|
||||||
color: Colors.white,
|
|
||||||
size: 16,
|
|
||||||
),
|
|
||||||
onPressed: downloadFile,
|
|
||||||
padding: EdgeInsets.all(4),
|
|
||||||
constraints: const BoxConstraints(),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(
|
|
||||||
Symbols.info,
|
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
size: 16,
|
size: 16,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
showModalBottomSheet(
|
context.pushNamed(
|
||||||
useRootNavigator: true,
|
'fileDetail',
|
||||||
context: context,
|
pathParameters: {'id': item.id},
|
||||||
isScrollControlled: true,
|
extra: item,
|
||||||
builder: (context) => FileInfoSheet(item: item),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
padding: EdgeInsets.all(4),
|
padding: EdgeInsets.all(4),
|
||||||
constraints: const BoxConstraints(),
|
constraints: const BoxConstraints(),
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -201,34 +156,6 @@ class CloudFileWidget extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (item.mimeType?.startsWith('text/') == true) {
|
if (item.mimeType?.startsWith('text/') == true) {
|
||||||
Future<void> downloadFile() async {
|
|
||||||
try {
|
|
||||||
showSnackBar('Downloading file...');
|
|
||||||
|
|
||||||
final client = ref.read(apiClientProvider);
|
|
||||||
final tempDir = await getTemporaryDirectory();
|
|
||||||
var extName = extension(item.name).trim();
|
|
||||||
if (extName.isEmpty) {
|
|
||||||
extName = item.mimeType?.split('/').lastOrNull ?? 'txt';
|
|
||||||
}
|
|
||||||
final filePath = '${tempDir.path}/${item.id}.$extName';
|
|
||||||
|
|
||||||
await client.download(
|
|
||||||
'/drive/files/${item.id}',
|
|
||||||
filePath,
|
|
||||||
queryParameters: {'original': true},
|
|
||||||
);
|
|
||||||
|
|
||||||
await FileSaver.instance.saveFile(
|
|
||||||
name: item.name.isEmpty ? '${item.id}.$extName' : item.name,
|
|
||||||
file: File(filePath),
|
|
||||||
);
|
|
||||||
showSnackBar('File saved to downloads');
|
|
||||||
} catch (e) {
|
|
||||||
showErrorAlert(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
height: 400,
|
height: 400,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -304,30 +231,20 @@ class CloudFileWidget extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Symbols.download,
|
Symbols.more_horiz,
|
||||||
color: Colors.white,
|
|
||||||
size: 16,
|
|
||||||
),
|
|
||||||
onPressed: downloadFile,
|
|
||||||
padding: EdgeInsets.all(4),
|
|
||||||
constraints: const BoxConstraints(),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(
|
|
||||||
Symbols.info,
|
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
size: 16,
|
size: 16,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
showModalBottomSheet(
|
context.pushNamed(
|
||||||
useRootNavigator: true,
|
'fileDetail',
|
||||||
context: context,
|
pathParameters: {'id': item.id},
|
||||||
isScrollControlled: true,
|
extra: item,
|
||||||
builder: (context) => FileInfoSheet(item: item),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
padding: EdgeInsets.all(4),
|
padding: EdgeInsets.all(4),
|
||||||
constraints: const BoxConstraints(),
|
constraints: const BoxConstraints(),
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -356,41 +273,13 @@ class CloudFileWidget extends HookConsumerWidget {
|
|||||||
'audio' => AudioFileContent(item: item, uri: uri),
|
'audio' => AudioFileContent(item: item, uri: uri),
|
||||||
_ => Builder(
|
_ => Builder(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
Future<void> downloadFile() async {
|
|
||||||
try {
|
|
||||||
showSnackBar('Downloading file...');
|
|
||||||
|
|
||||||
final client = ref.read(apiClientProvider);
|
|
||||||
final tempDir = await getTemporaryDirectory();
|
|
||||||
var extName = extension(item.name).trim();
|
|
||||||
if (extName.isEmpty) {
|
|
||||||
extName = item.mimeType?.split('/').lastOrNull ?? 'bin';
|
|
||||||
}
|
|
||||||
final filePath = '${tempDir.path}/${item.id}.$extName';
|
|
||||||
|
|
||||||
await client.download(
|
|
||||||
'/drive/files/${item.id}',
|
|
||||||
filePath,
|
|
||||||
queryParameters: {'original': true},
|
|
||||||
);
|
|
||||||
|
|
||||||
await FileSaver.instance.saveFile(
|
|
||||||
name: item.name.isEmpty ? '${item.id}.$extName' : item.name,
|
|
||||||
file: File(filePath),
|
|
||||||
);
|
|
||||||
showSnackBar('File saved to downloads');
|
|
||||||
} catch (e) {
|
|
||||||
showErrorAlert(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: Theme.of(context).colorScheme.outline,
|
color: Theme.of(context).colorScheme.outline,
|
||||||
width: 1,
|
width: 1,
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(16),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -422,19 +311,12 @@ class CloudFileWidget extends HookConsumerWidget {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
TextButton.icon(
|
|
||||||
onPressed: downloadFile,
|
|
||||||
icon: const Icon(Symbols.download),
|
|
||||||
label: Text('download').tr(),
|
|
||||||
),
|
|
||||||
const Gap(8),
|
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
showModalBottomSheet(
|
context.pushNamed(
|
||||||
useRootNavigator: true,
|
'fileDetail',
|
||||||
context: context,
|
pathParameters: {'id': item.id},
|
||||||
isScrollControlled: true,
|
extra: item,
|
||||||
builder: (context) => FileInfoSheet(item: item),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: const Icon(Symbols.info),
|
icon: const Icon(Symbols.info),
|
||||||
|
|||||||
@@ -55,14 +55,18 @@ class _EmbedLinkWidgetState extends State<EmbedLinkWidget> {
|
|||||||
stream.removeListener(listener);
|
stream.removeListener(listener);
|
||||||
|
|
||||||
final aspectRatio = info.image.width / info.image.height;
|
final aspectRatio = info.image.width / info.image.height;
|
||||||
setState(() {
|
if (mounted) {
|
||||||
_isSquare = aspectRatio >= 0.9 && aspectRatio <= 1.1;
|
setState(() {
|
||||||
});
|
_isSquare = aspectRatio >= 0.9 && aspectRatio <= 1.1;
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// If error, assume not square
|
// If error, assume not square
|
||||||
setState(() {
|
if (mounted) {
|
||||||
_isSquare = false;
|
setState(() {
|
||||||
});
|
_isSquare = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:file_saver/file_saver.dart';
|
import 'package:file_saver/file_saver.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
@@ -28,8 +30,64 @@ class PdfFileContent extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final pdfViewer = useMemoized(() => SfPdfViewer.network(uri), [uri]);
|
final fileFuture = useMemoized(
|
||||||
return pdfViewer;
|
() => DefaultCacheManager().getSingleFile(uri),
|
||||||
|
[uri],
|
||||||
|
);
|
||||||
|
|
||||||
|
final pdfController = useMemoized(() => PdfViewerController(), []);
|
||||||
|
|
||||||
|
final shadow = [
|
||||||
|
Shadow(color: Colors.black54, blurRadius: 5.0, offset: Offset(1.0, 1.0)),
|
||||||
|
];
|
||||||
|
|
||||||
|
return FutureBuilder<File>(
|
||||||
|
future: fileFuture,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
} else if (snapshot.hasError) {
|
||||||
|
return Center(child: Text('Error loading PDF: ${snapshot.error}'));
|
||||||
|
} else if (snapshot.hasData) {
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
SfPdfViewer.file(snapshot.data!, controller: pdfController),
|
||||||
|
// Controls overlay
|
||||||
|
Positioned(
|
||||||
|
bottom: MediaQuery.of(context).padding.bottom + 16,
|
||||||
|
left: 16,
|
||||||
|
right: 16,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
Icons.remove,
|
||||||
|
color: Colors.white,
|
||||||
|
shadows: shadow,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
pdfController.zoomLevel = pdfController.zoomLevel * 0.9;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
Icons.add,
|
||||||
|
color: Colors.white,
|
||||||
|
shadows: shadow,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
pdfController.zoomLevel = pdfController.zoomLevel * 1.1;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return const Center(child: Text('No PDF data'));
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,19 +147,39 @@ class ImageFileContent extends HookConsumerWidget {
|
|||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: PhotoView(
|
child: Listener(
|
||||||
backgroundDecoration: BoxDecoration(
|
onPointerSignal: (pointerSignal) {
|
||||||
color: Colors.black.withOpacity(0.9),
|
try {
|
||||||
|
// Handle mouse wheel zoom - cast to dynamic to access scrollDelta
|
||||||
|
final delta =
|
||||||
|
(pointerSignal as dynamic).scrollDelta.dy as double?;
|
||||||
|
if (delta != null && delta != 0) {
|
||||||
|
final currentScale = photoViewController.scale ?? 1.0;
|
||||||
|
// Adjust scale based on scroll direction (invert for natural zoom)
|
||||||
|
final newScale =
|
||||||
|
delta > 0 ? currentScale * 0.9 : currentScale * 1.1;
|
||||||
|
// Clamp scale to reasonable bounds
|
||||||
|
final clampedScale = newScale.clamp(0.1, 10.0);
|
||||||
|
photoViewController.scale = clampedScale;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore non-scroll events
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: PhotoView(
|
||||||
|
backgroundDecoration: BoxDecoration(
|
||||||
|
color: Colors.black.withOpacity(0.9),
|
||||||
|
),
|
||||||
|
controller: photoViewController,
|
||||||
|
imageProvider: CloudImageWidget.provider(
|
||||||
|
fileId: item.id,
|
||||||
|
serverUrl: ref.watch(serverUrlProvider),
|
||||||
|
original: showOriginal.value,
|
||||||
|
),
|
||||||
|
customSize: MediaQuery.of(context).size,
|
||||||
|
basePosition: Alignment.center,
|
||||||
|
filterQuality: FilterQuality.high,
|
||||||
),
|
),
|
||||||
controller: photoViewController,
|
|
||||||
imageProvider: CloudImageWidget.provider(
|
|
||||||
fileId: item.id,
|
|
||||||
serverUrl: ref.watch(serverUrlProvider),
|
|
||||||
original: showOriginal.value,
|
|
||||||
),
|
|
||||||
customSize: MediaQuery.of(context).size,
|
|
||||||
basePosition: Alignment.center,
|
|
||||||
filterQuality: FilterQuality.high,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Controls overlay
|
// Controls overlay
|
||||||
@@ -245,68 +323,57 @@ class GenericFileContent extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Center(
|
return Center(
|
||||||
child: Container(
|
child: Column(
|
||||||
margin: const EdgeInsets.all(32),
|
mainAxisSize: MainAxisSize.min,
|
||||||
padding: const EdgeInsets.all(32),
|
children: [
|
||||||
decoration: BoxDecoration(
|
Icon(
|
||||||
border: Border.all(
|
Symbols.insert_drive_file,
|
||||||
color: Theme.of(context).colorScheme.outline,
|
size: 64,
|
||||||
width: 1,
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(16),
|
const Gap(16),
|
||||||
),
|
Text(
|
||||||
child: Column(
|
item.name,
|
||||||
mainAxisSize: MainAxisSize.min,
|
style: TextStyle(
|
||||||
children: [
|
fontSize: 20,
|
||||||
Icon(
|
fontWeight: FontWeight.bold,
|
||||||
Symbols.insert_drive_file,
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
size: 64,
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
Text(
|
||||||
|
formatFileSize(item.size),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
const Gap(16),
|
),
|
||||||
Text(
|
const Gap(24),
|
||||||
item.name,
|
Row(
|
||||||
style: TextStyle(
|
mainAxisSize: MainAxisSize.min,
|
||||||
fontSize: 20,
|
children: [
|
||||||
fontWeight: FontWeight.bold,
|
FilledButton.icon(
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
onPressed: downloadFile,
|
||||||
|
icon: const Icon(Symbols.download),
|
||||||
|
label: Text('download').tr(),
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
const Gap(16),
|
||||||
),
|
OutlinedButton.icon(
|
||||||
const Gap(8),
|
onPressed: () {
|
||||||
Text(
|
showModalBottomSheet(
|
||||||
formatFileSize(item.size),
|
useRootNavigator: true,
|
||||||
style: TextStyle(
|
context: context,
|
||||||
fontSize: 16,
|
isScrollControlled: true,
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
builder: (context) => FileInfoSheet(item: item),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: const Icon(Symbols.info),
|
||||||
|
label: Text('info').tr(),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
const Gap(24),
|
),
|
||||||
Row(
|
],
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
FilledButton.icon(
|
|
||||||
onPressed: downloadFile,
|
|
||||||
icon: const Icon(Symbols.download),
|
|
||||||
label: Text('download'),
|
|
||||||
),
|
|
||||||
const Gap(16),
|
|
||||||
OutlinedButton.icon(
|
|
||||||
onPressed: () {
|
|
||||||
showModalBottomSheet(
|
|
||||||
useRootNavigator: true,
|
|
||||||
context: context,
|
|
||||||
isScrollControlled: true,
|
|
||||||
builder: (context) => FileInfoSheet(item: item),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
icon: const Icon(Symbols.info),
|
|
||||||
label: Text('info'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -166,7 +166,6 @@ class MarkdownTextContent extends HookConsumerWidget {
|
|||||||
label: 'copyToClipboard'.tr(),
|
label: 'copyToClipboard'.tr(),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Clipboard.setData(ClipboardData(text: href));
|
Clipboard.setData(ClipboardData(text: href));
|
||||||
clearSnackBar(context);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user