Compare commits
123 Commits
ac2951479b
...
refactor/w
| Author | SHA1 | Date | |
|---|---|---|---|
| 0622498f4e | |||
| 844efcda1a | |||
| 98e39cce6a | |||
| 0c459bf7e3 | |||
| a2576abee0 | |||
| f4b28c3fa2 | |||
|
43d767bc03
|
|||
|
0910be88ef
|
|||
|
e96b1fd9d4
|
|||
|
3f83bbc1d8
|
|||
| 001549b190 | |||
| 4595865ad3 | |||
|
|
1834643167 | ||
|
|
0e816eaa3e | ||
|
|
7c1f24b824 | ||
|
c6594ea2ce
|
|||
|
3bec6e683e
|
|||
|
83e92e2eed
|
|||
|
|
b7d44d96ba | ||
|
a83b929d42
|
|||
|
9423affa75
|
|||
|
cda23db609
|
|||
|
61074bc5a3
|
|||
|
5feafa9255
|
|||
|
e604577c1f
|
|||
|
af0ddd1273
|
|||
|
8a6bb34808
|
|||
|
4ef8445c77
|
|||
|
ec39ad6ca3
|
|||
|
eabb3154f1
|
|||
|
910bf20eef
|
|||
|
5efa9b2ae8
|
|||
|
dd3e39e891
|
|||
|
b6896ded23
|
|||
|
f28a73ff9c
|
|||
|
a014b64235
|
|||
|
7e0e7c20d7
|
|||
|
389fa515ba
|
|||
|
681ead02eb
|
|||
|
8d1c145b0b
|
|||
|
51b4754182
|
|||
|
8a2b321701
|
|||
|
f685a7a249
|
|||
|
76009147e9
|
|||
|
ce12f28e56
|
|||
|
3604373a1e
|
|||
|
9704a4c2c7
|
|||
|
67def56ad1
|
|||
|
1be33916af
|
|||
|
e8ff1bfd22
|
|||
|
3ae56f3d89
|
|||
|
707143e998
|
|||
|
1fd34eb2a3
|
|||
|
d7ca41e946
|
|||
|
ad9fb0719a
|
|||
|
e2d315afd4
|
|||
|
6124dbfd79
|
|||
|
5327f04ec0
|
|||
|
41c56a2319
|
|||
|
f9d033542e
|
|||
|
91784e65e6
|
|||
|
9d39c6a825
|
|||
|
537e49f1a4
|
|||
|
75bbd4df71
|
|||
|
6ef4580d93
|
|||
|
6ffd498761
|
|||
|
27157e7cc1
|
|||
|
bbb07d574a
|
|||
|
c660a419e2
|
|||
|
c3f61467c8
|
|||
|
9bc47df452
|
|||
|
9ef8ca4d45
|
|||
|
b55cbd08d1
|
|||
|
8c6bd0feaa
|
|||
|
7dd4b20628
|
|||
|
fec0cb7640
|
|||
|
75deb04a2b
|
|||
|
7c7ed21a96
|
|||
|
a201f20793
|
|||
|
598c51bc1a
|
|||
|
e1ea61c5f1
|
|||
|
ac424bde36
|
|||
|
b43b70df3f
|
|||
|
4321aa621a
|
|||
|
d5d275fb43
|
|||
|
6bb3307144
|
|||
|
391604d4a2
|
|||
|
1d9361c12f
|
|||
|
a129b9cdd0
|
|||
|
3bf815ac61
|
|||
|
77bae4d6fd
|
|||
|
0a301c4c9b
|
|||
|
27b390a51c
|
|||
|
018386d14e
|
|||
|
3825d7c6c7
|
|||
|
bf930291e4
|
|||
|
a8c4988790
|
|||
|
28dd204b1a
|
|||
|
3cbc1a59a7
|
|||
|
277e9ae3d1
|
|||
|
27b3ca25b7
|
|||
|
f871cd3b62
|
|||
|
a8a59ee30c
|
|||
|
2cd1416a13
|
|||
|
6be7dfbc61
|
|||
|
1abbd85614
|
|||
|
31ac5ad07c
|
|||
|
ae2ba495e9
|
|||
|
637aa44548
|
|||
|
44dbfc36d9
|
|||
|
5dbe7371cb
|
|||
|
6c91093198
|
|||
|
3f640b7898
|
|||
|
7db164fda6
|
|||
|
6df1d96cc9
|
|||
|
122a796f8c
|
|||
|
fbc7812a16
|
|||
|
0b1a23e81a
|
|||
|
c87e6cfe07
|
|||
|
53d51b8a0e
|
|||
|
337ae39e08
|
|||
|
8fe3a664a6
|
|||
|
3bfc0b8181
|
@@ -75,3 +75,4 @@ dependencies {
|
|||||||
flutter {
|
flutter {
|
||||||
source = "../.."
|
source = "../.."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,8 @@
|
|||||||
"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?",
|
||||||
@@ -133,6 +135,11 @@
|
|||||||
"reactionPositive": "Postive",
|
"reactionPositive": "Postive",
|
||||||
"reactionNegative": "Negative",
|
"reactionNegative": "Negative",
|
||||||
"reactionNeutral": "Neutral",
|
"reactionNeutral": "Neutral",
|
||||||
|
"customReaction": "Custom Reaction",
|
||||||
|
"customReactions": "Custom Reactions",
|
||||||
|
"stickerPlaceholder": "Sticker Placeholder",
|
||||||
|
"reactionAttitude": "Reaction Attitude",
|
||||||
|
"addReaction": "Add Reaction",
|
||||||
"connectionConnected": "Connected",
|
"connectionConnected": "Connected",
|
||||||
"connectionDisconnected": "Disconnected",
|
"connectionDisconnected": "Disconnected",
|
||||||
"connectionReconnecting": "Reconnecting",
|
"connectionReconnecting": "Reconnecting",
|
||||||
@@ -164,8 +171,8 @@
|
|||||||
"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",
|
"eventCalendar": "Event Calendar",
|
||||||
"eventCalanderEmpty": "No events on that day.",
|
"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",
|
||||||
@@ -251,11 +258,16 @@
|
|||||||
"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",
|
||||||
@@ -306,6 +318,8 @@
|
|||||||
"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"
|
||||||
@@ -319,6 +333,7 @@
|
|||||||
"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",
|
||||||
@@ -366,7 +381,6 @@
|
|||||||
"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",
|
||||||
@@ -465,7 +479,6 @@
|
|||||||
"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",
|
||||||
@@ -476,6 +489,7 @@
|
|||||||
"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",
|
||||||
@@ -625,6 +639,10 @@
|
|||||||
"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",
|
||||||
@@ -1009,6 +1027,10 @@
|
|||||||
"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",
|
||||||
@@ -1047,6 +1069,7 @@
|
|||||||
"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",
|
||||||
@@ -1087,5 +1110,147 @@
|
|||||||
"installUpdate": "Install update",
|
"installUpdate": "Install update",
|
||||||
"openReleasePage": "Open release page",
|
"openReleasePage": "Open release page",
|
||||||
"postCompose": "Compose Post",
|
"postCompose": "Compose Post",
|
||||||
"postPublish": "Publish 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",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"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",
|
||||||
|
"preview": "Preview",
|
||||||
|
"availableWithYourPlan": "Available with your plan",
|
||||||
|
"upgradeRequired": "Upgrade required",
|
||||||
|
"settingsDisableAnimation": "Disable Animation",
|
||||||
|
"addTag": "Add Tag"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -940,7 +940,7 @@
|
|||||||
"editBot": "编辑机器人",
|
"editBot": "编辑机器人",
|
||||||
"botAutomatedBy": "由 {} 自动化",
|
"botAutomatedBy": "由 {} 自动化",
|
||||||
"botDetails": "机器人详情",
|
"botDetails": "机器人详情",
|
||||||
"overview": "总揽",
|
"overview": "总览",
|
||||||
"keys": "密钥",
|
"keys": "密钥",
|
||||||
"botNotFound": "机器人未找到。",
|
"botNotFound": "机器人未找到。",
|
||||||
"newBotKey": "新建密钥",
|
"newBotKey": "新建密钥",
|
||||||
@@ -1060,7 +1060,7 @@
|
|||||||
"selectPool": "选择储存池",
|
"selectPool": "选择储存池",
|
||||||
"choosePool": "选择一个储存池",
|
"choosePool": "选择一个储存池",
|
||||||
"errorLoadingPools": "加载池时出错",
|
"errorLoadingPools": "加载池时出错",
|
||||||
"quotaCostInfo": "此上传将消耗{} 配额点",
|
"quotaCostInfo": "此上传将消耗 {} 配额点",
|
||||||
"uploadConstraints": "上传限制",
|
"uploadConstraints": "上传限制",
|
||||||
"fileSizeExceeded": "文件大小超过了 {} 的最大限制",
|
"fileSizeExceeded": "文件大小超过了 {} 的最大限制",
|
||||||
"fileTypeNotAccepted": "此储存池不接受该文件类型",
|
"fileTypeNotAccepted": "此储存池不接受该文件类型",
|
||||||
@@ -1075,5 +1075,11 @@
|
|||||||
"deleteRecycledFiles": "删除被回收的文件",
|
"deleteRecycledFiles": "删除被回收的文件",
|
||||||
"recycledFilesDeleted": "被回收文件成功删除",
|
"recycledFilesDeleted": "被回收文件成功删除",
|
||||||
"failedToDeleteRecycledFiles": "删除被回收文件失败",
|
"failedToDeleteRecycledFiles": "删除被回收文件失败",
|
||||||
"upload": "上传"
|
"upload": "上传",
|
||||||
|
"systemWallet": "中央统筹",
|
||||||
|
"postCompose": "撰写帖子",
|
||||||
|
"postPublish": "发布帖子",
|
||||||
|
"restoreDraftTitle": "恢复草稿",
|
||||||
|
"restoreDraftMessage": "发现了一个草稿。你想要恢复它吗?",
|
||||||
|
"draft": "草稿"
|
||||||
}
|
}
|
||||||
@@ -1075,5 +1075,7 @@
|
|||||||
"deleteRecycledFiles": "刪除已回收檔案",
|
"deleteRecycledFiles": "刪除已回收檔案",
|
||||||
"recycledFilesDeleted": "已回收檔案刪除成功",
|
"recycledFilesDeleted": "已回收檔案刪除成功",
|
||||||
"failedToDeleteRecycledFiles": "已回收檔案刪除失敗",
|
"failedToDeleteRecycledFiles": "已回收檔案刪除失敗",
|
||||||
"upload": "上傳"
|
"upload": "上傳",
|
||||||
|
"postCompose": "撰寫帖子",
|
||||||
|
"postPublish": "發佈帖子"
|
||||||
}
|
}
|
||||||
169
ios/Podfile.lock
169
ios/Podfile.lock
@@ -1,6 +1,6 @@
|
|||||||
PODS:
|
PODS:
|
||||||
- Alamofire (5.10.2)
|
- Alamofire (5.10.2)
|
||||||
- connectivity_plus (0.0.1):
|
- app_links (6.4.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- croppy (0.0.1):
|
- croppy (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
@@ -42,83 +42,83 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- file_saver (0.0.1):
|
- file_saver (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- Firebase/CoreOnly (12.2.0):
|
- Firebase/CoreOnly (12.4.0):
|
||||||
- FirebaseCore (~> 12.2.0)
|
- FirebaseCore (~> 12.4.0)
|
||||||
- Firebase/Crashlytics (12.2.0):
|
- Firebase/Crashlytics (12.4.0):
|
||||||
- Firebase/CoreOnly
|
- Firebase/CoreOnly
|
||||||
- FirebaseCrashlytics (~> 12.2.0)
|
- FirebaseCrashlytics (~> 12.4.0)
|
||||||
- Firebase/Messaging (12.2.0):
|
- Firebase/Messaging (12.4.0):
|
||||||
- Firebase/CoreOnly
|
- Firebase/CoreOnly
|
||||||
- FirebaseMessaging (~> 12.2.0)
|
- FirebaseMessaging (~> 12.4.0)
|
||||||
- firebase_analytics (12.0.2):
|
- firebase_analytics (12.0.3):
|
||||||
- firebase_core
|
- firebase_core
|
||||||
- FirebaseAnalytics (= 12.2.0)
|
- FirebaseAnalytics (= 12.4.0)
|
||||||
- Flutter
|
- Flutter
|
||||||
- firebase_core (4.1.1):
|
- firebase_core (4.2.0):
|
||||||
- Firebase/CoreOnly (= 12.2.0)
|
- Firebase/CoreOnly (= 12.4.0)
|
||||||
- Flutter
|
- Flutter
|
||||||
- firebase_crashlytics (5.0.2):
|
- firebase_crashlytics (5.0.3):
|
||||||
- Firebase/Crashlytics (= 12.2.0)
|
- Firebase/Crashlytics (= 12.4.0)
|
||||||
- firebase_core
|
- firebase_core
|
||||||
- Flutter
|
- Flutter
|
||||||
- firebase_messaging (16.0.2):
|
- firebase_messaging (16.0.3):
|
||||||
- Firebase/Messaging (= 12.2.0)
|
- Firebase/Messaging (= 12.4.0)
|
||||||
- firebase_core
|
- firebase_core
|
||||||
- Flutter
|
- Flutter
|
||||||
- FirebaseAnalytics (12.2.0):
|
- FirebaseAnalytics (12.4.0):
|
||||||
- FirebaseAnalytics/Default (= 12.2.0)
|
- FirebaseAnalytics/Default (= 12.4.0)
|
||||||
- FirebaseCore (~> 12.2.0)
|
- FirebaseCore (~> 12.4.0)
|
||||||
- FirebaseInstallations (~> 12.2.0)
|
- FirebaseInstallations (~> 12.4.0)
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||||
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
||||||
- GoogleUtilities/Network (~> 8.1)
|
- GoogleUtilities/Network (~> 8.1)
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- FirebaseAnalytics/Default (12.2.0):
|
- FirebaseAnalytics/Default (12.4.0):
|
||||||
- FirebaseCore (~> 12.2.0)
|
- FirebaseCore (~> 12.4.0)
|
||||||
- FirebaseInstallations (~> 12.2.0)
|
- FirebaseInstallations (~> 12.4.0)
|
||||||
- GoogleAppMeasurement/Default (= 12.2.0)
|
- GoogleAppMeasurement/Default (= 12.4.0)
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||||
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
||||||
- GoogleUtilities/Network (~> 8.1)
|
- GoogleUtilities/Network (~> 8.1)
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- FirebaseCore (12.2.0):
|
- FirebaseCore (12.4.0):
|
||||||
- FirebaseCoreInternal (~> 12.2.0)
|
- FirebaseCoreInternal (~> 12.4.0)
|
||||||
- GoogleUtilities/Environment (~> 8.1)
|
- GoogleUtilities/Environment (~> 8.1)
|
||||||
- GoogleUtilities/Logger (~> 8.1)
|
- GoogleUtilities/Logger (~> 8.1)
|
||||||
- FirebaseCoreExtension (12.2.0):
|
- FirebaseCoreExtension (12.4.0):
|
||||||
- FirebaseCore (~> 12.2.0)
|
- FirebaseCore (~> 12.4.0)
|
||||||
- FirebaseCoreInternal (12.2.0):
|
- FirebaseCoreInternal (12.4.0):
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||||
- FirebaseCrashlytics (12.2.0):
|
- FirebaseCrashlytics (12.4.0):
|
||||||
- FirebaseCore (~> 12.2.0)
|
- FirebaseCore (~> 12.4.0)
|
||||||
- FirebaseInstallations (~> 12.2.0)
|
- FirebaseInstallations (~> 12.4.0)
|
||||||
- FirebaseRemoteConfigInterop (~> 12.2.0)
|
- FirebaseRemoteConfigInterop (~> 12.4.0)
|
||||||
- FirebaseSessions (~> 12.2.0)
|
- FirebaseSessions (~> 12.4.0)
|
||||||
- GoogleDataTransport (~> 10.1)
|
- GoogleDataTransport (~> 10.1)
|
||||||
- GoogleUtilities/Environment (~> 8.1)
|
- GoogleUtilities/Environment (~> 8.1)
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- PromisesObjC (~> 2.4)
|
- PromisesObjC (~> 2.4)
|
||||||
- FirebaseInstallations (12.2.0):
|
- FirebaseInstallations (12.4.0):
|
||||||
- FirebaseCore (~> 12.2.0)
|
- FirebaseCore (~> 12.4.0)
|
||||||
- GoogleUtilities/Environment (~> 8.1)
|
- GoogleUtilities/Environment (~> 8.1)
|
||||||
- GoogleUtilities/UserDefaults (~> 8.1)
|
- GoogleUtilities/UserDefaults (~> 8.1)
|
||||||
- PromisesObjC (~> 2.4)
|
- PromisesObjC (~> 2.4)
|
||||||
- FirebaseMessaging (12.2.0):
|
- FirebaseMessaging (12.4.0):
|
||||||
- FirebaseCore (~> 12.2.0)
|
- FirebaseCore (~> 12.4.0)
|
||||||
- FirebaseInstallations (~> 12.2.0)
|
- FirebaseInstallations (~> 12.4.0)
|
||||||
- GoogleDataTransport (~> 10.1)
|
- GoogleDataTransport (~> 10.1)
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||||
- GoogleUtilities/Environment (~> 8.1)
|
- GoogleUtilities/Environment (~> 8.1)
|
||||||
- GoogleUtilities/Reachability (~> 8.1)
|
- GoogleUtilities/Reachability (~> 8.1)
|
||||||
- GoogleUtilities/UserDefaults (~> 8.1)
|
- GoogleUtilities/UserDefaults (~> 8.1)
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- FirebaseRemoteConfigInterop (12.2.0)
|
- FirebaseRemoteConfigInterop (12.4.0)
|
||||||
- FirebaseSessions (12.2.0):
|
- FirebaseSessions (12.4.0):
|
||||||
- FirebaseCore (~> 12.2.0)
|
- FirebaseCore (~> 12.4.0)
|
||||||
- FirebaseCoreExtension (~> 12.2.0)
|
- FirebaseCoreExtension (~> 12.4.0)
|
||||||
- FirebaseInstallations (~> 12.2.0)
|
- FirebaseInstallations (~> 12.4.0)
|
||||||
- GoogleDataTransport (~> 10.1)
|
- GoogleDataTransport (~> 10.1)
|
||||||
- GoogleUtilities/Environment (~> 8.1)
|
- GoogleUtilities/Environment (~> 8.1)
|
||||||
- GoogleUtilities/UserDefaults (~> 8.1)
|
- GoogleUtilities/UserDefaults (~> 8.1)
|
||||||
@@ -155,27 +155,28 @@ PODS:
|
|||||||
- gal (1.0.0):
|
- gal (1.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- GoogleAdsOnDeviceConversion (2.3.0):
|
- GoogleAdsOnDeviceConversion (3.1.0):
|
||||||
|
- GoogleUtilities/Environment (~> 8.1)
|
||||||
- GoogleUtilities/Logger (~> 8.1)
|
- GoogleUtilities/Logger (~> 8.1)
|
||||||
- GoogleUtilities/Network (~> 8.1)
|
- GoogleUtilities/Network (~> 8.1)
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- GoogleAppMeasurement/Core (12.2.0):
|
- GoogleAppMeasurement/Core (12.4.0):
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||||
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
||||||
- GoogleUtilities/Network (~> 8.1)
|
- GoogleUtilities/Network (~> 8.1)
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- GoogleAppMeasurement/Default (12.2.0):
|
- GoogleAppMeasurement/Default (12.4.0):
|
||||||
- GoogleAdsOnDeviceConversion (= 2.3.0)
|
- GoogleAdsOnDeviceConversion (~> 3.1.0)
|
||||||
- GoogleAppMeasurement/Core (= 12.2.0)
|
- GoogleAppMeasurement/Core (= 12.4.0)
|
||||||
- GoogleAppMeasurement/IdentitySupport (= 12.2.0)
|
- GoogleAppMeasurement/IdentitySupport (= 12.4.0)
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||||
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
||||||
- GoogleUtilities/Network (~> 8.1)
|
- GoogleUtilities/Network (~> 8.1)
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- GoogleAppMeasurement/IdentitySupport (12.2.0):
|
- GoogleAppMeasurement/IdentitySupport (12.4.0):
|
||||||
- GoogleAppMeasurement/Core (= 12.2.0)
|
- GoogleAppMeasurement/Core (= 12.4.0)
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||||
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
||||||
- GoogleUtilities/Network (~> 8.1)
|
- GoogleUtilities/Network (~> 8.1)
|
||||||
@@ -215,11 +216,7 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- irondash_engine_context (0.0.1):
|
- irondash_engine_context (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- Kingfisher (8.5.0)
|
- Kingfisher (8.6.0)
|
||||||
- livekit_client (2.5.0):
|
|
||||||
- Flutter
|
|
||||||
- flutter_webrtc
|
|
||||||
- WebRTC-SDK (= 137.7151.04)
|
|
||||||
- local_auth_darwin (0.0.1):
|
- local_auth_darwin (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
@@ -252,9 +249,9 @@ PODS:
|
|||||||
- record_ios (1.1.0):
|
- record_ios (1.1.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- SAMKeychain (1.5.3)
|
- SAMKeychain (1.5.3)
|
||||||
- SDWebImage (5.21.2):
|
- SDWebImage (5.21.3):
|
||||||
- SDWebImage/Core (= 5.21.2)
|
- SDWebImage/Core (= 5.21.3)
|
||||||
- SDWebImage/Core (5.21.2)
|
- SDWebImage/Core (5.21.3)
|
||||||
- share_plus (0.0.1):
|
- share_plus (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- shared_preferences_foundation (0.0.1):
|
- shared_preferences_foundation (0.0.1):
|
||||||
@@ -293,6 +290,8 @@ PODS:
|
|||||||
- super_native_extensions (0.0.1):
|
- super_native_extensions (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- SwiftyGif (5.4.5)
|
- SwiftyGif (5.4.5)
|
||||||
|
- syncfusion_flutter_pdfviewer (0.0.1):
|
||||||
|
- Flutter
|
||||||
- url_launcher_ios (0.0.1):
|
- url_launcher_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- volume_controller (0.0.1):
|
- volume_controller (0.0.1):
|
||||||
@@ -303,7 +302,7 @@ PODS:
|
|||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- Alamofire
|
- Alamofire
|
||||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
- app_links (from `.symlinks/plugins/app_links/ios`)
|
||||||
- croppy (from `.symlinks/plugins/croppy/ios`)
|
- croppy (from `.symlinks/plugins/croppy/ios`)
|
||||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||||
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
||||||
@@ -327,7 +326,6 @@ DEPENDENCIES:
|
|||||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||||
- irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`)
|
- irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`)
|
||||||
- Kingfisher (~> 8.0)
|
- Kingfisher (~> 8.0)
|
||||||
- livekit_client (from `.symlinks/plugins/livekit_client/ios`)
|
|
||||||
- local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`)
|
- local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`)
|
||||||
- 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`)
|
||||||
@@ -344,6 +342,7 @@ DEPENDENCIES:
|
|||||||
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
||||||
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`)
|
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`)
|
||||||
- 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`)
|
||||||
- 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`)
|
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
|
||||||
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
||||||
@@ -379,8 +378,8 @@ SPEC REPOS:
|
|||||||
- WebRTC-SDK
|
- WebRTC-SDK
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
connectivity_plus:
|
app_links:
|
||||||
:path: ".symlinks/plugins/connectivity_plus/ios"
|
:path: ".symlinks/plugins/app_links/ios"
|
||||||
croppy:
|
croppy:
|
||||||
:path: ".symlinks/plugins/croppy/ios"
|
:path: ".symlinks/plugins/croppy/ios"
|
||||||
device_info_plus:
|
device_info_plus:
|
||||||
@@ -425,8 +424,6 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/image_picker_ios/ios"
|
:path: ".symlinks/plugins/image_picker_ios/ios"
|
||||||
irondash_engine_context:
|
irondash_engine_context:
|
||||||
:path: ".symlinks/plugins/irondash_engine_context/ios"
|
:path: ".symlinks/plugins/irondash_engine_context/ios"
|
||||||
livekit_client:
|
|
||||||
:path: ".symlinks/plugins/livekit_client/ios"
|
|
||||||
local_auth_darwin:
|
local_auth_darwin:
|
||||||
:path: ".symlinks/plugins/local_auth_darwin/darwin"
|
:path: ".symlinks/plugins/local_auth_darwin/darwin"
|
||||||
media_kit_libs_ios_video:
|
media_kit_libs_ios_video:
|
||||||
@@ -459,6 +456,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/sqlite3_flutter_libs/darwin"
|
:path: ".symlinks/plugins/sqlite3_flutter_libs/darwin"
|
||||||
super_native_extensions:
|
super_native_extensions:
|
||||||
:path: ".symlinks/plugins/super_native_extensions/ios"
|
:path: ".symlinks/plugins/super_native_extensions/ios"
|
||||||
|
syncfusion_flutter_pdfviewer:
|
||||||
|
: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:
|
volume_controller:
|
||||||
@@ -468,27 +467,27 @@ EXTERNAL SOURCES:
|
|||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
|
Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
|
||||||
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
|
app_links: 3dbc685f76b1693c66a6d9dd1e9ab6f73d97dc0a
|
||||||
croppy: 979e8ddc254f4642bffe7d52dc7193354b27ba30
|
croppy: 979e8ddc254f4642bffe7d52dc7193354b27ba30
|
||||||
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
|
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
|
||||||
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
||||||
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||||
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
|
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
|
||||||
file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6
|
file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6
|
||||||
Firebase: 26f6f8d460603af3df970ad505b16b15f5e2e9a1
|
Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e
|
||||||
firebase_analytics: 8c78ce6224e0623152379d6cc7ef3d9098477b7e
|
firebase_analytics: 1d024068b1d4707d5ba7a42a12976ddf3316d835
|
||||||
firebase_core: dfc4bd142bee4bc53a5d482397ca322c2dd3165d
|
firebase_core: 744984dbbed8b3036abf34f0b98d80f130a7e464
|
||||||
firebase_crashlytics: e55dcf895eed0dd87c447dd5aff8db7f1bb8bbdb
|
firebase_crashlytics: f3a9a4338ab99b67042f64e9e22e1bf349cb44ed
|
||||||
firebase_messaging: 38c66c1184695b0c87abe51d40fc590718abed1a
|
firebase_messaging: 82c70650c426a0a14873e1acdb9ec2b443c4e8b4
|
||||||
FirebaseAnalytics: e04e23bc070e3014aa5cf4980f9df7ce5cd79ec8
|
FirebaseAnalytics: 0fc2b20091f0ddd21bf73397cf8f0eb5346dc24f
|
||||||
FirebaseCore: 311c48a147ad4a0ab7febbaed89e8025c67510cd
|
FirebaseCore: bb595f3114953664e3c1dc032f008a244147cfd3
|
||||||
FirebaseCoreExtension: 73af080c22a2f7b44cefa391dc08f7e4ee162cb5
|
FirebaseCoreExtension: 7e1f7118ee970e001a8013719fb90950ee5e0018
|
||||||
FirebaseCoreInternal: 56ea29f3dad2894f81b060f706f9d53509b6ed3b
|
FirebaseCoreInternal: d7f5a043c2cd01a08103ab586587c1468047bca6
|
||||||
FirebaseCrashlytics: f83cbf176d5c637ade108c0aacf1ccbd5ec499bf
|
FirebaseCrashlytics: a6ece278a837c7e88de2d9b5da0a3542f2342395
|
||||||
FirebaseInstallations: 3e884b01feabdf67582a80f3250425a00979b4ed
|
FirebaseInstallations: ae9f4902cb5bf1d0c5eaa31ec1f4e5495a0714e2
|
||||||
FirebaseMessaging: 43ec73bbfedd0c385a849bb91593ab4ad4b9e48e
|
FirebaseMessaging: d33971b7bb252745ea6cd31ab190d1a1df4b8ed5
|
||||||
FirebaseRemoteConfigInterop: 0896fd52ab72586a355c8f389ff85aaa9e5375e1
|
FirebaseRemoteConfigInterop: 1e31ec72b89c9924367c59bfb5ec9ab60d1d6766
|
||||||
FirebaseSessions: f4692789e770bec66ce17d772c0e9561c4f11737
|
FirebaseSessions: ba7c7a7ca8696a8d540eb3fe3800fbe98c79786d
|
||||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||||
flutter_app_update: 816fdb2e30e4832a7c45e3f108d391c42ef040a9
|
flutter_app_update: 816fdb2e30e4832a7c45e3f108d391c42ef040a9
|
||||||
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
|
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
|
||||||
@@ -501,14 +500,13 @@ SPEC CHECKSUMS:
|
|||||||
flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9
|
flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9
|
||||||
flutter_webrtc: c3e21fc0dcd9d8eb246ae4d5256fcbeb2f5ecd22
|
flutter_webrtc: c3e21fc0dcd9d8eb246ae4d5256fcbeb2f5ecd22
|
||||||
gal: baecd024ebfd13c441269ca7404792a7152fde89
|
gal: baecd024ebfd13c441269ca7404792a7152fde89
|
||||||
GoogleAdsOnDeviceConversion: 9090c435cde08903e8dd1ba2c77fbec9e46d9afe
|
GoogleAdsOnDeviceConversion: e03a386840803ea7eef3fd22a061930142c039c1
|
||||||
GoogleAppMeasurement: 09f341dfa8527d1612a09cbfe809a242c0b737af
|
GoogleAppMeasurement: 1e718274b7e015cefd846ac1fcf7820c70dc017d
|
||||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||||
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
||||||
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
|
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
|
||||||
irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
|
irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
|
||||||
Kingfisher: ff0d31a1f07bdff6a1ebb3ba08b8e6e567b6500c
|
Kingfisher: 64278f126a815d0e2d391cdf71311b85882c4de0
|
||||||
livekit_client: a6f5fa86ac28ccd7ded53626a5379961db311ab4
|
|
||||||
local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb
|
local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb
|
||||||
media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
|
media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
|
||||||
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
|
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
|
||||||
@@ -524,7 +522,7 @@ SPEC CHECKSUMS:
|
|||||||
receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
|
receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
|
||||||
record_ios: f75fa1d57f840012775c0e93a38a7f3ceea1a374
|
record_ios: f75fa1d57f840012775c0e93a38a7f3ceea1a374
|
||||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||||
SDWebImage: 9f177d83116802728e122410fb25ad88f5c7608a
|
SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a
|
||||||
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||||
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
||||||
sign_in_with_apple: c5dcc141574c8c54d5ac99dd2163c0c72ad22418
|
sign_in_with_apple: c5dcc141574c8c54d5ac99dd2163c0c72ad22418
|
||||||
@@ -533,6 +531,7 @@ SPEC CHECKSUMS:
|
|||||||
sqlite3_flutter_libs: 83f8e9f5b6554077f1d93119fe20ebaa5f3a9ef1
|
sqlite3_flutter_libs: 83f8e9f5b6554077f1d93119fe20ebaa5f3a9ef1
|
||||||
super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4
|
super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4
|
||||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||||
|
syncfusion_flutter_pdfviewer: 90dc48305d2e33d4aa20681d1e98ddeda891bc14
|
||||||
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
|
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
|
||||||
volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12
|
volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12
|
||||||
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import UIKit
|
|||||||
UNUserNotificationCenter.current().delegate = notifyDelegate
|
UNUserNotificationCenter.current().delegate = notifyDelegate
|
||||||
|
|
||||||
let replyableMessageCategory = UNNotificationCategory(
|
let replyableMessageCategory = UNNotificationCategory(
|
||||||
identifier: "REPLYABLE_MESSAGE",
|
identifier: "CHAT_MESSAGE",
|
||||||
actions: [
|
actions: [
|
||||||
UNTextInputNotificationAction(
|
UNTextInputNotificationAction(
|
||||||
identifier: "reply_action",
|
identifier: "reply_action",
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ class NotificationService: UNNotificationServiceExtension {
|
|||||||
private func processNotification(request: UNNotificationRequest, content: UNMutableNotificationContent) throws {
|
private func processNotification(request: UNNotificationRequest, content: UNMutableNotificationContent) throws {
|
||||||
switch content.userInfo["type"] as? String {
|
switch content.userInfo["type"] as? String {
|
||||||
case "messages.new":
|
case "messages.new":
|
||||||
content.categoryIdentifier = "REPLYABLE_MESSAGE"
|
|
||||||
try handleMessagingNotification(request: request, content: content)
|
try handleMessagingNotification(request: request, content: content)
|
||||||
default:
|
default:
|
||||||
try handleDefaultNotification(content: content)
|
try handleDefaultNotification(content: content)
|
||||||
@@ -89,7 +88,12 @@ class NotificationService: UNNotificationServiceExtension {
|
|||||||
let intent = self.createMessageIntent(with: sender, meta: metaCopy, body: content.body)
|
let intent = self.createMessageIntent(with: sender, meta: metaCopy, body: content.body)
|
||||||
self.donateInteraction(for: intent)
|
self.donateInteraction(for: intent)
|
||||||
let updatedContent = try? request.content.updating(from: intent)
|
let updatedContent = try? request.content.updating(from: intent)
|
||||||
self.contentHandler?(updatedContent ?? content)
|
content.categoryIdentifier = "CHAT_MESSAGE"
|
||||||
|
if let updatedContent = updatedContent {
|
||||||
|
self.contentHandler?(updatedContent)
|
||||||
|
} else {
|
||||||
|
self.contentHandler?(content)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:drift/wasm.dart';
|
import 'package:drift/wasm.dart';
|
||||||
import 'package:island/database/drift_db.dart';
|
import 'package:island/database/drift_db.dart';
|
||||||
|
import 'package:island/talker.dart';
|
||||||
|
|
||||||
AppDatabase constructDb() {
|
AppDatabase constructDb() {
|
||||||
return AppDatabase(connectOnWeb());
|
return AppDatabase(connectOnWeb());
|
||||||
@@ -9,12 +10,17 @@ AppDatabase constructDb() {
|
|||||||
DatabaseConnection connectOnWeb() {
|
DatabaseConnection connectOnWeb() {
|
||||||
return DatabaseConnection.delayed(
|
return DatabaseConnection.delayed(
|
||||||
Future(() async {
|
Future(() async {
|
||||||
|
try {
|
||||||
final result = await WasmDatabase.open(
|
final result = await WasmDatabase.open(
|
||||||
databaseName: 'solar_network_data',
|
databaseName: 'solar_network_data',
|
||||||
sqlite3Uri: Uri.parse('sqlite3.wasm'),
|
sqlite3Uri: Uri.parse('sqlite3.wasm'),
|
||||||
driftWorkerUri: Uri.parse('drift_worker.dart.js'),
|
driftWorkerUri: Uri.parse('drift_worker.dart.js'),
|
||||||
);
|
);
|
||||||
return result.resolvedExecutor;
|
return result.resolvedExecutor;
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
talker.error('Failed to open WASM database...', e, stackTrace);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,13 +120,24 @@ void main() async {
|
|||||||
windowButtonVisibility: true,
|
windowButtonVisibility: true,
|
||||||
);
|
);
|
||||||
windowManager.waitUntilReadyToShow(windowOptions, () async {
|
windowManager.waitUntilReadyToShow(windowOptions, () async {
|
||||||
|
final env = Platform.environment;
|
||||||
|
final isWayland = env.containsKey('WAYLAND_DISPLAY');
|
||||||
|
|
||||||
|
if (isWayland) {
|
||||||
|
try {
|
||||||
|
await windowManager.setAsFrameless();
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('[Wayland] setAsFrameless failed: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
await windowManager.setMinimumSize(defaultSize);
|
await windowManager.setMinimumSize(defaultSize);
|
||||||
await windowManager.show();
|
await windowManager.show();
|
||||||
await windowManager.focus();
|
await windowManager.focus();
|
||||||
final opacity = prefs.getDouble(kAppWindowOpacity) ?? 1.0;
|
final opacity = prefs.getDouble(kAppWindowOpacity) ?? 1.0;
|
||||||
await windowManager.setOpacity(opacity);
|
await windowManager.setOpacity(opacity);
|
||||||
talker.info(
|
talker.info(
|
||||||
"[SplashScreen] Desktop window is ready with size: ${initialSize.width}x${initialSize.height}",
|
"[SplashScreen] Desktop window is ready with size: ${initialSize.width}x${initialSize.height}"
|
||||||
|
"${isWayland ? " (Wayland frameless fix applied)" : ""}",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -279,8 +290,8 @@ class IslandApp extends HookConsumerWidget {
|
|||||||
|
|
||||||
return MaterialApp.router(
|
return MaterialApp.router(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
theme: theme?.light,
|
theme: theme.light,
|
||||||
darkTheme: theme?.dark,
|
darkTheme: theme.dark,
|
||||||
themeMode: getThemeMode(),
|
themeMode: getThemeMode(),
|
||||||
routerConfig: router,
|
routerConfig: router,
|
||||||
supportedLocales: context.supportedLocales,
|
supportedLocales: context.supportedLocales,
|
||||||
|
|||||||
@@ -55,6 +55,19 @@ class ProfileLinkConverter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
sealed class UsernameColor with _$UsernameColor {
|
||||||
|
const factory UsernameColor({
|
||||||
|
@Default('plain') String type,
|
||||||
|
String? value,
|
||||||
|
String? direction,
|
||||||
|
List<String>? colors,
|
||||||
|
}) = _UsernameColor;
|
||||||
|
|
||||||
|
factory UsernameColor.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$UsernameColorFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
sealed class SnAccountProfile with _$SnAccountProfile {
|
sealed class SnAccountProfile with _$SnAccountProfile {
|
||||||
const factory SnAccountProfile({
|
const factory SnAccountProfile({
|
||||||
@@ -79,6 +92,7 @@ sealed class SnAccountProfile with _$SnAccountProfile {
|
|||||||
required SnCloudFile? picture,
|
required SnCloudFile? picture,
|
||||||
required SnCloudFile? background,
|
required SnCloudFile? background,
|
||||||
required SnVerificationMark? verification,
|
required SnVerificationMark? verification,
|
||||||
|
UsernameColor? usernameColor,
|
||||||
required DateTime createdAt,
|
required DateTime createdAt,
|
||||||
required DateTime updatedAt,
|
required DateTime updatedAt,
|
||||||
required DateTime? deletedAt,
|
required DateTime? deletedAt,
|
||||||
|
|||||||
@@ -622,10 +622,284 @@ as String,
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$UsernameColor {
|
||||||
|
|
||||||
|
String get type; String? get value; String? get direction; List<String>? get colors;
|
||||||
|
/// Create a copy of UsernameColor
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$UsernameColorCopyWith<UsernameColor> get copyWith => _$UsernameColorCopyWithImpl<UsernameColor>(this as UsernameColor, _$identity);
|
||||||
|
|
||||||
|
/// Serializes this UsernameColor to a JSON map.
|
||||||
|
Map<String, dynamic> toJson();
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is UsernameColor&&(identical(other.type, type) || other.type == type)&&(identical(other.value, value) || other.value == value)&&(identical(other.direction, direction) || other.direction == direction)&&const DeepCollectionEquality().equals(other.colors, colors));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,type,value,direction,const DeepCollectionEquality().hash(colors));
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'UsernameColor(type: $type, value: $value, direction: $direction, colors: $colors)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class $UsernameColorCopyWith<$Res> {
|
||||||
|
factory $UsernameColorCopyWith(UsernameColor value, $Res Function(UsernameColor) _then) = _$UsernameColorCopyWithImpl;
|
||||||
|
@useResult
|
||||||
|
$Res call({
|
||||||
|
String type, String? value, String? direction, List<String>? colors
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class _$UsernameColorCopyWithImpl<$Res>
|
||||||
|
implements $UsernameColorCopyWith<$Res> {
|
||||||
|
_$UsernameColorCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final UsernameColor _self;
|
||||||
|
final $Res Function(UsernameColor) _then;
|
||||||
|
|
||||||
|
/// Create a copy of UsernameColor
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline') @override $Res call({Object? type = null,Object? value = freezed,Object? direction = freezed,Object? colors = freezed,}) {
|
||||||
|
return _then(_self.copyWith(
|
||||||
|
type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,value: freezed == value ? _self.value : value // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,direction: freezed == direction ? _self.direction : direction // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,colors: freezed == colors ? _self.colors : colors // ignore: cast_nullable_to_non_nullable
|
||||||
|
as List<String>?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Adds pattern-matching-related methods to [UsernameColor].
|
||||||
|
extension UsernameColorPatterns on UsernameColor {
|
||||||
|
/// 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( _UsernameColor value)? $default,{required TResult orElse(),}){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _UsernameColor() 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( _UsernameColor value) $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _UsernameColor():
|
||||||
|
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( _UsernameColor value)? $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _UsernameColor() 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 type, String? value, String? direction, List<String>? colors)? $default,{required TResult orElse(),}) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _UsernameColor() when $default != null:
|
||||||
|
return $default(_that.type,_that.value,_that.direction,_that.colors);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 type, String? value, String? direction, List<String>? colors) $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _UsernameColor():
|
||||||
|
return $default(_that.type,_that.value,_that.direction,_that.colors);}
|
||||||
|
}
|
||||||
|
/// 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 type, String? value, String? direction, List<String>? colors)? $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _UsernameColor() when $default != null:
|
||||||
|
return $default(_that.type,_that.value,_that.direction,_that.colors);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
|
||||||
|
class _UsernameColor implements UsernameColor {
|
||||||
|
const _UsernameColor({this.type = 'plain', this.value, this.direction, final List<String>? colors}): _colors = colors;
|
||||||
|
factory _UsernameColor.fromJson(Map<String, dynamic> json) => _$UsernameColorFromJson(json);
|
||||||
|
|
||||||
|
@override@JsonKey() final String type;
|
||||||
|
@override final String? value;
|
||||||
|
@override final String? direction;
|
||||||
|
final List<String>? _colors;
|
||||||
|
@override List<String>? get colors {
|
||||||
|
final value = _colors;
|
||||||
|
if (value == null) return null;
|
||||||
|
if (_colors is EqualUnmodifiableListView) return _colors;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableListView(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Create a copy of UsernameColor
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$UsernameColorCopyWith<_UsernameColor> get copyWith => __$UsernameColorCopyWithImpl<_UsernameColor>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$UsernameColorToJson(this, );
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is _UsernameColor&&(identical(other.type, type) || other.type == type)&&(identical(other.value, value) || other.value == value)&&(identical(other.direction, direction) || other.direction == direction)&&const DeepCollectionEquality().equals(other._colors, _colors));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,type,value,direction,const DeepCollectionEquality().hash(_colors));
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'UsernameColor(type: $type, value: $value, direction: $direction, colors: $colors)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class _$UsernameColorCopyWith<$Res> implements $UsernameColorCopyWith<$Res> {
|
||||||
|
factory _$UsernameColorCopyWith(_UsernameColor value, $Res Function(_UsernameColor) _then) = __$UsernameColorCopyWithImpl;
|
||||||
|
@override @useResult
|
||||||
|
$Res call({
|
||||||
|
String type, String? value, String? direction, List<String>? colors
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class __$UsernameColorCopyWithImpl<$Res>
|
||||||
|
implements _$UsernameColorCopyWith<$Res> {
|
||||||
|
__$UsernameColorCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final _UsernameColor _self;
|
||||||
|
final $Res Function(_UsernameColor) _then;
|
||||||
|
|
||||||
|
/// Create a copy of UsernameColor
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @pragma('vm:prefer-inline') $Res call({Object? type = null,Object? value = freezed,Object? direction = freezed,Object? colors = freezed,}) {
|
||||||
|
return _then(_UsernameColor(
|
||||||
|
type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,value: freezed == value ? _self.value : value // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,direction: freezed == direction ? _self.direction : direction // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,colors: freezed == colors ? _self._colors : colors // ignore: cast_nullable_to_non_nullable
|
||||||
|
as List<String>?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
mixin _$SnAccountProfile {
|
mixin _$SnAccountProfile {
|
||||||
|
|
||||||
String get id; String get firstName; String get middleName; String get lastName; String get bio; String get gender; String get pronouns; String get location; String get timeZone; DateTime? get birthday;@ProfileLinkConverter() List<ProfileLink> get links; DateTime? get lastSeenAt; SnAccountBadge? get activeBadge; int get experience; int get level; double get socialCredits; int get socialCreditsLevel; double get levelingProgress; SnCloudFile? get picture; SnCloudFile? get background; SnVerificationMark? get verification; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
|
String get id; String get firstName; String get middleName; String get lastName; String get bio; String get gender; String get pronouns; String get location; String get timeZone; DateTime? get birthday;@ProfileLinkConverter() List<ProfileLink> get links; DateTime? get lastSeenAt; SnAccountBadge? get activeBadge; int get experience; int get level; double get socialCredits; int get socialCreditsLevel; double get levelingProgress; SnCloudFile? get picture; SnCloudFile? get background; SnVerificationMark? get verification; UsernameColor? get usernameColor; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
|
||||||
/// Create a copy of SnAccountProfile
|
/// Create a copy of SnAccountProfile
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@@ -638,16 +912,16 @@ $SnAccountProfileCopyWith<SnAccountProfile> get copyWith => _$SnAccountProfileCo
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAccountProfile&&(identical(other.id, id) || other.id == id)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.middleName, middleName) || other.middleName == middleName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.gender, gender) || other.gender == gender)&&(identical(other.pronouns, pronouns) || other.pronouns == pronouns)&&(identical(other.location, location) || other.location == location)&&(identical(other.timeZone, timeZone) || other.timeZone == timeZone)&&(identical(other.birthday, birthday) || other.birthday == birthday)&&const DeepCollectionEquality().equals(other.links, links)&&(identical(other.lastSeenAt, lastSeenAt) || other.lastSeenAt == lastSeenAt)&&(identical(other.activeBadge, activeBadge) || other.activeBadge == activeBadge)&&(identical(other.experience, experience) || other.experience == experience)&&(identical(other.level, level) || other.level == level)&&(identical(other.socialCredits, socialCredits) || other.socialCredits == socialCredits)&&(identical(other.socialCreditsLevel, socialCreditsLevel) || other.socialCreditsLevel == socialCreditsLevel)&&(identical(other.levelingProgress, levelingProgress) || other.levelingProgress == levelingProgress)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.verification, verification) || other.verification == verification)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAccountProfile&&(identical(other.id, id) || other.id == id)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.middleName, middleName) || other.middleName == middleName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.gender, gender) || other.gender == gender)&&(identical(other.pronouns, pronouns) || other.pronouns == pronouns)&&(identical(other.location, location) || other.location == location)&&(identical(other.timeZone, timeZone) || other.timeZone == timeZone)&&(identical(other.birthday, birthday) || other.birthday == birthday)&&const DeepCollectionEquality().equals(other.links, links)&&(identical(other.lastSeenAt, lastSeenAt) || other.lastSeenAt == lastSeenAt)&&(identical(other.activeBadge, activeBadge) || other.activeBadge == activeBadge)&&(identical(other.experience, experience) || other.experience == experience)&&(identical(other.level, level) || other.level == level)&&(identical(other.socialCredits, socialCredits) || other.socialCredits == socialCredits)&&(identical(other.socialCreditsLevel, socialCreditsLevel) || other.socialCreditsLevel == socialCreditsLevel)&&(identical(other.levelingProgress, levelingProgress) || other.levelingProgress == levelingProgress)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.verification, verification) || other.verification == verification)&&(identical(other.usernameColor, usernameColor) || other.usernameColor == usernameColor)&&(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)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hashAll([runtimeType,id,firstName,middleName,lastName,bio,gender,pronouns,location,timeZone,birthday,const DeepCollectionEquality().hash(links),lastSeenAt,activeBadge,experience,level,socialCredits,socialCreditsLevel,levelingProgress,picture,background,verification,createdAt,updatedAt,deletedAt]);
|
int get hashCode => Object.hashAll([runtimeType,id,firstName,middleName,lastName,bio,gender,pronouns,location,timeZone,birthday,const DeepCollectionEquality().hash(links),lastSeenAt,activeBadge,experience,level,socialCredits,socialCreditsLevel,levelingProgress,picture,background,verification,usernameColor,createdAt,updatedAt,deletedAt]);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'SnAccountProfile(id: $id, firstName: $firstName, middleName: $middleName, lastName: $lastName, bio: $bio, gender: $gender, pronouns: $pronouns, location: $location, timeZone: $timeZone, birthday: $birthday, links: $links, lastSeenAt: $lastSeenAt, activeBadge: $activeBadge, experience: $experience, level: $level, socialCredits: $socialCredits, socialCreditsLevel: $socialCreditsLevel, levelingProgress: $levelingProgress, picture: $picture, background: $background, verification: $verification, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
return 'SnAccountProfile(id: $id, firstName: $firstName, middleName: $middleName, lastName: $lastName, bio: $bio, gender: $gender, pronouns: $pronouns, location: $location, timeZone: $timeZone, birthday: $birthday, links: $links, lastSeenAt: $lastSeenAt, activeBadge: $activeBadge, experience: $experience, level: $level, socialCredits: $socialCredits, socialCreditsLevel: $socialCreditsLevel, levelingProgress: $levelingProgress, picture: $picture, background: $background, verification: $verification, usernameColor: $usernameColor, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -658,11 +932,11 @@ abstract mixin class $SnAccountProfileCopyWith<$Res> {
|
|||||||
factory $SnAccountProfileCopyWith(SnAccountProfile value, $Res Function(SnAccountProfile) _then) = _$SnAccountProfileCopyWithImpl;
|
factory $SnAccountProfileCopyWith(SnAccountProfile value, $Res Function(SnAccountProfile) _then) = _$SnAccountProfileCopyWithImpl;
|
||||||
@useResult
|
@useResult
|
||||||
$Res call({
|
$Res call({
|
||||||
String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday,@ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday,@ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, UsernameColor? usernameColor, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
$SnAccountBadgeCopyWith<$Res>? get activeBadge;$SnCloudFileCopyWith<$Res>? get picture;$SnCloudFileCopyWith<$Res>? get background;$SnVerificationMarkCopyWith<$Res>? get verification;
|
$SnAccountBadgeCopyWith<$Res>? get activeBadge;$SnCloudFileCopyWith<$Res>? get picture;$SnCloudFileCopyWith<$Res>? get background;$SnVerificationMarkCopyWith<$Res>? get verification;$UsernameColorCopyWith<$Res>? get usernameColor;
|
||||||
|
|
||||||
}
|
}
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@@ -675,7 +949,7 @@ class _$SnAccountProfileCopyWithImpl<$Res>
|
|||||||
|
|
||||||
/// Create a copy of SnAccountProfile
|
/// Create a copy of SnAccountProfile
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? firstName = null,Object? middleName = null,Object? lastName = null,Object? bio = null,Object? gender = null,Object? pronouns = null,Object? location = null,Object? timeZone = null,Object? birthday = freezed,Object? links = null,Object? lastSeenAt = freezed,Object? activeBadge = freezed,Object? experience = null,Object? level = null,Object? socialCredits = null,Object? socialCreditsLevel = null,Object? levelingProgress = null,Object? picture = freezed,Object? background = freezed,Object? verification = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? firstName = null,Object? middleName = null,Object? lastName = null,Object? bio = null,Object? gender = null,Object? pronouns = null,Object? location = null,Object? timeZone = null,Object? birthday = freezed,Object? links = null,Object? lastSeenAt = freezed,Object? activeBadge = freezed,Object? experience = null,Object? level = null,Object? socialCredits = null,Object? socialCreditsLevel = null,Object? levelingProgress = null,Object? picture = freezed,Object? background = freezed,Object? verification = freezed,Object? usernameColor = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||||
return _then(_self.copyWith(
|
return _then(_self.copyWith(
|
||||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||||
as String,firstName: null == firstName ? _self.firstName : firstName // ignore: cast_nullable_to_non_nullable
|
as String,firstName: null == firstName ? _self.firstName : firstName // ignore: cast_nullable_to_non_nullable
|
||||||
@@ -698,7 +972,8 @@ as int,levelingProgress: null == levelingProgress ? _self.levelingProgress : lev
|
|||||||
as double,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable
|
as double,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable
|
||||||
as SnCloudFile?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable
|
as SnCloudFile?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable
|
||||||
as SnCloudFile?,verification: freezed == verification ? _self.verification : verification // ignore: cast_nullable_to_non_nullable
|
as SnCloudFile?,verification: freezed == verification ? _self.verification : verification // ignore: cast_nullable_to_non_nullable
|
||||||
as SnVerificationMark?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
as SnVerificationMark?,usernameColor: freezed == usernameColor ? _self.usernameColor : usernameColor // ignore: cast_nullable_to_non_nullable
|
||||||
|
as UsernameColor?,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,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,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
|
||||||
as DateTime?,
|
as DateTime?,
|
||||||
@@ -752,6 +1027,18 @@ $SnVerificationMarkCopyWith<$Res>? get verification {
|
|||||||
return $SnVerificationMarkCopyWith<$Res>(_self.verification!, (value) {
|
return $SnVerificationMarkCopyWith<$Res>(_self.verification!, (value) {
|
||||||
return _then(_self.copyWith(verification: value));
|
return _then(_self.copyWith(verification: value));
|
||||||
});
|
});
|
||||||
|
}/// Create a copy of SnAccountProfile
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$UsernameColorCopyWith<$Res>? get usernameColor {
|
||||||
|
if (_self.usernameColor == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $UsernameColorCopyWith<$Res>(_self.usernameColor!, (value) {
|
||||||
|
return _then(_self.copyWith(usernameColor: value));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -831,10 +1118,10 @@ return $default(_that);case _:
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, @ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
|
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, @ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, UsernameColor? usernameColor, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _SnAccountProfile() when $default != null:
|
case _SnAccountProfile() when $default != null:
|
||||||
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.socialCredits,_that.socialCreditsLevel,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.socialCredits,_that.socialCreditsLevel,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.usernameColor,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||||
return orElse();
|
return orElse();
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -852,10 +1139,10 @@ return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.b
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, @ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
|
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, @ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, UsernameColor? usernameColor, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _SnAccountProfile():
|
case _SnAccountProfile():
|
||||||
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.socialCredits,_that.socialCreditsLevel,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);}
|
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.socialCredits,_that.socialCreditsLevel,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.usernameColor,_that.createdAt,_that.updatedAt,_that.deletedAt);}
|
||||||
}
|
}
|
||||||
/// A variant of `when` that fallback to returning `null`
|
/// A variant of `when` that fallback to returning `null`
|
||||||
///
|
///
|
||||||
@@ -869,10 +1156,10 @@ return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.b
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, @ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
|
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, @ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, UsernameColor? usernameColor, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _SnAccountProfile() when $default != null:
|
case _SnAccountProfile() when $default != null:
|
||||||
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.socialCredits,_that.socialCreditsLevel,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.socialCredits,_that.socialCreditsLevel,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.usernameColor,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -884,7 +1171,7 @@ return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.b
|
|||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
|
|
||||||
class _SnAccountProfile implements SnAccountProfile {
|
class _SnAccountProfile implements SnAccountProfile {
|
||||||
const _SnAccountProfile({required this.id, this.firstName = '', this.middleName = '', this.lastName = '', this.bio = '', this.gender = '', this.pronouns = '', this.location = '', this.timeZone = '', this.birthday, @ProfileLinkConverter() final List<ProfileLink> links = const [], this.lastSeenAt, this.activeBadge, required this.experience, required this.level, this.socialCredits = 100, this.socialCreditsLevel = 0, required this.levelingProgress, required this.picture, required this.background, required this.verification, required this.createdAt, required this.updatedAt, required this.deletedAt}): _links = links;
|
const _SnAccountProfile({required this.id, this.firstName = '', this.middleName = '', this.lastName = '', this.bio = '', this.gender = '', this.pronouns = '', this.location = '', this.timeZone = '', this.birthday, @ProfileLinkConverter() final List<ProfileLink> links = const [], this.lastSeenAt, this.activeBadge, required this.experience, required this.level, this.socialCredits = 100, this.socialCreditsLevel = 0, required this.levelingProgress, required this.picture, required this.background, required this.verification, this.usernameColor, required this.createdAt, required this.updatedAt, required this.deletedAt}): _links = links;
|
||||||
factory _SnAccountProfile.fromJson(Map<String, dynamic> json) => _$SnAccountProfileFromJson(json);
|
factory _SnAccountProfile.fromJson(Map<String, dynamic> json) => _$SnAccountProfileFromJson(json);
|
||||||
|
|
||||||
@override final String id;
|
@override final String id;
|
||||||
@@ -914,6 +1201,7 @@ class _SnAccountProfile implements SnAccountProfile {
|
|||||||
@override final SnCloudFile? picture;
|
@override final SnCloudFile? picture;
|
||||||
@override final SnCloudFile? background;
|
@override final SnCloudFile? background;
|
||||||
@override final SnVerificationMark? verification;
|
@override final SnVerificationMark? verification;
|
||||||
|
@override final UsernameColor? usernameColor;
|
||||||
@override final DateTime createdAt;
|
@override final DateTime createdAt;
|
||||||
@override final DateTime updatedAt;
|
@override final DateTime updatedAt;
|
||||||
@override final DateTime? deletedAt;
|
@override final DateTime? deletedAt;
|
||||||
@@ -931,16 +1219,16 @@ Map<String, dynamic> toJson() {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAccountProfile&&(identical(other.id, id) || other.id == id)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.middleName, middleName) || other.middleName == middleName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.gender, gender) || other.gender == gender)&&(identical(other.pronouns, pronouns) || other.pronouns == pronouns)&&(identical(other.location, location) || other.location == location)&&(identical(other.timeZone, timeZone) || other.timeZone == timeZone)&&(identical(other.birthday, birthday) || other.birthday == birthday)&&const DeepCollectionEquality().equals(other._links, _links)&&(identical(other.lastSeenAt, lastSeenAt) || other.lastSeenAt == lastSeenAt)&&(identical(other.activeBadge, activeBadge) || other.activeBadge == activeBadge)&&(identical(other.experience, experience) || other.experience == experience)&&(identical(other.level, level) || other.level == level)&&(identical(other.socialCredits, socialCredits) || other.socialCredits == socialCredits)&&(identical(other.socialCreditsLevel, socialCreditsLevel) || other.socialCreditsLevel == socialCreditsLevel)&&(identical(other.levelingProgress, levelingProgress) || other.levelingProgress == levelingProgress)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.verification, verification) || other.verification == verification)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAccountProfile&&(identical(other.id, id) || other.id == id)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.middleName, middleName) || other.middleName == middleName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.gender, gender) || other.gender == gender)&&(identical(other.pronouns, pronouns) || other.pronouns == pronouns)&&(identical(other.location, location) || other.location == location)&&(identical(other.timeZone, timeZone) || other.timeZone == timeZone)&&(identical(other.birthday, birthday) || other.birthday == birthday)&&const DeepCollectionEquality().equals(other._links, _links)&&(identical(other.lastSeenAt, lastSeenAt) || other.lastSeenAt == lastSeenAt)&&(identical(other.activeBadge, activeBadge) || other.activeBadge == activeBadge)&&(identical(other.experience, experience) || other.experience == experience)&&(identical(other.level, level) || other.level == level)&&(identical(other.socialCredits, socialCredits) || other.socialCredits == socialCredits)&&(identical(other.socialCreditsLevel, socialCreditsLevel) || other.socialCreditsLevel == socialCreditsLevel)&&(identical(other.levelingProgress, levelingProgress) || other.levelingProgress == levelingProgress)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.verification, verification) || other.verification == verification)&&(identical(other.usernameColor, usernameColor) || other.usernameColor == usernameColor)&&(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)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hashAll([runtimeType,id,firstName,middleName,lastName,bio,gender,pronouns,location,timeZone,birthday,const DeepCollectionEquality().hash(_links),lastSeenAt,activeBadge,experience,level,socialCredits,socialCreditsLevel,levelingProgress,picture,background,verification,createdAt,updatedAt,deletedAt]);
|
int get hashCode => Object.hashAll([runtimeType,id,firstName,middleName,lastName,bio,gender,pronouns,location,timeZone,birthday,const DeepCollectionEquality().hash(_links),lastSeenAt,activeBadge,experience,level,socialCredits,socialCreditsLevel,levelingProgress,picture,background,verification,usernameColor,createdAt,updatedAt,deletedAt]);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'SnAccountProfile(id: $id, firstName: $firstName, middleName: $middleName, lastName: $lastName, bio: $bio, gender: $gender, pronouns: $pronouns, location: $location, timeZone: $timeZone, birthday: $birthday, links: $links, lastSeenAt: $lastSeenAt, activeBadge: $activeBadge, experience: $experience, level: $level, socialCredits: $socialCredits, socialCreditsLevel: $socialCreditsLevel, levelingProgress: $levelingProgress, picture: $picture, background: $background, verification: $verification, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
return 'SnAccountProfile(id: $id, firstName: $firstName, middleName: $middleName, lastName: $lastName, bio: $bio, gender: $gender, pronouns: $pronouns, location: $location, timeZone: $timeZone, birthday: $birthday, links: $links, lastSeenAt: $lastSeenAt, activeBadge: $activeBadge, experience: $experience, level: $level, socialCredits: $socialCredits, socialCreditsLevel: $socialCreditsLevel, levelingProgress: $levelingProgress, picture: $picture, background: $background, verification: $verification, usernameColor: $usernameColor, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -951,11 +1239,11 @@ abstract mixin class _$SnAccountProfileCopyWith<$Res> implements $SnAccountProfi
|
|||||||
factory _$SnAccountProfileCopyWith(_SnAccountProfile value, $Res Function(_SnAccountProfile) _then) = __$SnAccountProfileCopyWithImpl;
|
factory _$SnAccountProfileCopyWith(_SnAccountProfile value, $Res Function(_SnAccountProfile) _then) = __$SnAccountProfileCopyWithImpl;
|
||||||
@override @useResult
|
@override @useResult
|
||||||
$Res call({
|
$Res call({
|
||||||
String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday,@ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday,@ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, UsernameColor? usernameColor, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@override $SnAccountBadgeCopyWith<$Res>? get activeBadge;@override $SnCloudFileCopyWith<$Res>? get picture;@override $SnCloudFileCopyWith<$Res>? get background;@override $SnVerificationMarkCopyWith<$Res>? get verification;
|
@override $SnAccountBadgeCopyWith<$Res>? get activeBadge;@override $SnCloudFileCopyWith<$Res>? get picture;@override $SnCloudFileCopyWith<$Res>? get background;@override $SnVerificationMarkCopyWith<$Res>? get verification;@override $UsernameColorCopyWith<$Res>? get usernameColor;
|
||||||
|
|
||||||
}
|
}
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@@ -968,7 +1256,7 @@ class __$SnAccountProfileCopyWithImpl<$Res>
|
|||||||
|
|
||||||
/// Create a copy of SnAccountProfile
|
/// Create a copy of SnAccountProfile
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? firstName = null,Object? middleName = null,Object? lastName = null,Object? bio = null,Object? gender = null,Object? pronouns = null,Object? location = null,Object? timeZone = null,Object? birthday = freezed,Object? links = null,Object? lastSeenAt = freezed,Object? activeBadge = freezed,Object? experience = null,Object? level = null,Object? socialCredits = null,Object? socialCreditsLevel = null,Object? levelingProgress = null,Object? picture = freezed,Object? background = freezed,Object? verification = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? firstName = null,Object? middleName = null,Object? lastName = null,Object? bio = null,Object? gender = null,Object? pronouns = null,Object? location = null,Object? timeZone = null,Object? birthday = freezed,Object? links = null,Object? lastSeenAt = freezed,Object? activeBadge = freezed,Object? experience = null,Object? level = null,Object? socialCredits = null,Object? socialCreditsLevel = null,Object? levelingProgress = null,Object? picture = freezed,Object? background = freezed,Object? verification = freezed,Object? usernameColor = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||||
return _then(_SnAccountProfile(
|
return _then(_SnAccountProfile(
|
||||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||||
as String,firstName: null == firstName ? _self.firstName : firstName // ignore: cast_nullable_to_non_nullable
|
as String,firstName: null == firstName ? _self.firstName : firstName // ignore: cast_nullable_to_non_nullable
|
||||||
@@ -991,7 +1279,8 @@ as int,levelingProgress: null == levelingProgress ? _self.levelingProgress : lev
|
|||||||
as double,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable
|
as double,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable
|
||||||
as SnCloudFile?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable
|
as SnCloudFile?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable
|
||||||
as SnCloudFile?,verification: freezed == verification ? _self.verification : verification // ignore: cast_nullable_to_non_nullable
|
as SnCloudFile?,verification: freezed == verification ? _self.verification : verification // ignore: cast_nullable_to_non_nullable
|
||||||
as SnVerificationMark?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
as SnVerificationMark?,usernameColor: freezed == usernameColor ? _self.usernameColor : usernameColor // ignore: cast_nullable_to_non_nullable
|
||||||
|
as UsernameColor?,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,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,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
|
||||||
as DateTime?,
|
as DateTime?,
|
||||||
@@ -1046,6 +1335,18 @@ $SnVerificationMarkCopyWith<$Res>? get verification {
|
|||||||
return $SnVerificationMarkCopyWith<$Res>(_self.verification!, (value) {
|
return $SnVerificationMarkCopyWith<$Res>(_self.verification!, (value) {
|
||||||
return _then(_self.copyWith(verification: value));
|
return _then(_self.copyWith(verification: value));
|
||||||
});
|
});
|
||||||
|
}/// Create a copy of SnAccountProfile
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$UsernameColorCopyWith<$Res>? get usernameColor {
|
||||||
|
if (_self.usernameColor == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $UsernameColorCopyWith<$Res>(_self.usernameColor!, (value) {
|
||||||
|
return _then(_self.copyWith(usernameColor: value));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,23 @@ _ProfileLink _$ProfileLinkFromJson(Map<String, dynamic> json) =>
|
|||||||
Map<String, dynamic> _$ProfileLinkToJson(_ProfileLink instance) =>
|
Map<String, dynamic> _$ProfileLinkToJson(_ProfileLink instance) =>
|
||||||
<String, dynamic>{'name': instance.name, 'url': instance.url};
|
<String, dynamic>{'name': instance.name, 'url': instance.url};
|
||||||
|
|
||||||
|
_UsernameColor _$UsernameColorFromJson(Map<String, dynamic> json) =>
|
||||||
|
_UsernameColor(
|
||||||
|
type: json['type'] as String? ?? 'plain',
|
||||||
|
value: json['value'] as String?,
|
||||||
|
direction: json['direction'] as String?,
|
||||||
|
colors:
|
||||||
|
(json['colors'] as List<dynamic>?)?.map((e) => e as String).toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$UsernameColorToJson(_UsernameColor instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'type': instance.type,
|
||||||
|
'value': instance.value,
|
||||||
|
'direction': instance.direction,
|
||||||
|
'colors': instance.colors,
|
||||||
|
};
|
||||||
|
|
||||||
_SnAccountProfile _$SnAccountProfileFromJson(Map<String, dynamic> json) =>
|
_SnAccountProfile _$SnAccountProfileFromJson(Map<String, dynamic> json) =>
|
||||||
_SnAccountProfile(
|
_SnAccountProfile(
|
||||||
id: json['id'] as String,
|
id: json['id'] as String,
|
||||||
@@ -113,6 +130,12 @@ _SnAccountProfile _$SnAccountProfileFromJson(Map<String, dynamic> json) =>
|
|||||||
: SnVerificationMark.fromJson(
|
: SnVerificationMark.fromJson(
|
||||||
json['verification'] as Map<String, dynamic>,
|
json['verification'] as Map<String, dynamic>,
|
||||||
),
|
),
|
||||||
|
usernameColor:
|
||||||
|
json['username_color'] == null
|
||||||
|
? null
|
||||||
|
: UsernameColor.fromJson(
|
||||||
|
json['username_color'] as Map<String, dynamic>,
|
||||||
|
),
|
||||||
createdAt: DateTime.parse(json['created_at'] as String),
|
createdAt: DateTime.parse(json['created_at'] as String),
|
||||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||||
deletedAt:
|
deletedAt:
|
||||||
@@ -144,6 +167,7 @@ Map<String, dynamic> _$SnAccountProfileToJson(_SnAccountProfile instance) =>
|
|||||||
'picture': instance.picture?.toJson(),
|
'picture': instance.picture?.toJson(),
|
||||||
'background': instance.background?.toJson(),
|
'background': instance.background?.toJson(),
|
||||||
'verification': instance.verification?.toJson(),
|
'verification': instance.verification?.toJson(),
|
||||||
|
'username_color': instance.usernameColor?.toJson(),
|
||||||
'created_at': instance.createdAt.toIso8601String(),
|
'created_at': instance.createdAt.toIso8601String(),
|
||||||
'updated_at': instance.updatedAt.toIso8601String(),
|
'updated_at': instance.updatedAt.toIso8601String(),
|
||||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||||
|
|||||||
16
lib/models/autocomplete_response.dart
Normal file
16
lib/models/autocomplete_response.dart
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'autocomplete_response.freezed.dart';
|
||||||
|
part 'autocomplete_response.g.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
sealed class AutocompleteSuggestion with _$AutocompleteSuggestion {
|
||||||
|
const factory AutocompleteSuggestion({
|
||||||
|
required String type,
|
||||||
|
required String keyword,
|
||||||
|
required dynamic data,
|
||||||
|
}) = _AutocompleteSuggestion;
|
||||||
|
|
||||||
|
factory AutocompleteSuggestion.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$AutocompleteSuggestionFromJson(json);
|
||||||
|
}
|
||||||
277
lib/models/autocomplete_response.freezed.dart
Normal file
277
lib/models/autocomplete_response.freezed.dart
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
// 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 'autocomplete_response.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// FreezedGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// dart format off
|
||||||
|
T _$identity<T>(T value) => value;
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$AutocompleteSuggestion {
|
||||||
|
|
||||||
|
String get type; String get keyword; dynamic get data;
|
||||||
|
/// Create a copy of AutocompleteSuggestion
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$AutocompleteSuggestionCopyWith<AutocompleteSuggestion> get copyWith => _$AutocompleteSuggestionCopyWithImpl<AutocompleteSuggestion>(this as AutocompleteSuggestion, _$identity);
|
||||||
|
|
||||||
|
/// Serializes this AutocompleteSuggestion to a JSON map.
|
||||||
|
Map<String, dynamic> toJson();
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is AutocompleteSuggestion&&(identical(other.type, type) || other.type == type)&&(identical(other.keyword, keyword) || other.keyword == keyword)&&const DeepCollectionEquality().equals(other.data, data));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,type,keyword,const DeepCollectionEquality().hash(data));
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'AutocompleteSuggestion(type: $type, keyword: $keyword, data: $data)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class $AutocompleteSuggestionCopyWith<$Res> {
|
||||||
|
factory $AutocompleteSuggestionCopyWith(AutocompleteSuggestion value, $Res Function(AutocompleteSuggestion) _then) = _$AutocompleteSuggestionCopyWithImpl;
|
||||||
|
@useResult
|
||||||
|
$Res call({
|
||||||
|
String type, String keyword, dynamic data
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class _$AutocompleteSuggestionCopyWithImpl<$Res>
|
||||||
|
implements $AutocompleteSuggestionCopyWith<$Res> {
|
||||||
|
_$AutocompleteSuggestionCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final AutocompleteSuggestion _self;
|
||||||
|
final $Res Function(AutocompleteSuggestion) _then;
|
||||||
|
|
||||||
|
/// Create a copy of AutocompleteSuggestion
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline') @override $Res call({Object? type = null,Object? keyword = null,Object? data = freezed,}) {
|
||||||
|
return _then(_self.copyWith(
|
||||||
|
type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,keyword: null == keyword ? _self.keyword : keyword // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
|
||||||
|
as dynamic,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Adds pattern-matching-related methods to [AutocompleteSuggestion].
|
||||||
|
extension AutocompleteSuggestionPatterns on AutocompleteSuggestion {
|
||||||
|
/// 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( _AutocompleteSuggestion value)? $default,{required TResult orElse(),}){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _AutocompleteSuggestion() 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( _AutocompleteSuggestion value) $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _AutocompleteSuggestion():
|
||||||
|
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( _AutocompleteSuggestion value)? $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _AutocompleteSuggestion() 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 type, String keyword, dynamic data)? $default,{required TResult orElse(),}) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _AutocompleteSuggestion() when $default != null:
|
||||||
|
return $default(_that.type,_that.keyword,_that.data);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 type, String keyword, dynamic data) $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _AutocompleteSuggestion():
|
||||||
|
return $default(_that.type,_that.keyword,_that.data);}
|
||||||
|
}
|
||||||
|
/// 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 type, String keyword, dynamic data)? $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _AutocompleteSuggestion() when $default != null:
|
||||||
|
return $default(_that.type,_that.keyword,_that.data);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
|
||||||
|
class _AutocompleteSuggestion implements AutocompleteSuggestion {
|
||||||
|
const _AutocompleteSuggestion({required this.type, required this.keyword, required this.data});
|
||||||
|
factory _AutocompleteSuggestion.fromJson(Map<String, dynamic> json) => _$AutocompleteSuggestionFromJson(json);
|
||||||
|
|
||||||
|
@override final String type;
|
||||||
|
@override final String keyword;
|
||||||
|
@override final dynamic data;
|
||||||
|
|
||||||
|
/// Create a copy of AutocompleteSuggestion
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$AutocompleteSuggestionCopyWith<_AutocompleteSuggestion> get copyWith => __$AutocompleteSuggestionCopyWithImpl<_AutocompleteSuggestion>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$AutocompleteSuggestionToJson(this, );
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AutocompleteSuggestion&&(identical(other.type, type) || other.type == type)&&(identical(other.keyword, keyword) || other.keyword == keyword)&&const DeepCollectionEquality().equals(other.data, data));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,type,keyword,const DeepCollectionEquality().hash(data));
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'AutocompleteSuggestion(type: $type, keyword: $keyword, data: $data)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class _$AutocompleteSuggestionCopyWith<$Res> implements $AutocompleteSuggestionCopyWith<$Res> {
|
||||||
|
factory _$AutocompleteSuggestionCopyWith(_AutocompleteSuggestion value, $Res Function(_AutocompleteSuggestion) _then) = __$AutocompleteSuggestionCopyWithImpl;
|
||||||
|
@override @useResult
|
||||||
|
$Res call({
|
||||||
|
String type, String keyword, dynamic data
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class __$AutocompleteSuggestionCopyWithImpl<$Res>
|
||||||
|
implements _$AutocompleteSuggestionCopyWith<$Res> {
|
||||||
|
__$AutocompleteSuggestionCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final _AutocompleteSuggestion _self;
|
||||||
|
final $Res Function(_AutocompleteSuggestion) _then;
|
||||||
|
|
||||||
|
/// Create a copy of AutocompleteSuggestion
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @pragma('vm:prefer-inline') $Res call({Object? type = null,Object? keyword = null,Object? data = freezed,}) {
|
||||||
|
return _then(_AutocompleteSuggestion(
|
||||||
|
type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,keyword: null == keyword ? _self.keyword : keyword // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
|
||||||
|
as dynamic,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// dart format on
|
||||||
23
lib/models/autocomplete_response.g.dart
Normal file
23
lib/models/autocomplete_response.g.dart
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'autocomplete_response.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
_AutocompleteSuggestion _$AutocompleteSuggestionFromJson(
|
||||||
|
Map<String, dynamic> json,
|
||||||
|
) => _AutocompleteSuggestion(
|
||||||
|
type: json['type'] as String,
|
||||||
|
keyword: json['keyword'] as String,
|
||||||
|
data: json['data'],
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$AutocompleteSuggestionToJson(
|
||||||
|
_AutocompleteSuggestion instance,
|
||||||
|
) => <String, dynamic>{
|
||||||
|
'type': instance.type,
|
||||||
|
'keyword': instance.keyword,
|
||||||
|
'data': instance.data,
|
||||||
|
};
|
||||||
@@ -149,6 +149,8 @@ sealed class CallParticipant with _$CallParticipant {
|
|||||||
const factory CallParticipant({
|
const factory CallParticipant({
|
||||||
required String identity,
|
required String identity,
|
||||||
required String name,
|
required String name,
|
||||||
|
required String accountId,
|
||||||
|
@Default(null) SnAccount? account,
|
||||||
required DateTime joinedAt,
|
required DateTime joinedAt,
|
||||||
}) = _CallParticipant;
|
}) = _CallParticipant;
|
||||||
|
|
||||||
|
|||||||
@@ -2241,7 +2241,7 @@ as List<CallParticipant>,
|
|||||||
/// @nodoc
|
/// @nodoc
|
||||||
mixin _$CallParticipant {
|
mixin _$CallParticipant {
|
||||||
|
|
||||||
String get identity; String get name; DateTime get joinedAt;
|
String get identity; String get name; String get accountId; SnAccount? get account; DateTime get joinedAt;
|
||||||
/// Create a copy of CallParticipant
|
/// Create a copy of CallParticipant
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@@ -2254,16 +2254,16 @@ $CallParticipantCopyWith<CallParticipant> get copyWith => _$CallParticipantCopyW
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is CallParticipant&&(identical(other.identity, identity) || other.identity == identity)&&(identical(other.name, name) || other.name == name)&&(identical(other.joinedAt, joinedAt) || other.joinedAt == joinedAt));
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is CallParticipant&&(identical(other.identity, identity) || other.identity == identity)&&(identical(other.name, name) || other.name == name)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.account, account) || other.account == account)&&(identical(other.joinedAt, joinedAt) || other.joinedAt == joinedAt));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType,identity,name,joinedAt);
|
int get hashCode => Object.hash(runtimeType,identity,name,accountId,account,joinedAt);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'CallParticipant(identity: $identity, name: $name, joinedAt: $joinedAt)';
|
return 'CallParticipant(identity: $identity, name: $name, accountId: $accountId, account: $account, joinedAt: $joinedAt)';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -2274,11 +2274,11 @@ abstract mixin class $CallParticipantCopyWith<$Res> {
|
|||||||
factory $CallParticipantCopyWith(CallParticipant value, $Res Function(CallParticipant) _then) = _$CallParticipantCopyWithImpl;
|
factory $CallParticipantCopyWith(CallParticipant value, $Res Function(CallParticipant) _then) = _$CallParticipantCopyWithImpl;
|
||||||
@useResult
|
@useResult
|
||||||
$Res call({
|
$Res call({
|
||||||
String identity, String name, DateTime joinedAt
|
String identity, String name, String accountId, SnAccount? account, DateTime joinedAt
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
$SnAccountCopyWith<$Res>? get account;
|
||||||
|
|
||||||
}
|
}
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@@ -2291,15 +2291,29 @@ class _$CallParticipantCopyWithImpl<$Res>
|
|||||||
|
|
||||||
/// Create a copy of CallParticipant
|
/// Create a copy of CallParticipant
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@pragma('vm:prefer-inline') @override $Res call({Object? identity = null,Object? name = null,Object? joinedAt = null,}) {
|
@pragma('vm:prefer-inline') @override $Res call({Object? identity = null,Object? name = null,Object? accountId = null,Object? account = freezed,Object? joinedAt = null,}) {
|
||||||
return _then(_self.copyWith(
|
return _then(_self.copyWith(
|
||||||
identity: null == identity ? _self.identity : identity // ignore: cast_nullable_to_non_nullable
|
identity: null == identity ? _self.identity : identity // ignore: cast_nullable_to_non_nullable
|
||||||
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||||
as String,joinedAt: null == joinedAt ? _self.joinedAt : joinedAt // ignore: cast_nullable_to_non_nullable
|
as String,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,account: freezed == account ? _self.account : account // ignore: cast_nullable_to_non_nullable
|
||||||
|
as SnAccount?,joinedAt: null == joinedAt ? _self.joinedAt : joinedAt // ignore: cast_nullable_to_non_nullable
|
||||||
as DateTime,
|
as DateTime,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
/// Create a copy of CallParticipant
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnAccountCopyWith<$Res>? get account {
|
||||||
|
if (_self.account == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $SnAccountCopyWith<$Res>(_self.account!, (value) {
|
||||||
|
return _then(_self.copyWith(account: value));
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -2378,10 +2392,10 @@ return $default(_that);case _:
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String identity, String name, DateTime joinedAt)? $default,{required TResult orElse(),}) {final _that = this;
|
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String identity, String name, String accountId, SnAccount? account, DateTime joinedAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _CallParticipant() when $default != null:
|
case _CallParticipant() when $default != null:
|
||||||
return $default(_that.identity,_that.name,_that.joinedAt);case _:
|
return $default(_that.identity,_that.name,_that.accountId,_that.account,_that.joinedAt);case _:
|
||||||
return orElse();
|
return orElse();
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -2399,10 +2413,10 @@ return $default(_that.identity,_that.name,_that.joinedAt);case _:
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String identity, String name, DateTime joinedAt) $default,) {final _that = this;
|
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String identity, String name, String accountId, SnAccount? account, DateTime joinedAt) $default,) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _CallParticipant():
|
case _CallParticipant():
|
||||||
return $default(_that.identity,_that.name,_that.joinedAt);}
|
return $default(_that.identity,_that.name,_that.accountId,_that.account,_that.joinedAt);}
|
||||||
}
|
}
|
||||||
/// A variant of `when` that fallback to returning `null`
|
/// A variant of `when` that fallback to returning `null`
|
||||||
///
|
///
|
||||||
@@ -2416,10 +2430,10 @@ return $default(_that.identity,_that.name,_that.joinedAt);}
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String identity, String name, DateTime joinedAt)? $default,) {final _that = this;
|
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String identity, String name, String accountId, SnAccount? account, DateTime joinedAt)? $default,) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _CallParticipant() when $default != null:
|
case _CallParticipant() when $default != null:
|
||||||
return $default(_that.identity,_that.name,_that.joinedAt);case _:
|
return $default(_that.identity,_that.name,_that.accountId,_that.account,_that.joinedAt);case _:
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -2431,11 +2445,13 @@ return $default(_that.identity,_that.name,_that.joinedAt);case _:
|
|||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
|
|
||||||
class _CallParticipant implements CallParticipant {
|
class _CallParticipant implements CallParticipant {
|
||||||
const _CallParticipant({required this.identity, required this.name, required this.joinedAt});
|
const _CallParticipant({required this.identity, required this.name, required this.accountId, this.account = null, required this.joinedAt});
|
||||||
factory _CallParticipant.fromJson(Map<String, dynamic> json) => _$CallParticipantFromJson(json);
|
factory _CallParticipant.fromJson(Map<String, dynamic> json) => _$CallParticipantFromJson(json);
|
||||||
|
|
||||||
@override final String identity;
|
@override final String identity;
|
||||||
@override final String name;
|
@override final String name;
|
||||||
|
@override final String accountId;
|
||||||
|
@override@JsonKey() final SnAccount? account;
|
||||||
@override final DateTime joinedAt;
|
@override final DateTime joinedAt;
|
||||||
|
|
||||||
/// Create a copy of CallParticipant
|
/// Create a copy of CallParticipant
|
||||||
@@ -2451,16 +2467,16 @@ Map<String, dynamic> toJson() {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _CallParticipant&&(identical(other.identity, identity) || other.identity == identity)&&(identical(other.name, name) || other.name == name)&&(identical(other.joinedAt, joinedAt) || other.joinedAt == joinedAt));
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is _CallParticipant&&(identical(other.identity, identity) || other.identity == identity)&&(identical(other.name, name) || other.name == name)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.account, account) || other.account == account)&&(identical(other.joinedAt, joinedAt) || other.joinedAt == joinedAt));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType,identity,name,joinedAt);
|
int get hashCode => Object.hash(runtimeType,identity,name,accountId,account,joinedAt);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'CallParticipant(identity: $identity, name: $name, joinedAt: $joinedAt)';
|
return 'CallParticipant(identity: $identity, name: $name, accountId: $accountId, account: $account, joinedAt: $joinedAt)';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -2471,11 +2487,11 @@ abstract mixin class _$CallParticipantCopyWith<$Res> implements $CallParticipant
|
|||||||
factory _$CallParticipantCopyWith(_CallParticipant value, $Res Function(_CallParticipant) _then) = __$CallParticipantCopyWithImpl;
|
factory _$CallParticipantCopyWith(_CallParticipant value, $Res Function(_CallParticipant) _then) = __$CallParticipantCopyWithImpl;
|
||||||
@override @useResult
|
@override @useResult
|
||||||
$Res call({
|
$Res call({
|
||||||
String identity, String name, DateTime joinedAt
|
String identity, String name, String accountId, SnAccount? account, DateTime joinedAt
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
@override $SnAccountCopyWith<$Res>? get account;
|
||||||
|
|
||||||
}
|
}
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@@ -2488,16 +2504,30 @@ class __$CallParticipantCopyWithImpl<$Res>
|
|||||||
|
|
||||||
/// Create a copy of CallParticipant
|
/// Create a copy of CallParticipant
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@override @pragma('vm:prefer-inline') $Res call({Object? identity = null,Object? name = null,Object? joinedAt = null,}) {
|
@override @pragma('vm:prefer-inline') $Res call({Object? identity = null,Object? name = null,Object? accountId = null,Object? account = freezed,Object? joinedAt = null,}) {
|
||||||
return _then(_CallParticipant(
|
return _then(_CallParticipant(
|
||||||
identity: null == identity ? _self.identity : identity // ignore: cast_nullable_to_non_nullable
|
identity: null == identity ? _self.identity : identity // ignore: cast_nullable_to_non_nullable
|
||||||
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||||
as String,joinedAt: null == joinedAt ? _self.joinedAt : joinedAt // ignore: cast_nullable_to_non_nullable
|
as String,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,account: freezed == account ? _self.account : account // ignore: cast_nullable_to_non_nullable
|
||||||
|
as SnAccount?,joinedAt: null == joinedAt ? _self.joinedAt : joinedAt // ignore: cast_nullable_to_non_nullable
|
||||||
as DateTime,
|
as DateTime,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a copy of CallParticipant
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnAccountCopyWith<$Res>? get account {
|
||||||
|
if (_self.account == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $SnAccountCopyWith<$Res>(_self.account!, (value) {
|
||||||
|
return _then(_self.copyWith(account: value));
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -275,6 +275,11 @@ _CallParticipant _$CallParticipantFromJson(Map<String, dynamic> json) =>
|
|||||||
_CallParticipant(
|
_CallParticipant(
|
||||||
identity: json['identity'] as String,
|
identity: json['identity'] as String,
|
||||||
name: json['name'] as String,
|
name: json['name'] as String,
|
||||||
|
accountId: json['account_id'] as String,
|
||||||
|
account:
|
||||||
|
json['account'] == null
|
||||||
|
? null
|
||||||
|
: SnAccount.fromJson(json['account'] as Map<String, dynamic>),
|
||||||
joinedAt: DateTime.parse(json['joined_at'] as String),
|
joinedAt: DateTime.parse(json['joined_at'] as String),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -282,6 +287,8 @@ Map<String, dynamic> _$CallParticipantToJson(_CallParticipant instance) =>
|
|||||||
<String, dynamic>{
|
<String, dynamic>{
|
||||||
'identity': instance.identity,
|
'identity': instance.identity,
|
||||||
'name': instance.name,
|
'name': instance.name,
|
||||||
|
'account_id': instance.accountId,
|
||||||
|
'account': instance.account?.toJson(),
|
||||||
'joined_at': instance.joinedAt.toIso8601String(),
|
'joined_at': instance.joinedAt.toIso8601String(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ sealed class UniversalFile with _$UniversalFile {
|
|||||||
required dynamic data,
|
required dynamic data,
|
||||||
required UniversalFileType type,
|
required UniversalFileType type,
|
||||||
@Default(false) bool isLink,
|
@Default(false) bool isLink,
|
||||||
|
String? displayName,
|
||||||
}) = _UniversalFile;
|
}) = _UniversalFile;
|
||||||
|
|
||||||
factory UniversalFile.fromJson(Map<String, dynamic> json) =>
|
factory UniversalFile.fromJson(Map<String, dynamic> json) =>
|
||||||
@@ -31,6 +32,7 @@ sealed class UniversalFile with _$UniversalFile {
|
|||||||
'video' => UniversalFileType.video,
|
'video' => UniversalFileType.video,
|
||||||
_ => UniversalFileType.file,
|
_ => UniversalFileType.file,
|
||||||
},
|
},
|
||||||
|
displayName: attachment.name,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ T _$identity<T>(T value) => value;
|
|||||||
/// @nodoc
|
/// @nodoc
|
||||||
mixin _$UniversalFile {
|
mixin _$UniversalFile {
|
||||||
|
|
||||||
dynamic get data; UniversalFileType get type; bool get isLink;
|
dynamic get data; UniversalFileType get type; bool get isLink; String? get displayName;
|
||||||
/// Create a copy of UniversalFile
|
/// Create a copy of UniversalFile
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@@ -28,16 +28,16 @@ $UniversalFileCopyWith<UniversalFile> get copyWith => _$UniversalFileCopyWithImp
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is UniversalFile&&const DeepCollectionEquality().equals(other.data, data)&&(identical(other.type, type) || other.type == type)&&(identical(other.isLink, isLink) || other.isLink == isLink));
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is UniversalFile&&const DeepCollectionEquality().equals(other.data, data)&&(identical(other.type, type) || other.type == type)&&(identical(other.isLink, isLink) || other.isLink == isLink)&&(identical(other.displayName, displayName) || other.displayName == displayName));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(data),type,isLink);
|
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(data),type,isLink,displayName);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'UniversalFile(data: $data, type: $type, isLink: $isLink)';
|
return 'UniversalFile(data: $data, type: $type, isLink: $isLink, displayName: $displayName)';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ abstract mixin class $UniversalFileCopyWith<$Res> {
|
|||||||
factory $UniversalFileCopyWith(UniversalFile value, $Res Function(UniversalFile) _then) = _$UniversalFileCopyWithImpl;
|
factory $UniversalFileCopyWith(UniversalFile value, $Res Function(UniversalFile) _then) = _$UniversalFileCopyWithImpl;
|
||||||
@useResult
|
@useResult
|
||||||
$Res call({
|
$Res call({
|
||||||
dynamic data, UniversalFileType type, bool isLink
|
dynamic data, UniversalFileType type, bool isLink, String? displayName
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -65,12 +65,13 @@ class _$UniversalFileCopyWithImpl<$Res>
|
|||||||
|
|
||||||
/// Create a copy of UniversalFile
|
/// Create a copy of UniversalFile
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@pragma('vm:prefer-inline') @override $Res call({Object? data = freezed,Object? type = null,Object? isLink = null,}) {
|
@pragma('vm:prefer-inline') @override $Res call({Object? data = freezed,Object? type = null,Object? isLink = null,Object? displayName = freezed,}) {
|
||||||
return _then(_self.copyWith(
|
return _then(_self.copyWith(
|
||||||
data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
|
data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
|
||||||
as dynamic,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
|
as dynamic,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
|
||||||
as UniversalFileType,isLink: null == isLink ? _self.isLink : isLink // ignore: cast_nullable_to_non_nullable
|
as UniversalFileType,isLink: null == isLink ? _self.isLink : isLink // ignore: cast_nullable_to_non_nullable
|
||||||
as bool,
|
as bool,displayName: freezed == displayName ? _self.displayName : displayName // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,10 +153,10 @@ return $default(_that);case _:
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( dynamic data, UniversalFileType type, bool isLink)? $default,{required TResult orElse(),}) {final _that = this;
|
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( dynamic data, UniversalFileType type, bool isLink, String? displayName)? $default,{required TResult orElse(),}) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _UniversalFile() when $default != null:
|
case _UniversalFile() when $default != null:
|
||||||
return $default(_that.data,_that.type,_that.isLink);case _:
|
return $default(_that.data,_that.type,_that.isLink,_that.displayName);case _:
|
||||||
return orElse();
|
return orElse();
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -173,10 +174,10 @@ return $default(_that.data,_that.type,_that.isLink);case _:
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( dynamic data, UniversalFileType type, bool isLink) $default,) {final _that = this;
|
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( dynamic data, UniversalFileType type, bool isLink, String? displayName) $default,) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _UniversalFile():
|
case _UniversalFile():
|
||||||
return $default(_that.data,_that.type,_that.isLink);}
|
return $default(_that.data,_that.type,_that.isLink,_that.displayName);}
|
||||||
}
|
}
|
||||||
/// A variant of `when` that fallback to returning `null`
|
/// A variant of `when` that fallback to returning `null`
|
||||||
///
|
///
|
||||||
@@ -190,10 +191,10 @@ return $default(_that.data,_that.type,_that.isLink);}
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( dynamic data, UniversalFileType type, bool isLink)? $default,) {final _that = this;
|
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( dynamic data, UniversalFileType type, bool isLink, String? displayName)? $default,) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _UniversalFile() when $default != null:
|
case _UniversalFile() when $default != null:
|
||||||
return $default(_that.data,_that.type,_that.isLink);case _:
|
return $default(_that.data,_that.type,_that.isLink,_that.displayName);case _:
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -205,12 +206,13 @@ return $default(_that.data,_that.type,_that.isLink);case _:
|
|||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
|
|
||||||
class _UniversalFile extends UniversalFile {
|
class _UniversalFile extends UniversalFile {
|
||||||
const _UniversalFile({required this.data, required this.type, this.isLink = false}): super._();
|
const _UniversalFile({required this.data, required this.type, this.isLink = false, this.displayName}): super._();
|
||||||
factory _UniversalFile.fromJson(Map<String, dynamic> json) => _$UniversalFileFromJson(json);
|
factory _UniversalFile.fromJson(Map<String, dynamic> json) => _$UniversalFileFromJson(json);
|
||||||
|
|
||||||
@override final dynamic data;
|
@override final dynamic data;
|
||||||
@override final UniversalFileType type;
|
@override final UniversalFileType type;
|
||||||
@override@JsonKey() final bool isLink;
|
@override@JsonKey() final bool isLink;
|
||||||
|
@override final String? displayName;
|
||||||
|
|
||||||
/// Create a copy of UniversalFile
|
/// Create a copy of UniversalFile
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@@ -225,16 +227,16 @@ Map<String, dynamic> toJson() {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _UniversalFile&&const DeepCollectionEquality().equals(other.data, data)&&(identical(other.type, type) || other.type == type)&&(identical(other.isLink, isLink) || other.isLink == isLink));
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is _UniversalFile&&const DeepCollectionEquality().equals(other.data, data)&&(identical(other.type, type) || other.type == type)&&(identical(other.isLink, isLink) || other.isLink == isLink)&&(identical(other.displayName, displayName) || other.displayName == displayName));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(data),type,isLink);
|
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(data),type,isLink,displayName);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'UniversalFile(data: $data, type: $type, isLink: $isLink)';
|
return 'UniversalFile(data: $data, type: $type, isLink: $isLink, displayName: $displayName)';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -245,7 +247,7 @@ abstract mixin class _$UniversalFileCopyWith<$Res> implements $UniversalFileCopy
|
|||||||
factory _$UniversalFileCopyWith(_UniversalFile value, $Res Function(_UniversalFile) _then) = __$UniversalFileCopyWithImpl;
|
factory _$UniversalFileCopyWith(_UniversalFile value, $Res Function(_UniversalFile) _then) = __$UniversalFileCopyWithImpl;
|
||||||
@override @useResult
|
@override @useResult
|
||||||
$Res call({
|
$Res call({
|
||||||
dynamic data, UniversalFileType type, bool isLink
|
dynamic data, UniversalFileType type, bool isLink, String? displayName
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -262,12 +264,13 @@ class __$UniversalFileCopyWithImpl<$Res>
|
|||||||
|
|
||||||
/// Create a copy of UniversalFile
|
/// Create a copy of UniversalFile
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@override @pragma('vm:prefer-inline') $Res call({Object? data = freezed,Object? type = null,Object? isLink = null,}) {
|
@override @pragma('vm:prefer-inline') $Res call({Object? data = freezed,Object? type = null,Object? isLink = null,Object? displayName = freezed,}) {
|
||||||
return _then(_UniversalFile(
|
return _then(_UniversalFile(
|
||||||
data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
|
data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
|
||||||
as dynamic,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
|
as dynamic,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
|
||||||
as UniversalFileType,isLink: null == isLink ? _self.isLink : isLink // ignore: cast_nullable_to_non_nullable
|
as UniversalFileType,isLink: null == isLink ? _self.isLink : isLink // ignore: cast_nullable_to_non_nullable
|
||||||
as bool,
|
as bool,displayName: freezed == displayName ? _self.displayName : displayName // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ _UniversalFile _$UniversalFileFromJson(Map<String, dynamic> json) =>
|
|||||||
data: json['data'],
|
data: json['data'],
|
||||||
type: $enumDecode(_$UniversalFileTypeEnumMap, json['type']),
|
type: $enumDecode(_$UniversalFileTypeEnumMap, json['type']),
|
||||||
isLink: json['is_link'] as bool? ?? false,
|
isLink: json['is_link'] as bool? ?? false,
|
||||||
|
displayName: json['display_name'] as String?,
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$UniversalFileToJson(_UniversalFile instance) =>
|
Map<String, dynamic> _$UniversalFileToJson(_UniversalFile instance) =>
|
||||||
@@ -18,6 +19,7 @@ Map<String, dynamic> _$UniversalFileToJson(_UniversalFile instance) =>
|
|||||||
'data': instance.data,
|
'data': instance.data,
|
||||||
'type': _$UniversalFileTypeEnumMap[instance.type]!,
|
'type': _$UniversalFileTypeEnumMap[instance.type]!,
|
||||||
'is_link': instance.isLink,
|
'is_link': instance.isLink,
|
||||||
|
'display_name': instance.displayName,
|
||||||
};
|
};
|
||||||
|
|
||||||
const _$UniversalFileTypeEnumMap = {
|
const _$UniversalFileTypeEnumMap = {
|
||||||
|
|||||||
26
lib/models/heatmap.dart
Normal file
26
lib/models/heatmap.dart
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'heatmap.freezed.dart';
|
||||||
|
part 'heatmap.g.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
sealed class SnHeatmap with _$SnHeatmap {
|
||||||
|
const factory SnHeatmap({
|
||||||
|
required String unit,
|
||||||
|
@JsonKey(name: 'period_start') required DateTime periodStart,
|
||||||
|
@JsonKey(name: 'period_end') required DateTime periodEnd,
|
||||||
|
required List<SnHeatmapItem> items,
|
||||||
|
}) = _SnHeatmap;
|
||||||
|
|
||||||
|
factory SnHeatmap.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SnHeatmapFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
sealed class SnHeatmapItem with _$SnHeatmapItem {
|
||||||
|
const factory SnHeatmapItem({required DateTime date, required int count}) =
|
||||||
|
_SnHeatmapItem;
|
||||||
|
|
||||||
|
factory SnHeatmapItem.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SnHeatmapItemFromJson(json);
|
||||||
|
}
|
||||||
546
lib/models/heatmap.freezed.dart
Normal file
546
lib/models/heatmap.freezed.dart
Normal file
@@ -0,0 +1,546 @@
|
|||||||
|
// 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 'heatmap.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// FreezedGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// dart format off
|
||||||
|
T _$identity<T>(T value) => value;
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$SnHeatmap {
|
||||||
|
|
||||||
|
String get unit;@JsonKey(name: 'period_start') DateTime get periodStart;@JsonKey(name: 'period_end') DateTime get periodEnd; List<SnHeatmapItem> get items;
|
||||||
|
/// Create a copy of SnHeatmap
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnHeatmapCopyWith<SnHeatmap> get copyWith => _$SnHeatmapCopyWithImpl<SnHeatmap>(this as SnHeatmap, _$identity);
|
||||||
|
|
||||||
|
/// Serializes this SnHeatmap to a JSON map.
|
||||||
|
Map<String, dynamic> toJson();
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnHeatmap&&(identical(other.unit, unit) || other.unit == unit)&&(identical(other.periodStart, periodStart) || other.periodStart == periodStart)&&(identical(other.periodEnd, periodEnd) || other.periodEnd == periodEnd)&&const DeepCollectionEquality().equals(other.items, items));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,unit,periodStart,periodEnd,const DeepCollectionEquality().hash(items));
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SnHeatmap(unit: $unit, periodStart: $periodStart, periodEnd: $periodEnd, items: $items)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class $SnHeatmapCopyWith<$Res> {
|
||||||
|
factory $SnHeatmapCopyWith(SnHeatmap value, $Res Function(SnHeatmap) _then) = _$SnHeatmapCopyWithImpl;
|
||||||
|
@useResult
|
||||||
|
$Res call({
|
||||||
|
String unit,@JsonKey(name: 'period_start') DateTime periodStart,@JsonKey(name: 'period_end') DateTime periodEnd, List<SnHeatmapItem> items
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class _$SnHeatmapCopyWithImpl<$Res>
|
||||||
|
implements $SnHeatmapCopyWith<$Res> {
|
||||||
|
_$SnHeatmapCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final SnHeatmap _self;
|
||||||
|
final $Res Function(SnHeatmap) _then;
|
||||||
|
|
||||||
|
/// Create a copy of SnHeatmap
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline') @override $Res call({Object? unit = null,Object? periodStart = null,Object? periodEnd = null,Object? items = null,}) {
|
||||||
|
return _then(_self.copyWith(
|
||||||
|
unit: null == unit ? _self.unit : unit // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,periodStart: null == periodStart ? _self.periodStart : periodStart // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,periodEnd: null == periodEnd ? _self.periodEnd : periodEnd // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,items: null == items ? _self.items : items // ignore: cast_nullable_to_non_nullable
|
||||||
|
as List<SnHeatmapItem>,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Adds pattern-matching-related methods to [SnHeatmap].
|
||||||
|
extension SnHeatmapPatterns on SnHeatmap {
|
||||||
|
/// 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( _SnHeatmap value)? $default,{required TResult orElse(),}){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnHeatmap() 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( _SnHeatmap value) $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnHeatmap():
|
||||||
|
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( _SnHeatmap value)? $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnHeatmap() 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 unit, @JsonKey(name: 'period_start') DateTime periodStart, @JsonKey(name: 'period_end') DateTime periodEnd, List<SnHeatmapItem> items)? $default,{required TResult orElse(),}) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnHeatmap() when $default != null:
|
||||||
|
return $default(_that.unit,_that.periodStart,_that.periodEnd,_that.items);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 unit, @JsonKey(name: 'period_start') DateTime periodStart, @JsonKey(name: 'period_end') DateTime periodEnd, List<SnHeatmapItem> items) $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnHeatmap():
|
||||||
|
return $default(_that.unit,_that.periodStart,_that.periodEnd,_that.items);}
|
||||||
|
}
|
||||||
|
/// 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 unit, @JsonKey(name: 'period_start') DateTime periodStart, @JsonKey(name: 'period_end') DateTime periodEnd, List<SnHeatmapItem> items)? $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnHeatmap() when $default != null:
|
||||||
|
return $default(_that.unit,_that.periodStart,_that.periodEnd,_that.items);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
|
||||||
|
class _SnHeatmap implements SnHeatmap {
|
||||||
|
const _SnHeatmap({required this.unit, @JsonKey(name: 'period_start') required this.periodStart, @JsonKey(name: 'period_end') required this.periodEnd, required final List<SnHeatmapItem> items}): _items = items;
|
||||||
|
factory _SnHeatmap.fromJson(Map<String, dynamic> json) => _$SnHeatmapFromJson(json);
|
||||||
|
|
||||||
|
@override final String unit;
|
||||||
|
@override@JsonKey(name: 'period_start') final DateTime periodStart;
|
||||||
|
@override@JsonKey(name: 'period_end') final DateTime periodEnd;
|
||||||
|
final List<SnHeatmapItem> _items;
|
||||||
|
@override List<SnHeatmapItem> get items {
|
||||||
|
if (_items is EqualUnmodifiableListView) return _items;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableListView(_items);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Create a copy of SnHeatmap
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$SnHeatmapCopyWith<_SnHeatmap> get copyWith => __$SnHeatmapCopyWithImpl<_SnHeatmap>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$SnHeatmapToJson(this, );
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnHeatmap&&(identical(other.unit, unit) || other.unit == unit)&&(identical(other.periodStart, periodStart) || other.periodStart == periodStart)&&(identical(other.periodEnd, periodEnd) || other.periodEnd == periodEnd)&&const DeepCollectionEquality().equals(other._items, _items));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,unit,periodStart,periodEnd,const DeepCollectionEquality().hash(_items));
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SnHeatmap(unit: $unit, periodStart: $periodStart, periodEnd: $periodEnd, items: $items)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class _$SnHeatmapCopyWith<$Res> implements $SnHeatmapCopyWith<$Res> {
|
||||||
|
factory _$SnHeatmapCopyWith(_SnHeatmap value, $Res Function(_SnHeatmap) _then) = __$SnHeatmapCopyWithImpl;
|
||||||
|
@override @useResult
|
||||||
|
$Res call({
|
||||||
|
String unit,@JsonKey(name: 'period_start') DateTime periodStart,@JsonKey(name: 'period_end') DateTime periodEnd, List<SnHeatmapItem> items
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class __$SnHeatmapCopyWithImpl<$Res>
|
||||||
|
implements _$SnHeatmapCopyWith<$Res> {
|
||||||
|
__$SnHeatmapCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final _SnHeatmap _self;
|
||||||
|
final $Res Function(_SnHeatmap) _then;
|
||||||
|
|
||||||
|
/// Create a copy of SnHeatmap
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @pragma('vm:prefer-inline') $Res call({Object? unit = null,Object? periodStart = null,Object? periodEnd = null,Object? items = null,}) {
|
||||||
|
return _then(_SnHeatmap(
|
||||||
|
unit: null == unit ? _self.unit : unit // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,periodStart: null == periodStart ? _self.periodStart : periodStart // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,periodEnd: null == periodEnd ? _self.periodEnd : periodEnd // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,items: null == items ? _self._items : items // ignore: cast_nullable_to_non_nullable
|
||||||
|
as List<SnHeatmapItem>,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$SnHeatmapItem {
|
||||||
|
|
||||||
|
DateTime get date; int get count;
|
||||||
|
/// Create a copy of SnHeatmapItem
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnHeatmapItemCopyWith<SnHeatmapItem> get copyWith => _$SnHeatmapItemCopyWithImpl<SnHeatmapItem>(this as SnHeatmapItem, _$identity);
|
||||||
|
|
||||||
|
/// Serializes this SnHeatmapItem to a JSON map.
|
||||||
|
Map<String, dynamic> toJson();
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnHeatmapItem&&(identical(other.date, date) || other.date == date)&&(identical(other.count, count) || other.count == count));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,date,count);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SnHeatmapItem(date: $date, count: $count)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class $SnHeatmapItemCopyWith<$Res> {
|
||||||
|
factory $SnHeatmapItemCopyWith(SnHeatmapItem value, $Res Function(SnHeatmapItem) _then) = _$SnHeatmapItemCopyWithImpl;
|
||||||
|
@useResult
|
||||||
|
$Res call({
|
||||||
|
DateTime date, int count
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class _$SnHeatmapItemCopyWithImpl<$Res>
|
||||||
|
implements $SnHeatmapItemCopyWith<$Res> {
|
||||||
|
_$SnHeatmapItemCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final SnHeatmapItem _self;
|
||||||
|
final $Res Function(SnHeatmapItem) _then;
|
||||||
|
|
||||||
|
/// Create a copy of SnHeatmapItem
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline') @override $Res call({Object? date = null,Object? count = null,}) {
|
||||||
|
return _then(_self.copyWith(
|
||||||
|
date: null == date ? _self.date : date // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,count: null == count ? _self.count : count // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Adds pattern-matching-related methods to [SnHeatmapItem].
|
||||||
|
extension SnHeatmapItemPatterns on SnHeatmapItem {
|
||||||
|
/// 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( _SnHeatmapItem value)? $default,{required TResult orElse(),}){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnHeatmapItem() 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( _SnHeatmapItem value) $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnHeatmapItem():
|
||||||
|
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( _SnHeatmapItem value)? $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnHeatmapItem() 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( DateTime date, int count)? $default,{required TResult orElse(),}) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnHeatmapItem() when $default != null:
|
||||||
|
return $default(_that.date,_that.count);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( DateTime date, int count) $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnHeatmapItem():
|
||||||
|
return $default(_that.date,_that.count);}
|
||||||
|
}
|
||||||
|
/// 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( DateTime date, int count)? $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnHeatmapItem() when $default != null:
|
||||||
|
return $default(_that.date,_that.count);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
|
||||||
|
class _SnHeatmapItem implements SnHeatmapItem {
|
||||||
|
const _SnHeatmapItem({required this.date, required this.count});
|
||||||
|
factory _SnHeatmapItem.fromJson(Map<String, dynamic> json) => _$SnHeatmapItemFromJson(json);
|
||||||
|
|
||||||
|
@override final DateTime date;
|
||||||
|
@override final int count;
|
||||||
|
|
||||||
|
/// Create a copy of SnHeatmapItem
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$SnHeatmapItemCopyWith<_SnHeatmapItem> get copyWith => __$SnHeatmapItemCopyWithImpl<_SnHeatmapItem>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$SnHeatmapItemToJson(this, );
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnHeatmapItem&&(identical(other.date, date) || other.date == date)&&(identical(other.count, count) || other.count == count));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,date,count);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SnHeatmapItem(date: $date, count: $count)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class _$SnHeatmapItemCopyWith<$Res> implements $SnHeatmapItemCopyWith<$Res> {
|
||||||
|
factory _$SnHeatmapItemCopyWith(_SnHeatmapItem value, $Res Function(_SnHeatmapItem) _then) = __$SnHeatmapItemCopyWithImpl;
|
||||||
|
@override @useResult
|
||||||
|
$Res call({
|
||||||
|
DateTime date, int count
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class __$SnHeatmapItemCopyWithImpl<$Res>
|
||||||
|
implements _$SnHeatmapItemCopyWith<$Res> {
|
||||||
|
__$SnHeatmapItemCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final _SnHeatmapItem _self;
|
||||||
|
final $Res Function(_SnHeatmapItem) _then;
|
||||||
|
|
||||||
|
/// Create a copy of SnHeatmapItem
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @pragma('vm:prefer-inline') $Res call({Object? date = null,Object? count = null,}) {
|
||||||
|
return _then(_SnHeatmapItem(
|
||||||
|
date: null == date ? _self.date : date // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,count: null == count ? _self.count : count // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// dart format on
|
||||||
37
lib/models/heatmap.g.dart
Normal file
37
lib/models/heatmap.g.dart
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'heatmap.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
_SnHeatmap _$SnHeatmapFromJson(Map<String, dynamic> json) => _SnHeatmap(
|
||||||
|
unit: json['unit'] as String,
|
||||||
|
periodStart: DateTime.parse(json['period_start'] as String),
|
||||||
|
periodEnd: DateTime.parse(json['period_end'] as String),
|
||||||
|
items:
|
||||||
|
(json['items'] as List<dynamic>)
|
||||||
|
.map((e) => SnHeatmapItem.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$SnHeatmapToJson(_SnHeatmap instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'unit': instance.unit,
|
||||||
|
'period_start': instance.periodStart.toIso8601String(),
|
||||||
|
'period_end': instance.periodEnd.toIso8601String(),
|
||||||
|
'items': instance.items.map((e) => e.toJson()).toList(),
|
||||||
|
};
|
||||||
|
|
||||||
|
_SnHeatmapItem _$SnHeatmapItemFromJson(Map<String, dynamic> json) =>
|
||||||
|
_SnHeatmapItem(
|
||||||
|
date: DateTime.parse(json['date'] as String),
|
||||||
|
count: (json['count'] as num).toInt(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$SnHeatmapItemToJson(_SnHeatmapItem instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'date': instance.date.toIso8601String(),
|
||||||
|
'count': instance.count,
|
||||||
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
import 'package:island/models/account.dart';
|
||||||
import 'package:island/models/file.dart';
|
import 'package:island/models/file.dart';
|
||||||
import 'package:island/models/post_category.dart';
|
import 'package:island/models/post_category.dart';
|
||||||
import 'package:island/models/post_tag.dart';
|
import 'package:island/models/post_tag.dart';
|
||||||
@@ -142,3 +143,21 @@ sealed class SnPostAward with _$SnPostAward {
|
|||||||
factory SnPostAward.fromJson(Map<String, dynamic> json) =>
|
factory SnPostAward.fromJson(Map<String, dynamic> json) =>
|
||||||
_$SnPostAwardFromJson(json);
|
_$SnPostAwardFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
sealed class SnPostReaction with _$SnPostReaction {
|
||||||
|
const factory SnPostReaction({
|
||||||
|
required String id,
|
||||||
|
required String symbol,
|
||||||
|
required int attitude,
|
||||||
|
required String postId,
|
||||||
|
required String accountId,
|
||||||
|
required DateTime createdAt,
|
||||||
|
required DateTime updatedAt,
|
||||||
|
@Default(null) SnAccount? account,
|
||||||
|
DateTime? deletedAt,
|
||||||
|
}) = _SnPostReaction;
|
||||||
|
|
||||||
|
factory SnPostReaction.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SnPostReactionFromJson(json);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1900,4 +1900,309 @@ as DateTime?,
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$SnPostReaction {
|
||||||
|
|
||||||
|
String get id; String get symbol; int get attitude; String get postId; String get accountId; DateTime get createdAt; DateTime get updatedAt; SnAccount? get account; DateTime? get deletedAt;
|
||||||
|
/// Create a copy of SnPostReaction
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnPostReactionCopyWith<SnPostReaction> get copyWith => _$SnPostReactionCopyWithImpl<SnPostReaction>(this as SnPostReaction, _$identity);
|
||||||
|
|
||||||
|
/// Serializes this SnPostReaction to a JSON map.
|
||||||
|
Map<String, dynamic> toJson();
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPostReaction&&(identical(other.id, id) || other.id == id)&&(identical(other.symbol, symbol) || other.symbol == symbol)&&(identical(other.attitude, attitude) || other.attitude == attitude)&&(identical(other.postId, postId) || other.postId == postId)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.account, account) || other.account == account)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,id,symbol,attitude,postId,accountId,createdAt,updatedAt,account,deletedAt);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SnPostReaction(id: $id, symbol: $symbol, attitude: $attitude, postId: $postId, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, account: $account, deletedAt: $deletedAt)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class $SnPostReactionCopyWith<$Res> {
|
||||||
|
factory $SnPostReactionCopyWith(SnPostReaction value, $Res Function(SnPostReaction) _then) = _$SnPostReactionCopyWithImpl;
|
||||||
|
@useResult
|
||||||
|
$Res call({
|
||||||
|
String id, String symbol, int attitude, String postId, String accountId, DateTime createdAt, DateTime updatedAt, SnAccount? account, DateTime? deletedAt
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
$SnAccountCopyWith<$Res>? get account;
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class _$SnPostReactionCopyWithImpl<$Res>
|
||||||
|
implements $SnPostReactionCopyWith<$Res> {
|
||||||
|
_$SnPostReactionCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final SnPostReaction _self;
|
||||||
|
final $Res Function(SnPostReaction) _then;
|
||||||
|
|
||||||
|
/// Create a copy of SnPostReaction
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? symbol = null,Object? attitude = null,Object? postId = null,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? account = freezed,Object? deletedAt = freezed,}) {
|
||||||
|
return _then(_self.copyWith(
|
||||||
|
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,symbol: null == symbol ? _self.symbol : symbol // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,attitude: null == attitude ? _self.attitude : attitude // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,postId: null == postId ? _self.postId : postId // 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,account: freezed == account ? _self.account : account // ignore: cast_nullable_to_non_nullable
|
||||||
|
as SnAccount?,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
/// Create a copy of SnPostReaction
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnAccountCopyWith<$Res>? get account {
|
||||||
|
if (_self.account == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $SnAccountCopyWith<$Res>(_self.account!, (value) {
|
||||||
|
return _then(_self.copyWith(account: value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Adds pattern-matching-related methods to [SnPostReaction].
|
||||||
|
extension SnPostReactionPatterns on SnPostReaction {
|
||||||
|
/// 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( _SnPostReaction value)? $default,{required TResult orElse(),}){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnPostReaction() 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( _SnPostReaction value) $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnPostReaction():
|
||||||
|
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( _SnPostReaction value)? $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnPostReaction() 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 symbol, int attitude, String postId, String accountId, DateTime createdAt, DateTime updatedAt, SnAccount? account, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnPostReaction() when $default != null:
|
||||||
|
return $default(_that.id,_that.symbol,_that.attitude,_that.postId,_that.accountId,_that.createdAt,_that.updatedAt,_that.account,_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, String symbol, int attitude, String postId, String accountId, DateTime createdAt, DateTime updatedAt, SnAccount? account, DateTime? deletedAt) $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnPostReaction():
|
||||||
|
return $default(_that.id,_that.symbol,_that.attitude,_that.postId,_that.accountId,_that.createdAt,_that.updatedAt,_that.account,_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, String symbol, int attitude, String postId, String accountId, DateTime createdAt, DateTime updatedAt, SnAccount? account, DateTime? deletedAt)? $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnPostReaction() when $default != null:
|
||||||
|
return $default(_that.id,_that.symbol,_that.attitude,_that.postId,_that.accountId,_that.createdAt,_that.updatedAt,_that.account,_that.deletedAt);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
|
||||||
|
class _SnPostReaction implements SnPostReaction {
|
||||||
|
const _SnPostReaction({required this.id, required this.symbol, required this.attitude, required this.postId, required this.accountId, required this.createdAt, required this.updatedAt, this.account = null, this.deletedAt});
|
||||||
|
factory _SnPostReaction.fromJson(Map<String, dynamic> json) => _$SnPostReactionFromJson(json);
|
||||||
|
|
||||||
|
@override final String id;
|
||||||
|
@override final String symbol;
|
||||||
|
@override final int attitude;
|
||||||
|
@override final String postId;
|
||||||
|
@override final String accountId;
|
||||||
|
@override final DateTime createdAt;
|
||||||
|
@override final DateTime updatedAt;
|
||||||
|
@override@JsonKey() final SnAccount? account;
|
||||||
|
@override final DateTime? deletedAt;
|
||||||
|
|
||||||
|
/// Create a copy of SnPostReaction
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$SnPostReactionCopyWith<_SnPostReaction> get copyWith => __$SnPostReactionCopyWithImpl<_SnPostReaction>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$SnPostReactionToJson(this, );
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPostReaction&&(identical(other.id, id) || other.id == id)&&(identical(other.symbol, symbol) || other.symbol == symbol)&&(identical(other.attitude, attitude) || other.attitude == attitude)&&(identical(other.postId, postId) || other.postId == postId)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.account, account) || other.account == account)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,id,symbol,attitude,postId,accountId,createdAt,updatedAt,account,deletedAt);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SnPostReaction(id: $id, symbol: $symbol, attitude: $attitude, postId: $postId, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, account: $account, deletedAt: $deletedAt)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class _$SnPostReactionCopyWith<$Res> implements $SnPostReactionCopyWith<$Res> {
|
||||||
|
factory _$SnPostReactionCopyWith(_SnPostReaction value, $Res Function(_SnPostReaction) _then) = __$SnPostReactionCopyWithImpl;
|
||||||
|
@override @useResult
|
||||||
|
$Res call({
|
||||||
|
String id, String symbol, int attitude, String postId, String accountId, DateTime createdAt, DateTime updatedAt, SnAccount? account, DateTime? deletedAt
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
@override $SnAccountCopyWith<$Res>? get account;
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class __$SnPostReactionCopyWithImpl<$Res>
|
||||||
|
implements _$SnPostReactionCopyWith<$Res> {
|
||||||
|
__$SnPostReactionCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final _SnPostReaction _self;
|
||||||
|
final $Res Function(_SnPostReaction) _then;
|
||||||
|
|
||||||
|
/// Create a copy of SnPostReaction
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? symbol = null,Object? attitude = null,Object? postId = null,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? account = freezed,Object? deletedAt = freezed,}) {
|
||||||
|
return _then(_SnPostReaction(
|
||||||
|
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,symbol: null == symbol ? _self.symbol : symbol // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,attitude: null == attitude ? _self.attitude : attitude // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,postId: null == postId ? _self.postId : postId // 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,account: freezed == account ? _self.account : account // ignore: cast_nullable_to_non_nullable
|
||||||
|
as SnAccount?,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a copy of SnPostReaction
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnAccountCopyWith<$Res>? get account {
|
||||||
|
if (_self.account == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $SnAccountCopyWith<$Res>(_self.account!, (value) {
|
||||||
|
return _then(_self.copyWith(account: value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// dart format on
|
// dart format on
|
||||||
|
|||||||
@@ -230,3 +230,35 @@ Map<String, dynamic> _$SnPostAwardToJson(_SnPostAward instance) =>
|
|||||||
'updated_at': instance.updatedAt?.toIso8601String(),
|
'updated_at': instance.updatedAt?.toIso8601String(),
|
||||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_SnPostReaction _$SnPostReactionFromJson(Map<String, dynamic> json) =>
|
||||||
|
_SnPostReaction(
|
||||||
|
id: json['id'] as String,
|
||||||
|
symbol: json['symbol'] as String,
|
||||||
|
attitude: (json['attitude'] as num).toInt(),
|
||||||
|
postId: json['post_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),
|
||||||
|
account:
|
||||||
|
json['account'] == null
|
||||||
|
? null
|
||||||
|
: SnAccount.fromJson(json['account'] as Map<String, dynamic>),
|
||||||
|
deletedAt:
|
||||||
|
json['deleted_at'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['deleted_at'] as String),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$SnPostReactionToJson(_SnPostReaction instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'symbol': instance.symbol,
|
||||||
|
'attitude': instance.attitude,
|
||||||
|
'post_id': instance.postId,
|
||||||
|
'account_id': instance.accountId,
|
||||||
|
'created_at': instance.createdAt.toIso8601String(),
|
||||||
|
'updated_at': instance.updatedAt.toIso8601String(),
|
||||||
|
'account': instance.account?.toJson(),
|
||||||
|
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||||
|
};
|
||||||
|
|||||||
@@ -20,6 +20,24 @@ sealed class SnWallet with _$SnWallet {
|
|||||||
_$SnWalletFromJson(json);
|
_$SnWalletFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
sealed class SnWalletStats with _$SnWalletStats {
|
||||||
|
const factory SnWalletStats({
|
||||||
|
required DateTime periodBegin,
|
||||||
|
required DateTime periodEnd,
|
||||||
|
required int totalTransactions,
|
||||||
|
required int totalOrders,
|
||||||
|
required double totalIncome,
|
||||||
|
required double totalOutgoing,
|
||||||
|
required double sum,
|
||||||
|
@Default({}) Map<String, double> incomeCategories,
|
||||||
|
@Default({}) Map<String, double> outgoingCategories,
|
||||||
|
}) = _SnWalletStats;
|
||||||
|
|
||||||
|
factory SnWalletStats.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SnWalletStatsFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
sealed class SnWalletPocket with _$SnWalletPocket {
|
sealed class SnWalletPocket with _$SnWalletPocket {
|
||||||
const factory SnWalletPocket({
|
const factory SnWalletPocket({
|
||||||
@@ -124,3 +142,72 @@ sealed class SnWalletOrder with _$SnWalletOrder {
|
|||||||
factory SnWalletOrder.fromJson(Map<String, dynamic> json) =>
|
factory SnWalletOrder.fromJson(Map<String, dynamic> json) =>
|
||||||
_$SnWalletOrderFromJson(json);
|
_$SnWalletOrderFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
sealed class SnWalletGift with _$SnWalletGift {
|
||||||
|
const factory SnWalletGift({
|
||||||
|
required String id,
|
||||||
|
required String giftCode,
|
||||||
|
required String subscriptionIdentifier,
|
||||||
|
required String? recipientId,
|
||||||
|
required SnAccount? recipient,
|
||||||
|
required String gifterId,
|
||||||
|
required SnAccount? gifter,
|
||||||
|
required String? redeemerId,
|
||||||
|
required SnAccount? redeemer,
|
||||||
|
required String? message,
|
||||||
|
required int status,
|
||||||
|
required DateTime? redeemedAt,
|
||||||
|
required DateTime? expiredAt,
|
||||||
|
required String? subscriptionId,
|
||||||
|
required SnWalletSubscription? subscription,
|
||||||
|
required DateTime createdAt,
|
||||||
|
required DateTime updatedAt,
|
||||||
|
required DateTime? deletedAt,
|
||||||
|
}) = _SnWalletGift;
|
||||||
|
|
||||||
|
factory SnWalletGift.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SnWalletGiftFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
sealed class SnWalletFund with _$SnWalletFund {
|
||||||
|
const factory SnWalletFund({
|
||||||
|
required String id,
|
||||||
|
required String currency,
|
||||||
|
required double totalAmount,
|
||||||
|
required int splitType, // 0: even, 1: random
|
||||||
|
required int
|
||||||
|
status, // 0: created, 1: partially claimed, 2: fully claimed, 3: expired
|
||||||
|
required String? message,
|
||||||
|
required String creatorAccountId,
|
||||||
|
required SnAccount? creatorAccount,
|
||||||
|
required DateTime expiredAt,
|
||||||
|
required List<SnWalletFundRecipient> recipients,
|
||||||
|
required DateTime createdAt,
|
||||||
|
required DateTime updatedAt,
|
||||||
|
required DateTime? deletedAt,
|
||||||
|
}) = _SnWalletFund;
|
||||||
|
|
||||||
|
factory SnWalletFund.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SnWalletFundFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
sealed class SnWalletFundRecipient with _$SnWalletFundRecipient {
|
||||||
|
const factory SnWalletFundRecipient({
|
||||||
|
required String id,
|
||||||
|
required String fundId,
|
||||||
|
required String recipientAccountId,
|
||||||
|
required SnAccount? recipientAccount,
|
||||||
|
required double amount,
|
||||||
|
required bool isReceived,
|
||||||
|
required DateTime? receivedAt,
|
||||||
|
required DateTime createdAt,
|
||||||
|
required DateTime updatedAt,
|
||||||
|
required DateTime? deletedAt,
|
||||||
|
}) = _SnWalletFundRecipient;
|
||||||
|
|
||||||
|
factory SnWalletFundRecipient.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SnWalletFundRecipientFromJson(json);
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -35,6 +35,40 @@ Map<String, dynamic> _$SnWalletToJson(_SnWallet instance) => <String, dynamic>{
|
|||||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_SnWalletStats _$SnWalletStatsFromJson(Map<String, dynamic> json) =>
|
||||||
|
_SnWalletStats(
|
||||||
|
periodBegin: DateTime.parse(json['period_begin'] as String),
|
||||||
|
periodEnd: DateTime.parse(json['period_end'] as String),
|
||||||
|
totalTransactions: (json['total_transactions'] as num).toInt(),
|
||||||
|
totalOrders: (json['total_orders'] as num).toInt(),
|
||||||
|
totalIncome: (json['total_income'] as num).toDouble(),
|
||||||
|
totalOutgoing: (json['total_outgoing'] as num).toDouble(),
|
||||||
|
sum: (json['sum'] as num).toDouble(),
|
||||||
|
incomeCategories:
|
||||||
|
(json['income_categories'] as Map<String, dynamic>?)?.map(
|
||||||
|
(k, e) => MapEntry(k, (e as num).toDouble()),
|
||||||
|
) ??
|
||||||
|
const {},
|
||||||
|
outgoingCategories:
|
||||||
|
(json['outgoing_categories'] as Map<String, dynamic>?)?.map(
|
||||||
|
(k, e) => MapEntry(k, (e as num).toDouble()),
|
||||||
|
) ??
|
||||||
|
const {},
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$SnWalletStatsToJson(_SnWalletStats instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'period_begin': instance.periodBegin.toIso8601String(),
|
||||||
|
'period_end': instance.periodEnd.toIso8601String(),
|
||||||
|
'total_transactions': instance.totalTransactions,
|
||||||
|
'total_orders': instance.totalOrders,
|
||||||
|
'total_income': instance.totalIncome,
|
||||||
|
'total_outgoing': instance.totalOutgoing,
|
||||||
|
'sum': instance.sum,
|
||||||
|
'income_categories': instance.incomeCategories,
|
||||||
|
'outgoing_categories': instance.outgoingCategories,
|
||||||
|
};
|
||||||
|
|
||||||
_SnWalletPocket _$SnWalletPocketFromJson(Map<String, dynamic> json) =>
|
_SnWalletPocket _$SnWalletPocketFromJson(Map<String, dynamic> json) =>
|
||||||
_SnWalletPocket(
|
_SnWalletPocket(
|
||||||
id: json['id'] as String,
|
id: json['id'] as String,
|
||||||
@@ -228,3 +262,155 @@ Map<String, dynamic> _$SnWalletOrderToJson(_SnWalletOrder instance) =>
|
|||||||
'updated_at': instance.updatedAt.toIso8601String(),
|
'updated_at': instance.updatedAt.toIso8601String(),
|
||||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_SnWalletGift _$SnWalletGiftFromJson(Map<String, dynamic> json) =>
|
||||||
|
_SnWalletGift(
|
||||||
|
id: json['id'] as String,
|
||||||
|
giftCode: json['gift_code'] as String,
|
||||||
|
subscriptionIdentifier: json['subscription_identifier'] as String,
|
||||||
|
recipientId: json['recipient_id'] as String?,
|
||||||
|
recipient:
|
||||||
|
json['recipient'] == null
|
||||||
|
? null
|
||||||
|
: SnAccount.fromJson(json['recipient'] as Map<String, dynamic>),
|
||||||
|
gifterId: json['gifter_id'] as String,
|
||||||
|
gifter:
|
||||||
|
json['gifter'] == null
|
||||||
|
? null
|
||||||
|
: SnAccount.fromJson(json['gifter'] as Map<String, dynamic>),
|
||||||
|
redeemerId: json['redeemer_id'] as String?,
|
||||||
|
redeemer:
|
||||||
|
json['redeemer'] == null
|
||||||
|
? null
|
||||||
|
: SnAccount.fromJson(json['redeemer'] as Map<String, dynamic>),
|
||||||
|
message: json['message'] as String?,
|
||||||
|
status: (json['status'] as num).toInt(),
|
||||||
|
redeemedAt:
|
||||||
|
json['redeemed_at'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['redeemed_at'] as String),
|
||||||
|
expiredAt:
|
||||||
|
json['expired_at'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['expired_at'] as String),
|
||||||
|
subscriptionId: json['subscription_id'] as String?,
|
||||||
|
subscription:
|
||||||
|
json['subscription'] == null
|
||||||
|
? null
|
||||||
|
: SnWalletSubscription.fromJson(
|
||||||
|
json['subscription'] as Map<String, dynamic>,
|
||||||
|
),
|
||||||
|
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> _$SnWalletGiftToJson(_SnWalletGift instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'gift_code': instance.giftCode,
|
||||||
|
'subscription_identifier': instance.subscriptionIdentifier,
|
||||||
|
'recipient_id': instance.recipientId,
|
||||||
|
'recipient': instance.recipient?.toJson(),
|
||||||
|
'gifter_id': instance.gifterId,
|
||||||
|
'gifter': instance.gifter?.toJson(),
|
||||||
|
'redeemer_id': instance.redeemerId,
|
||||||
|
'redeemer': instance.redeemer?.toJson(),
|
||||||
|
'message': instance.message,
|
||||||
|
'status': instance.status,
|
||||||
|
'redeemed_at': instance.redeemedAt?.toIso8601String(),
|
||||||
|
'expired_at': instance.expiredAt?.toIso8601String(),
|
||||||
|
'subscription_id': instance.subscriptionId,
|
||||||
|
'subscription': instance.subscription?.toJson(),
|
||||||
|
'created_at': instance.createdAt.toIso8601String(),
|
||||||
|
'updated_at': instance.updatedAt.toIso8601String(),
|
||||||
|
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||||
|
};
|
||||||
|
|
||||||
|
_SnWalletFund _$SnWalletFundFromJson(
|
||||||
|
Map<String, dynamic> json,
|
||||||
|
) => _SnWalletFund(
|
||||||
|
id: json['id'] as String,
|
||||||
|
currency: json['currency'] as String,
|
||||||
|
totalAmount: (json['total_amount'] as num).toDouble(),
|
||||||
|
splitType: (json['split_type'] as num).toInt(),
|
||||||
|
status: (json['status'] as num).toInt(),
|
||||||
|
message: json['message'] as String?,
|
||||||
|
creatorAccountId: json['creator_account_id'] as String,
|
||||||
|
creatorAccount:
|
||||||
|
json['creator_account'] == null
|
||||||
|
? null
|
||||||
|
: SnAccount.fromJson(json['creator_account'] as Map<String, dynamic>),
|
||||||
|
expiredAt: DateTime.parse(json['expired_at'] as String),
|
||||||
|
recipients:
|
||||||
|
(json['recipients'] as List<dynamic>)
|
||||||
|
.map((e) => SnWalletFundRecipient.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
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> _$SnWalletFundToJson(_SnWalletFund instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'currency': instance.currency,
|
||||||
|
'total_amount': instance.totalAmount,
|
||||||
|
'split_type': instance.splitType,
|
||||||
|
'status': instance.status,
|
||||||
|
'message': instance.message,
|
||||||
|
'creator_account_id': instance.creatorAccountId,
|
||||||
|
'creator_account': instance.creatorAccount?.toJson(),
|
||||||
|
'expired_at': instance.expiredAt.toIso8601String(),
|
||||||
|
'recipients': instance.recipients.map((e) => e.toJson()).toList(),
|
||||||
|
'created_at': instance.createdAt.toIso8601String(),
|
||||||
|
'updated_at': instance.updatedAt.toIso8601String(),
|
||||||
|
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||||
|
};
|
||||||
|
|
||||||
|
_SnWalletFundRecipient _$SnWalletFundRecipientFromJson(
|
||||||
|
Map<String, dynamic> json,
|
||||||
|
) => _SnWalletFundRecipient(
|
||||||
|
id: json['id'] as String,
|
||||||
|
fundId: json['fund_id'] as String,
|
||||||
|
recipientAccountId: json['recipient_account_id'] as String,
|
||||||
|
recipientAccount:
|
||||||
|
json['recipient_account'] == null
|
||||||
|
? null
|
||||||
|
: SnAccount.fromJson(
|
||||||
|
json['recipient_account'] as Map<String, dynamic>,
|
||||||
|
),
|
||||||
|
amount: (json['amount'] as num).toDouble(),
|
||||||
|
isReceived: json['is_received'] as bool,
|
||||||
|
receivedAt:
|
||||||
|
json['received_at'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['received_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> _$SnWalletFundRecipientToJson(
|
||||||
|
_SnWalletFundRecipient instance,
|
||||||
|
) => <String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'fund_id': instance.fundId,
|
||||||
|
'recipient_account_id': instance.recipientAccountId,
|
||||||
|
'recipient_account': instance.recipientAccount?.toJson(),
|
||||||
|
'amount': instance.amount,
|
||||||
|
'is_received': instance.isReceived,
|
||||||
|
'received_at': instance.receivedAt?.toIso8601String(),
|
||||||
|
'created_at': instance.createdAt.toIso8601String(),
|
||||||
|
'updated_at': instance.updatedAt.toIso8601String(),
|
||||||
|
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||||
|
};
|
||||||
|
|||||||
@@ -312,7 +312,7 @@ class ServerStateNotifier extends StateNotifier<ServerState> {
|
|||||||
: super(ServerState(status: 'Server not started'));
|
: super(ServerState(status: 'Server not started'));
|
||||||
|
|
||||||
Future<void> start() async {
|
Future<void> start() async {
|
||||||
if (!Platform.isAndroid && !Platform.isIOS && !kIsWeb) {
|
if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) {
|
||||||
try {
|
try {
|
||||||
await server.start();
|
await server.start();
|
||||||
state = state.copyWith(status: 'Server running');
|
state = state.copyWith(status: 'Server running');
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||||
|
import 'package:island/pods/config.dart';
|
||||||
|
import 'package:island/pods/userinfo.dart';
|
||||||
import 'package:island/widgets/chat/call_button.dart';
|
import 'package:island/widgets/chat/call_button.dart';
|
||||||
import 'package:livekit_client/livekit_client.dart';
|
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
import 'package:island/models/chat.dart';
|
import 'package:island/models/chat.dart';
|
||||||
|
import 'package:island/models/account.dart';
|
||||||
|
import 'package:island/pods/chat/webrtc_manager.dart';
|
||||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||||
import 'package:island/talker.dart';
|
import 'package:island/talker.dart';
|
||||||
|
|
||||||
@@ -41,193 +45,212 @@ sealed class CallParticipantLive with _$CallParticipantLive {
|
|||||||
|
|
||||||
const factory CallParticipantLive({
|
const factory CallParticipantLive({
|
||||||
required CallParticipant participant,
|
required CallParticipant participant,
|
||||||
required Participant remoteParticipant,
|
required WebRTCParticipant remoteParticipant,
|
||||||
}) = _CallParticipantLive;
|
}) = _CallParticipantLive;
|
||||||
|
|
||||||
bool get isSpeaking => remoteParticipant.isSpeaking;
|
bool get isSpeaking {
|
||||||
bool get isMuted =>
|
// Use the actual audio level from WebRTC monitoring
|
||||||
remoteParticipant.isMuted || !remoteParticipant.isMicrophoneEnabled();
|
return remoteParticipant.audioLevel > 0.1; // Threshold for speaking
|
||||||
bool get isScreenSharing => remoteParticipant.isScreenShareEnabled();
|
}
|
||||||
bool get isScreenSharingWithAudio =>
|
|
||||||
remoteParticipant.isScreenShareAudioEnabled();
|
|
||||||
|
|
||||||
bool get hasVideo => remoteParticipant.hasVideo;
|
double get audioLevel => remoteParticipant.audioLevel;
|
||||||
bool get hasAudio => remoteParticipant.hasAudio;
|
|
||||||
|
bool get isMuted => !remoteParticipant.isAudioEnabled;
|
||||||
|
bool get isScreenSharing => remoteParticipant.isVideoEnabled; // Simplified
|
||||||
|
bool get isScreenSharingWithAudio => false; // TODO: Implement screen sharing
|
||||||
|
|
||||||
|
bool get hasVideo => remoteParticipant.isVideoEnabled;
|
||||||
|
bool get hasAudio => remoteParticipant.isAudioEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
class CallNotifier extends _$CallNotifier {
|
class CallNotifier extends _$CallNotifier {
|
||||||
Room? _room;
|
WebRTCManager? _webrtcManager;
|
||||||
LocalParticipant? _localParticipant;
|
|
||||||
List<CallParticipantLive> _participants = [];
|
List<CallParticipantLive> _participants = [];
|
||||||
final Map<String, CallParticipant> _participantInfoByIdentity = {};
|
final Map<String, CallParticipant> _participantInfoByIdentity = {};
|
||||||
EventsListener? _roomListener;
|
StreamSubscription<WebRTCParticipant>? _participantJoinedSubscription;
|
||||||
|
StreamSubscription<String>? _participantLeftSubscription;
|
||||||
|
|
||||||
List<CallParticipantLive> get participants =>
|
List<CallParticipantLive> get participants =>
|
||||||
List.unmodifiable(_participants);
|
List.unmodifiable(_participants);
|
||||||
LocalParticipant? get localParticipant => _localParticipant;
|
|
||||||
|
|
||||||
Map<String, double> participantsVolumes = {};
|
Map<String, double> participantsVolumes = {};
|
||||||
|
|
||||||
Timer? _durationTimer;
|
Timer? _durationTimer;
|
||||||
|
|
||||||
Room? get room => _room;
|
String? _roomId;
|
||||||
|
String? get roomId => _roomId;
|
||||||
|
WebRTCManager? get webrtcManager => _webrtcManager;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
CallState build() {
|
CallState build() {
|
||||||
// Subscribe to websocket updates
|
// Subscribe to websocket updates
|
||||||
return const CallState(
|
return const CallState(
|
||||||
isConnected: false,
|
isConnected: false,
|
||||||
isMicrophoneEnabled: true,
|
isMicrophoneEnabled:
|
||||||
isCameraEnabled: false,
|
true, // Audio enabled by default (matches WebRTC init)
|
||||||
|
isCameraEnabled: true, // Video enabled by default (matches WebRTC init)
|
||||||
isScreenSharing: false,
|
isScreenSharing: false,
|
||||||
isSpeakerphone: true,
|
isSpeakerphone: true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _initRoomListeners() {
|
void _initWebRTCListeners() {
|
||||||
if (_room == null) return;
|
_participantJoinedSubscription?.cancel();
|
||||||
_roomListener?.dispose();
|
_participantLeftSubscription?.cancel();
|
||||||
_roomListener = _room!.createListener();
|
|
||||||
_room!.addListener(_onRoomChange);
|
_participantJoinedSubscription = _webrtcManager?.onParticipantJoined.listen(
|
||||||
_roomListener!
|
(participant) {
|
||||||
..on<ParticipantConnectedEvent>((e) {
|
_updateLiveParticipantsFromWebRTC();
|
||||||
_refreshLiveParticipants();
|
},
|
||||||
})
|
);
|
||||||
..on<RoomDisconnectedEvent>((e) {
|
|
||||||
_participants = [];
|
_participantLeftSubscription = _webrtcManager?.onParticipantLeft.listen((
|
||||||
|
participantId,
|
||||||
|
) {
|
||||||
|
_participants.removeWhere((p) => p.remoteParticipant.id == participantId);
|
||||||
state = state.copyWith();
|
state = state.copyWith();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add local participant immediately when WebRTC is initialized
|
||||||
|
final userinfo = ref.watch(userInfoProvider);
|
||||||
|
if (userinfo.value != null) {
|
||||||
|
_addLocalParticipant(userinfo.value!);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onRoomChange() {
|
void _addLocalParticipant(SnAccount userinfo) {
|
||||||
_refreshLiveParticipants();
|
if (_webrtcManager == null) return;
|
||||||
}
|
|
||||||
|
|
||||||
void _refreshLiveParticipants() {
|
// Remove any existing local participant first
|
||||||
if (_room == null) return;
|
_participants.removeWhere((p) => p.participant.identity == userinfo.id);
|
||||||
final remoteParticipants = _room!.remoteParticipants;
|
|
||||||
_participants = [];
|
// Add local participant (current user)
|
||||||
// Add local participant first if available
|
final localParticipant = CallParticipantLive(
|
||||||
if (_localParticipant != null) {
|
participant: CallParticipant(
|
||||||
final localInfo = _buildParticipant();
|
identity: userinfo.id, // Use roomId as local identity
|
||||||
_participants.add(
|
name: userinfo.name,
|
||||||
CallParticipantLive(
|
accountId: userinfo.id,
|
||||||
participant: localInfo,
|
account: userinfo,
|
||||||
remoteParticipant: _localParticipant!,
|
joinedAt: DateTime.now(),
|
||||||
),
|
),
|
||||||
|
remoteParticipant: WebRTCParticipant(
|
||||||
|
id: _webrtcManager!.roomId,
|
||||||
|
name: userinfo.nick,
|
||||||
|
userinfo: userinfo,
|
||||||
|
isLocal: true,
|
||||||
|
)..remoteStream = _webrtcManager!.localStream, // Access local stream
|
||||||
);
|
);
|
||||||
|
|
||||||
|
_participants.insert(0, localParticipant); // Add at the beginning
|
||||||
|
state = state.copyWith();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _updateLiveParticipantsFromWebRTC() {
|
||||||
|
if (_webrtcManager == null) return;
|
||||||
|
|
||||||
|
final webrtcParticipants = _webrtcManager!.participants;
|
||||||
|
|
||||||
|
// Always ensure local participant exists
|
||||||
|
final existingLocalParticipant =
|
||||||
|
_participants.isNotEmpty &&
|
||||||
|
_participants[0].remoteParticipant.id == _webrtcManager!.roomId
|
||||||
|
? _participants[0]
|
||||||
|
: null;
|
||||||
|
|
||||||
|
final localParticipant =
|
||||||
|
existingLocalParticipant ?? _createLocalParticipant();
|
||||||
|
|
||||||
// Add remote participants
|
// Add remote participants
|
||||||
_participants.addAll(
|
final remoteParticipants =
|
||||||
remoteParticipants.values.map((remote) {
|
webrtcParticipants.map((p) {
|
||||||
final match =
|
final participantInfo =
|
||||||
_participantInfoByIdentity[remote.identity] ??
|
_participantInfoByIdentity[p.id] ??
|
||||||
CallParticipant(
|
CallParticipant(
|
||||||
identity: remote.identity,
|
identity: p.id,
|
||||||
name: remote.identity,
|
name: p.name,
|
||||||
|
accountId: p.userinfo.id,
|
||||||
|
account: p.userinfo,
|
||||||
joinedAt: DateTime.now(),
|
joinedAt: DateTime.now(),
|
||||||
);
|
);
|
||||||
return CallParticipantLive(
|
return CallParticipantLive(
|
||||||
participant: match,
|
participant: participantInfo,
|
||||||
remoteParticipant: remote,
|
remoteParticipant: p,
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
// Combine local participant with remote participants
|
||||||
|
_participants = [localParticipant, ...remoteParticipants];
|
||||||
|
|
||||||
state = state.copyWith();
|
state = state.copyWith();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builds the CallParticipant object for the local participant.
|
CallParticipantLive _createLocalParticipant() {
|
||||||
/// Optionally, pass [participants] if you want to prioritize info from the latest list.
|
return CallParticipantLive(
|
||||||
CallParticipant _buildParticipant({List<CallParticipant>? participants}) {
|
participant: CallParticipant(
|
||||||
if (_localParticipant == null) {
|
identity: _webrtcManager!.roomId, // Use roomId as local identity
|
||||||
throw StateError('No local participant available');
|
name: 'You',
|
||||||
}
|
accountId: '',
|
||||||
// Prefer info from the latest participants list if available
|
account: null,
|
||||||
if (participants != null) {
|
|
||||||
final idx = participants.indexWhere(
|
|
||||||
(p) => p.identity == _localParticipant!.identity,
|
|
||||||
);
|
|
||||||
if (idx != -1) return participants[idx];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, use info from the identity map or fallback to minimal
|
|
||||||
return _participantInfoByIdentity[_localParticipant!.identity] ??
|
|
||||||
CallParticipant(
|
|
||||||
identity: _localParticipant!.identity,
|
|
||||||
name: _localParticipant!.identity,
|
|
||||||
joinedAt: DateTime.now(),
|
joinedAt: DateTime.now(),
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _updateLiveParticipants(List<CallParticipant> participants) {
|
|
||||||
// Update the info map for lookup
|
|
||||||
for (final p in participants) {
|
|
||||||
_participantInfoByIdentity[p.identity] = p;
|
|
||||||
}
|
|
||||||
if (_room == null) {
|
|
||||||
// Can't build live objects, just store empty
|
|
||||||
_participants = [];
|
|
||||||
state = state.copyWith();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final remoteParticipants = _room!.remoteParticipants;
|
|
||||||
final remotes = remoteParticipants.values.toList();
|
|
||||||
_participants = [];
|
|
||||||
// Add local participant if present in the list
|
|
||||||
if (_localParticipant != null) {
|
|
||||||
final localInfo = _buildParticipant(participants: participants);
|
|
||||||
_participants.add(
|
|
||||||
CallParticipantLive(
|
|
||||||
participant: localInfo,
|
|
||||||
remoteParticipant: _localParticipant!,
|
|
||||||
),
|
),
|
||||||
|
remoteParticipant: WebRTCParticipant(
|
||||||
|
id: _webrtcManager!.roomId,
|
||||||
|
name: 'You',
|
||||||
|
userinfo: SnAccount(
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
nick: '',
|
||||||
|
language: '',
|
||||||
|
isSuperuser: false,
|
||||||
|
automatedId: null,
|
||||||
|
profile: SnAccountProfile(
|
||||||
|
id: '',
|
||||||
|
firstName: '',
|
||||||
|
middleName: '',
|
||||||
|
lastName: '',
|
||||||
|
bio: '',
|
||||||
|
gender: '',
|
||||||
|
pronouns: '',
|
||||||
|
location: '',
|
||||||
|
timeZone: '',
|
||||||
|
links: [],
|
||||||
|
experience: 0,
|
||||||
|
level: 0,
|
||||||
|
socialCredits: 0,
|
||||||
|
socialCreditsLevel: 0,
|
||||||
|
levelingProgress: 0,
|
||||||
|
picture: null,
|
||||||
|
background: null,
|
||||||
|
verification: null,
|
||||||
|
usernameColor: null,
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
deletedAt: null,
|
||||||
|
),
|
||||||
|
perkSubscription: null,
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
deletedAt: null,
|
||||||
|
),
|
||||||
|
)..remoteStream = _webrtcManager!.localStream, // Access local stream
|
||||||
);
|
);
|
||||||
state = state.copyWith();
|
|
||||||
}
|
}
|
||||||
// Add remote participants
|
|
||||||
_participants.addAll(
|
|
||||||
participants.map((p) {
|
|
||||||
RemoteParticipant? remote;
|
|
||||||
for (final r in remotes) {
|
|
||||||
if (r.identity == p.identity) {
|
|
||||||
remote = r;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (_localParticipant != null &&
|
|
||||||
p.identity == _localParticipant!.identity) {
|
|
||||||
return null; // Already added local
|
|
||||||
}
|
|
||||||
return remote != null
|
|
||||||
? CallParticipantLive(participant: p, remoteParticipant: remote)
|
|
||||||
: null;
|
|
||||||
}).whereType<CallParticipantLive>(),
|
|
||||||
);
|
|
||||||
state = state.copyWith();
|
|
||||||
}
|
|
||||||
|
|
||||||
String? _roomId;
|
|
||||||
String? get roomId => _roomId;
|
|
||||||
|
|
||||||
Future<void> joinRoom(String roomId) async {
|
Future<void> joinRoom(String roomId) async {
|
||||||
if (_roomId == roomId && _room != null) {
|
if (_roomId == roomId && _webrtcManager != null) {
|
||||||
talker.info('[Call] Call skipped. Already has data');
|
talker.info('[Call] Call skipped. Already connected to this room');
|
||||||
return;
|
// Ensure state is connected even if we skip the join process
|
||||||
} else if (_room != null) {
|
if (!state.isConnected) {
|
||||||
if (!_room!.isDisposed &&
|
state = state.copyWith(isConnected: true);
|
||||||
_room!.connectionState != ConnectionState.disconnected) {
|
|
||||||
throw Exception('Call already connected');
|
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
_roomId = roomId;
|
_roomId = roomId;
|
||||||
if (_room != null) {
|
|
||||||
await _room!.disconnect();
|
// Clean up existing connection
|
||||||
await _room!.dispose();
|
await disconnect();
|
||||||
_room = null;
|
|
||||||
_localParticipant = null;
|
|
||||||
_participants = [];
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
final apiClient = ref.read(apiClientProvider);
|
final apiClient = ref.read(apiClientProvider);
|
||||||
final ongoingCall = await ref.read(ongoingCallProvider(roomId).future);
|
final ongoingCall = await ref.read(ongoingCallProvider(roomId).future);
|
||||||
@@ -239,8 +262,11 @@ class CallNotifier extends _$CallNotifier {
|
|||||||
// Parse join response
|
// Parse join response
|
||||||
final joinResponse = ChatRealtimeJoinResponse.fromJson(data);
|
final joinResponse = ChatRealtimeJoinResponse.fromJson(data);
|
||||||
final participants = joinResponse.participants;
|
final participants = joinResponse.participants;
|
||||||
final String endpoint = joinResponse.endpoint;
|
|
||||||
final String token = joinResponse.token;
|
// Update participant info map
|
||||||
|
for (final p in participants) {
|
||||||
|
_participantInfoByIdentity[p.identity] = p;
|
||||||
|
}
|
||||||
|
|
||||||
// Setup duration timer
|
// Setup duration timer
|
||||||
_durationTimer?.cancel();
|
_durationTimer?.cancel();
|
||||||
@@ -255,47 +281,18 @@ class CallNotifier extends _$CallNotifier {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Connect to LiveKit
|
// Initialize WebRTC manager
|
||||||
_room = Room();
|
final serverUrl = ref.watch(serverUrlProvider);
|
||||||
|
|
||||||
await _room!.connect(
|
_webrtcManager = WebRTCManager(roomId: roomId, serverUrl: serverUrl);
|
||||||
endpoint,
|
|
||||||
token,
|
|
||||||
connectOptions: ConnectOptions(autoSubscribe: true),
|
|
||||||
roomOptions: RoomOptions(adaptiveStream: true, dynacast: true),
|
|
||||||
fastConnectOptions: FastConnectOptions(
|
|
||||||
microphone: TrackOption(enabled: true),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
_localParticipant = _room!.localParticipant;
|
|
||||||
|
|
||||||
_initRoomListeners();
|
await _webrtcManager!.initialize(ref);
|
||||||
_updateLiveParticipants(participants);
|
_initWebRTCListeners();
|
||||||
|
|
||||||
if (!kIsWeb && (Platform.isIOS || Platform.isAndroid)) {
|
if (!kIsWeb && (Platform.isIOS || Platform.isAndroid)) {
|
||||||
Hardware.instance.setSpeakerphoneOn(true);
|
// TODO: Implement speakerphone control for WebRTC
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listen for connection updates
|
|
||||||
_room!.addListener(() {
|
|
||||||
final wasConnected = state.isConnected;
|
|
||||||
final isNowConnected =
|
|
||||||
_room!.connectionState == ConnectionState.connected;
|
|
||||||
state = state.copyWith(
|
|
||||||
isConnected: isNowConnected,
|
|
||||||
isMicrophoneEnabled: _localParticipant!.isMicrophoneEnabled(),
|
|
||||||
isCameraEnabled: _localParticipant!.isCameraEnabled(),
|
|
||||||
isScreenSharing: _localParticipant!.isScreenShareEnabled(),
|
|
||||||
);
|
|
||||||
// Enable wakelock when call connects
|
|
||||||
if (!wasConnected && isNowConnected) {
|
|
||||||
WakelockPlus.enable();
|
|
||||||
}
|
|
||||||
// Disable wakelock when call disconnects
|
|
||||||
else if (wasConnected && !isNowConnected) {
|
|
||||||
WakelockPlus.disable();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
state = state.copyWith(isConnected: true);
|
state = state.copyWith(isConnected: true);
|
||||||
// Enable wakelock when call connects
|
// Enable wakelock when call connects
|
||||||
WakelockPlus.enable();
|
WakelockPlus.enable();
|
||||||
@@ -308,50 +305,91 @@ class CallNotifier extends _$CallNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> toggleMicrophone() async {
|
Future<void> toggleMicrophone() async {
|
||||||
if (_localParticipant != null) {
|
final target = !state.isMicrophoneEnabled;
|
||||||
const autostop = true;
|
|
||||||
final target = !_localParticipant!.isMicrophoneEnabled();
|
|
||||||
state = state.copyWith(isMicrophoneEnabled: target);
|
state = state.copyWith(isMicrophoneEnabled: target);
|
||||||
if (target) {
|
await _webrtcManager?.toggleMicrophone(target);
|
||||||
await _localParticipant!.audioTrackPublications.firstOrNull?.unmute(
|
|
||||||
stopOnMute: autostop,
|
// Update local participant's audio state
|
||||||
);
|
if (_participants.isNotEmpty) {
|
||||||
} else {
|
_participants[0].remoteParticipant.isAudioEnabled = target;
|
||||||
await _localParticipant!.audioTrackPublications.firstOrNull?.mute(
|
state = state.copyWith(); // Trigger UI update
|
||||||
stopOnMute: autostop,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
state = state.copyWith();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> toggleCamera() async {
|
Future<void> toggleCamera() async {
|
||||||
if (_localParticipant != null) {
|
final target = !state.isCameraEnabled;
|
||||||
final target = !_localParticipant!.isCameraEnabled();
|
|
||||||
state = state.copyWith(isCameraEnabled: target);
|
state = state.copyWith(isCameraEnabled: target);
|
||||||
await _localParticipant!.setCameraEnabled(target);
|
await _webrtcManager?.toggleCamera(target);
|
||||||
state = state.copyWith();
|
|
||||||
|
// Update local participant's video state
|
||||||
|
if (_participants.isNotEmpty) {
|
||||||
|
_participants[0].remoteParticipant.isVideoEnabled = target;
|
||||||
|
state = state.copyWith(); // Trigger UI update
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> toggleScreenShare() async {
|
Future<void> toggleScreenShare(BuildContext context) async {
|
||||||
if (_localParticipant != null) {
|
if (_webrtcManager == null) return;
|
||||||
final target = !_localParticipant!.isScreenShareEnabled();
|
|
||||||
state = state.copyWith(isScreenSharing: target);
|
try {
|
||||||
await _localParticipant!.setScreenShareEnabled(target);
|
if (state.isScreenSharing) {
|
||||||
state = state.copyWith();
|
// Stop screen sharing - switch back to camera
|
||||||
|
await _webrtcManager!.toggleCamera(state.isCameraEnabled);
|
||||||
|
state = state.copyWith(isScreenSharing: false);
|
||||||
|
} else {
|
||||||
|
// Start screen sharing
|
||||||
|
if (WebRTC.platformIsDesktop) {
|
||||||
|
// For desktop, we need to get screen capture source
|
||||||
|
// This would require implementing a screen selection dialog
|
||||||
|
// For now, just toggle the state
|
||||||
|
state = state.copyWith(isScreenSharing: true);
|
||||||
|
} else if (WebRTC.platformIsWeb) {
|
||||||
|
// For web, get display media directly
|
||||||
|
await navigator.mediaDevices.getDisplayMedia({
|
||||||
|
'video': true,
|
||||||
|
'audio':
|
||||||
|
false, // Screen sharing typically doesn't include system audio
|
||||||
|
});
|
||||||
|
|
||||||
|
// Replace video track with screen sharing track
|
||||||
|
// This is a simplified implementation
|
||||||
|
state = state.copyWith(isScreenSharing: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
talker.error('[Call] Screen sharing error: $e');
|
||||||
|
state = state.copyWith(error: 'Failed to toggle screen sharing: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> toggleSpeakerphone() async {
|
Future<void> toggleSpeakerphone() async {
|
||||||
|
if (!kIsWeb && (Platform.isIOS || Platform.isAndroid)) {
|
||||||
|
try {
|
||||||
|
// For mobile platforms, we can control audio routing
|
||||||
|
// This is a simplified implementation
|
||||||
|
final newSpeakerphoneState = !state.isSpeakerphone;
|
||||||
|
state = state.copyWith(isSpeakerphone: newSpeakerphoneState);
|
||||||
|
|
||||||
|
// Note: Actual speakerphone control would require platform-specific code
|
||||||
|
// For a full implementation, you'd need to use platform channels
|
||||||
|
// to control audio routing on iOS/Android
|
||||||
|
talker.info('[Call] Speakerphone toggled to: $newSpeakerphoneState');
|
||||||
|
} catch (e) {
|
||||||
|
talker.error('[Call] Speakerphone control error: $e');
|
||||||
|
state = state.copyWith(error: 'Failed to toggle speakerphone: $e');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For web/desktop, speakerphone control is handled by the browser/OS
|
||||||
state = state.copyWith(isSpeakerphone: !state.isSpeakerphone);
|
state = state.copyWith(isSpeakerphone: !state.isSpeakerphone);
|
||||||
await Hardware.instance.setSpeakerphoneOn(state.isSpeakerphone);
|
}
|
||||||
state = state.copyWith();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> disconnect() async {
|
Future<void> disconnect() async {
|
||||||
if (_room != null) {
|
_webrtcManager?.dispose();
|
||||||
await _room!.disconnect();
|
_webrtcManager = null;
|
||||||
|
_participantJoinedSubscription?.cancel();
|
||||||
|
_participantLeftSubscription?.cancel();
|
||||||
|
_participants.clear();
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
isConnected: false,
|
isConnected: false,
|
||||||
isMicrophoneEnabled: false,
|
isMicrophoneEnabled: false,
|
||||||
@@ -361,26 +399,20 @@ class CallNotifier extends _$CallNotifier {
|
|||||||
// Disable wakelock when call disconnects
|
// Disable wakelock when call disconnects
|
||||||
WakelockPlus.disable();
|
WakelockPlus.disable();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
void setParticipantVolume(CallParticipantLive live, double volume) {
|
void setParticipantVolume(CallParticipantLive live, double volume) {
|
||||||
if (participantsVolumes[live.remoteParticipant.sid] == null) {
|
// Store volume setting for this participant
|
||||||
participantsVolumes[live.remoteParticipant.sid] = 1;
|
// Note: WebRTC doesn't have built-in per-participant volume control
|
||||||
}
|
// This is just storing the preference for UI purposes
|
||||||
Helper.setVolume(
|
// Actual volume control would need to be implemented at the audio rendering level
|
||||||
volume,
|
participantsVolumes[live.remoteParticipant.id] = volume.clamp(0.0, 1.0);
|
||||||
live
|
talker.info(
|
||||||
.remoteParticipant
|
'[Call] Volume set to $volume for participant ${live.remoteParticipant.id}',
|
||||||
.audioTrackPublications
|
|
||||||
.first
|
|
||||||
.track!
|
|
||||||
.mediaStreamTrack,
|
|
||||||
);
|
);
|
||||||
participantsVolumes[live.remoteParticipant.sid] = volume;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
double getParticipantVolume(CallParticipantLive live) {
|
double getParticipantVolume(CallParticipantLive live) {
|
||||||
return participantsVolumes[live.remoteParticipant.sid] ?? 1;
|
return participantsVolumes[live.remoteParticipant.id] ?? 1.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void dispose() {
|
void dispose() {
|
||||||
@@ -391,9 +423,10 @@ class CallNotifier extends _$CallNotifier {
|
|||||||
isCameraEnabled: false,
|
isCameraEnabled: false,
|
||||||
isScreenSharing: false,
|
isScreenSharing: false,
|
||||||
);
|
);
|
||||||
_roomListener?.dispose();
|
_participantJoinedSubscription?.cancel();
|
||||||
_room?.removeListener(_onRoomChange);
|
_participantLeftSubscription?.cancel();
|
||||||
_room?.dispose();
|
_webrtcManager?.dispose();
|
||||||
|
_webrtcManager = null;
|
||||||
_durationTimer?.cancel();
|
_durationTimer?.cancel();
|
||||||
_roomId = null;
|
_roomId = null;
|
||||||
participantsVolumes = {};
|
participantsVolumes = {};
|
||||||
|
|||||||
@@ -295,7 +295,7 @@ as String?,
|
|||||||
/// @nodoc
|
/// @nodoc
|
||||||
mixin _$CallParticipantLive implements DiagnosticableTreeMixin {
|
mixin _$CallParticipantLive implements DiagnosticableTreeMixin {
|
||||||
|
|
||||||
CallParticipant get participant; Participant get remoteParticipant;
|
CallParticipant get participant; WebRTCParticipant get remoteParticipant;
|
||||||
/// Create a copy of CallParticipantLive
|
/// Create a copy of CallParticipantLive
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@@ -332,7 +332,7 @@ abstract mixin class $CallParticipantLiveCopyWith<$Res> {
|
|||||||
factory $CallParticipantLiveCopyWith(CallParticipantLive value, $Res Function(CallParticipantLive) _then) = _$CallParticipantLiveCopyWithImpl;
|
factory $CallParticipantLiveCopyWith(CallParticipantLive value, $Res Function(CallParticipantLive) _then) = _$CallParticipantLiveCopyWithImpl;
|
||||||
@useResult
|
@useResult
|
||||||
$Res call({
|
$Res call({
|
||||||
CallParticipant participant, Participant remoteParticipant
|
CallParticipant participant, WebRTCParticipant remoteParticipant
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -353,7 +353,7 @@ class _$CallParticipantLiveCopyWithImpl<$Res>
|
|||||||
return _then(_self.copyWith(
|
return _then(_self.copyWith(
|
||||||
participant: null == participant ? _self.participant : participant // ignore: cast_nullable_to_non_nullable
|
participant: null == participant ? _self.participant : participant // ignore: cast_nullable_to_non_nullable
|
||||||
as CallParticipant,remoteParticipant: null == remoteParticipant ? _self.remoteParticipant : remoteParticipant // ignore: cast_nullable_to_non_nullable
|
as CallParticipant,remoteParticipant: null == remoteParticipant ? _self.remoteParticipant : remoteParticipant // ignore: cast_nullable_to_non_nullable
|
||||||
as Participant,
|
as WebRTCParticipant,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
/// Create a copy of CallParticipantLive
|
/// Create a copy of CallParticipantLive
|
||||||
@@ -444,7 +444,7 @@ return $default(_that);case _:
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( CallParticipant participant, Participant remoteParticipant)? $default,{required TResult orElse(),}) {final _that = this;
|
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( CallParticipant participant, WebRTCParticipant remoteParticipant)? $default,{required TResult orElse(),}) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _CallParticipantLive() when $default != null:
|
case _CallParticipantLive() when $default != null:
|
||||||
return $default(_that.participant,_that.remoteParticipant);case _:
|
return $default(_that.participant,_that.remoteParticipant);case _:
|
||||||
@@ -465,7 +465,7 @@ return $default(_that.participant,_that.remoteParticipant);case _:
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( CallParticipant participant, Participant remoteParticipant) $default,) {final _that = this;
|
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( CallParticipant participant, WebRTCParticipant remoteParticipant) $default,) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _CallParticipantLive():
|
case _CallParticipantLive():
|
||||||
return $default(_that.participant,_that.remoteParticipant);}
|
return $default(_that.participant,_that.remoteParticipant);}
|
||||||
@@ -482,7 +482,7 @@ return $default(_that.participant,_that.remoteParticipant);}
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( CallParticipant participant, Participant remoteParticipant)? $default,) {final _that = this;
|
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( CallParticipant participant, WebRTCParticipant remoteParticipant)? $default,) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _CallParticipantLive() when $default != null:
|
case _CallParticipantLive() when $default != null:
|
||||||
return $default(_that.participant,_that.remoteParticipant);case _:
|
return $default(_that.participant,_that.remoteParticipant);case _:
|
||||||
@@ -501,7 +501,7 @@ class _CallParticipantLive extends CallParticipantLive with DiagnosticableTreeMi
|
|||||||
|
|
||||||
|
|
||||||
@override final CallParticipant participant;
|
@override final CallParticipant participant;
|
||||||
@override final Participant remoteParticipant;
|
@override final WebRTCParticipant remoteParticipant;
|
||||||
|
|
||||||
/// Create a copy of CallParticipantLive
|
/// Create a copy of CallParticipantLive
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@@ -539,7 +539,7 @@ abstract mixin class _$CallParticipantLiveCopyWith<$Res> implements $CallPartici
|
|||||||
factory _$CallParticipantLiveCopyWith(_CallParticipantLive value, $Res Function(_CallParticipantLive) _then) = __$CallParticipantLiveCopyWithImpl;
|
factory _$CallParticipantLiveCopyWith(_CallParticipantLive value, $Res Function(_CallParticipantLive) _then) = __$CallParticipantLiveCopyWithImpl;
|
||||||
@override @useResult
|
@override @useResult
|
||||||
$Res call({
|
$Res call({
|
||||||
CallParticipant participant, Participant remoteParticipant
|
CallParticipant participant, WebRTCParticipant remoteParticipant
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -560,7 +560,7 @@ class __$CallParticipantLiveCopyWithImpl<$Res>
|
|||||||
return _then(_CallParticipantLive(
|
return _then(_CallParticipantLive(
|
||||||
participant: null == participant ? _self.participant : participant // ignore: cast_nullable_to_non_nullable
|
participant: null == participant ? _self.participant : participant // ignore: cast_nullable_to_non_nullable
|
||||||
as CallParticipant,remoteParticipant: null == remoteParticipant ? _self.remoteParticipant : remoteParticipant // ignore: cast_nullable_to_non_nullable
|
as CallParticipant,remoteParticipant: null == remoteParticipant ? _self.remoteParticipant : remoteParticipant // ignore: cast_nullable_to_non_nullable
|
||||||
as Participant,
|
as WebRTCParticipant,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ part of 'call.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$callNotifierHash() => r'd374402e51d331cf40724e51fd86bce3c5504776';
|
String _$callNotifierHash() => r'4015d326388553c46859fe537e84d2c9da4236c9';
|
||||||
|
|
||||||
/// See also [CallNotifier].
|
/// See also [CallNotifier].
|
||||||
@ProviderFor(CallNotifier)
|
@ProviderFor(CallNotifier)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ part of 'chat_subscribe.dart';
|
|||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$chatSubscribeNotifierHash() =>
|
String _$chatSubscribeNotifierHash() =>
|
||||||
r'df65ecf15d0e97d7e6850ac57b4e681606e77179';
|
r'c605e0c9c45df64e5ba7b65f8de9b47bde8e2b3b';
|
||||||
|
|
||||||
/// Copied from Dart SDK
|
/// Copied from Dart SDK
|
||||||
class _SystemHash {
|
class _SystemHash {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ part of 'chat_summary.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$chatSummaryHash() => r'7b79dba7445f634373fbb2ee0ced99b2302097c2';
|
String _$chatSummaryHash() => r'33815a3bd81d20902b7063e8194fe336930df9b4';
|
||||||
|
|
||||||
/// See also [ChatSummary].
|
/// See also [ChatSummary].
|
||||||
@ProviderFor(ChatSummary)
|
@ProviderFor(ChatSummary)
|
||||||
|
|||||||
@@ -7,11 +7,10 @@ import "package:island/database/drift_db.dart";
|
|||||||
import "package:island/database/message.dart";
|
import "package:island/database/message.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/pods/config.dart";
|
|
||||||
import "package:island/pods/database.dart";
|
import "package:island/pods/database.dart";
|
||||||
import "package:island/pods/lifecycle.dart";
|
import "package:island/pods/lifecycle.dart";
|
||||||
import "package:island/pods/network.dart";
|
import "package:island/pods/network.dart";
|
||||||
import "package:island/services/file.dart";
|
import "package:island/services/file_uploader.dart";
|
||||||
import "package:island/talker.dart";
|
import "package:island/talker.dart";
|
||||||
import "package:island/widgets/alert.dart";
|
import "package:island/widgets/alert.dart";
|
||||||
import "package:riverpod_annotation/riverpod_annotation.dart";
|
import "package:riverpod_annotation/riverpod_annotation.dart";
|
||||||
@@ -40,6 +39,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
bool _hasMore = true;
|
bool _hasMore = true;
|
||||||
bool _isSyncing = false;
|
bool _isSyncing = false;
|
||||||
bool _isJumping = false;
|
bool _isJumping = false;
|
||||||
|
bool _isUpdatingState = false;
|
||||||
DateTime? _lastPauseTime;
|
DateTime? _lastPauseTime;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -93,17 +93,43 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
return messages;
|
return messages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _updateStateSafely(List<LocalChatMessage> messages) async {
|
||||||
|
if (_isUpdatingState) {
|
||||||
|
talker.log('State update already in progress, skipping');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_isUpdatingState = true;
|
||||||
|
try {
|
||||||
|
// Ensure messages are properly sorted and deduplicated
|
||||||
|
final sortedMessages = _sortMessages(messages);
|
||||||
|
final uniqueMessages = <LocalChatMessage>[];
|
||||||
|
final seenIds = <String>{};
|
||||||
|
for (final message in sortedMessages) {
|
||||||
|
if (seenIds.add(message.id)) {
|
||||||
|
uniqueMessages.add(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state = AsyncValue.data(uniqueMessages);
|
||||||
|
} finally {
|
||||||
|
_isUpdatingState = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<List<LocalChatMessage>> _getCachedMessages({
|
Future<List<LocalChatMessage>> _getCachedMessages({
|
||||||
int offset = 0,
|
int offset = 0,
|
||||||
int take = 20,
|
int take = 20,
|
||||||
|
String? searchQuery,
|
||||||
|
bool? withLinks,
|
||||||
|
bool? withAttachments,
|
||||||
}) async {
|
}) async {
|
||||||
talker.log('Getting cached messages from offset $offset, take $take');
|
talker.log('Getting cached messages from offset $offset, take $take');
|
||||||
final List<LocalChatMessage> dbMessages;
|
final List<LocalChatMessage> dbMessages;
|
||||||
if (_searchQuery != null && _searchQuery!.isNotEmpty) {
|
|
||||||
|
if (searchQuery != null && searchQuery.isNotEmpty) {
|
||||||
dbMessages = await _database.searchMessages(
|
dbMessages = await _database.searchMessages(
|
||||||
_roomId,
|
_roomId,
|
||||||
_searchQuery ?? '',
|
searchQuery,
|
||||||
withAttachments: _withAttachments,
|
withAttachments: withAttachments,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
final chatMessagesFromDb = await _database.getMessagesForRoom(
|
final chatMessagesFromDb = await _database.getMessagesForRoom(
|
||||||
@@ -117,11 +143,18 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
|
|
||||||
List<LocalChatMessage> filteredMessages = dbMessages;
|
List<LocalChatMessage> filteredMessages = dbMessages;
|
||||||
|
|
||||||
if (_withLinks == true) {
|
if (withLinks == true) {
|
||||||
filteredMessages =
|
filteredMessages =
|
||||||
filteredMessages.where((msg) => _hasLink(msg)).toList();
|
filteredMessages.where((msg) => _hasLink(msg)).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (withAttachments == true) {
|
||||||
|
filteredMessages =
|
||||||
|
filteredMessages
|
||||||
|
.where((msg) => msg.toRemoteMessage().attachments.isNotEmpty)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
final dbLocalMessages = filteredMessages;
|
final dbLocalMessages = filteredMessages;
|
||||||
|
|
||||||
// Always ensure unique messages to prevent duplicate keys
|
// Always ensure unique messages to prevent duplicate keys
|
||||||
@@ -155,6 +188,51 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
return uniqueMessages;
|
return uniqueMessages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get all messages without search filters for jump operations
|
||||||
|
Future<List<LocalChatMessage>> _getAllMessagesForJump({
|
||||||
|
int offset = 0,
|
||||||
|
int take = 20,
|
||||||
|
}) async {
|
||||||
|
talker.log('Getting all messages for jump from offset $offset, take $take');
|
||||||
|
final chatMessagesFromDb = await _database.getMessagesForRoom(
|
||||||
|
_roomId,
|
||||||
|
offset: offset,
|
||||||
|
limit: take,
|
||||||
|
);
|
||||||
|
final dbMessages =
|
||||||
|
chatMessagesFromDb.map(_database.companionToMessage).toList();
|
||||||
|
|
||||||
|
// Always ensure unique messages to prevent duplicate keys
|
||||||
|
final uniqueMessages = <LocalChatMessage>[];
|
||||||
|
final seenIds = <String>{};
|
||||||
|
for (final message in dbMessages) {
|
||||||
|
if (seenIds.add(message.id)) {
|
||||||
|
uniqueMessages.add(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (offset == 0) {
|
||||||
|
final pendingForRoom =
|
||||||
|
_pendingMessages.values
|
||||||
|
.where((msg) => msg.roomId == _roomId)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final allMessages = [...pendingForRoom, ...uniqueMessages];
|
||||||
|
_sortMessages(allMessages);
|
||||||
|
|
||||||
|
final finalUniqueMessages = <LocalChatMessage>[];
|
||||||
|
final finalSeenIds = <String>{};
|
||||||
|
for (final message in allMessages) {
|
||||||
|
if (finalSeenIds.add(message.id)) {
|
||||||
|
finalUniqueMessages.add(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return finalUniqueMessages;
|
||||||
|
}
|
||||||
|
|
||||||
|
return uniqueMessages;
|
||||||
|
}
|
||||||
|
|
||||||
Future<List<LocalChatMessage>> _fetchAndCacheMessages({
|
Future<List<LocalChatMessage>> _fetchAndCacheMessages({
|
||||||
int offset = 0,
|
int offset = 0,
|
||||||
int take = 20,
|
int take = 20,
|
||||||
@@ -242,16 +320,6 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
final response = MessageSyncResponse.fromJson(resp.data);
|
final response = MessageSyncResponse.fromJson(resp.data);
|
||||||
talker.log('Sync response: ${response.messages.length} changes');
|
talker.log('Sync response: ${response.messages.length} changes');
|
||||||
for (final message in response.messages) {
|
for (final message in response.messages) {
|
||||||
switch (message.type) {
|
|
||||||
case "messages.update":
|
|
||||||
case "messages.update.links":
|
|
||||||
await receiveMessageUpdate(message);
|
|
||||||
break;
|
|
||||||
case "messages.delete":
|
|
||||||
await receiveMessageDeletion(message.id.toString());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// Still need receive the message to show the history actions
|
|
||||||
await receiveMessage(message);
|
await receiveMessage(message);
|
||||||
}
|
}
|
||||||
} catch (err, stackTrace) {
|
} catch (err, stackTrace) {
|
||||||
@@ -287,6 +355,9 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
final localMessages = await _getCachedMessages(
|
final localMessages = await _getCachedMessages(
|
||||||
offset: offset,
|
offset: offset,
|
||||||
take: take,
|
take: take,
|
||||||
|
searchQuery: _searchQuery,
|
||||||
|
withLinks: _withLinks,
|
||||||
|
withAttachments: _withAttachments,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (localMessages.isNotEmpty) {
|
if (localMessages.isNotEmpty) {
|
||||||
@@ -302,6 +373,9 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
final localMessages = await _getCachedMessages(
|
final localMessages = await _getCachedMessages(
|
||||||
offset: offset,
|
offset: offset,
|
||||||
take: take,
|
take: take,
|
||||||
|
searchQuery: _searchQuery,
|
||||||
|
withLinks: _withLinks,
|
||||||
|
withAttachments: _withAttachments,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (localMessages.isNotEmpty) {
|
if (localMessages.isNotEmpty) {
|
||||||
@@ -317,7 +391,13 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
syncMessages();
|
syncMessages();
|
||||||
}
|
}
|
||||||
|
|
||||||
final messages = await _getCachedMessages(offset: 0, take: _pageSize);
|
final messages = await _getCachedMessages(
|
||||||
|
offset: 0,
|
||||||
|
take: _pageSize,
|
||||||
|
searchQuery: _searchQuery,
|
||||||
|
withLinks: _withLinks,
|
||||||
|
withAttachments: _withAttachments,
|
||||||
|
);
|
||||||
|
|
||||||
_hasMore = messages.length == _pageSize;
|
_hasMore = messages.length == _pageSize;
|
||||||
|
|
||||||
@@ -362,9 +442,6 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
}) async {
|
}) async {
|
||||||
final nonce = const Uuid().v4();
|
final nonce = const Uuid().v4();
|
||||||
talker.log('Sending message with nonce $nonce');
|
talker.log('Sending message with nonce $nonce');
|
||||||
final baseUrl = ref.read(serverUrlProvider);
|
|
||||||
final token = await getToken(ref.watch(tokenProvider));
|
|
||||||
if (token == null) throw ArgumentError('Access token is null');
|
|
||||||
|
|
||||||
final mockMessage = SnChatMessage(
|
final mockMessage = SnChatMessage(
|
||||||
id: 'pending_$nonce',
|
id: 'pending_$nonce',
|
||||||
@@ -393,19 +470,9 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
var cloudAttachments = List.empty(growable: true);
|
var cloudAttachments = List.empty(growable: true);
|
||||||
for (var idx = 0; idx < attachments.length; idx++) {
|
for (var idx = 0; idx < attachments.length; idx++) {
|
||||||
final cloudFile =
|
final cloudFile =
|
||||||
await putFileToCloud(
|
await FileUploader.createCloudFile(
|
||||||
fileData: attachments[idx],
|
fileData: attachments[idx],
|
||||||
atk: token,
|
client: ref.read(apiClientProvider),
|
||||||
baseUrl: baseUrl,
|
|
||||||
filename: attachments[idx].data.name ?? 'Post media',
|
|
||||||
mimetype:
|
|
||||||
attachments[idx].data.mimeType ??
|
|
||||||
switch (attachments[idx].type) {
|
|
||||||
UniversalFileType.image => 'image/unknown',
|
|
||||||
UniversalFileType.video => 'video/unknown',
|
|
||||||
UniversalFileType.audio => 'audio/unknown',
|
|
||||||
UniversalFileType.file => 'application/octet-stream',
|
|
||||||
},
|
|
||||||
onProgress: (progress, _) {
|
onProgress: (progress, _) {
|
||||||
_fileUploadProgress[localMessage.id]?[idx] = progress;
|
_fileUploadProgress[localMessage.id]?[idx] = progress;
|
||||||
onProgress?.call(
|
onProgress?.call(
|
||||||
@@ -562,6 +629,13 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
|
|
||||||
Future<void> receiveMessage(SnChatMessage remoteMessage) async {
|
Future<void> receiveMessage(SnChatMessage remoteMessage) async {
|
||||||
if (remoteMessage.chatRoomId != _roomId) return;
|
if (remoteMessage.chatRoomId != _roomId) return;
|
||||||
|
|
||||||
|
// Block message receiving during jumps to prevent list resets
|
||||||
|
if (_isJumping) {
|
||||||
|
talker.log('Blocking message receive during jump operation');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
talker.log('Received new message ${remoteMessage.id}');
|
talker.log('Received new message ${remoteMessage.id}');
|
||||||
|
|
||||||
final localMessage = LocalChatMessage.fromRemoteMessage(
|
final localMessage = LocalChatMessage.fromRemoteMessage(
|
||||||
@@ -607,6 +681,13 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
|
|
||||||
Future<void> receiveMessageUpdate(SnChatMessage remoteMessage) async {
|
Future<void> receiveMessageUpdate(SnChatMessage remoteMessage) async {
|
||||||
if (remoteMessage.chatRoomId != _roomId) return;
|
if (remoteMessage.chatRoomId != _roomId) return;
|
||||||
|
|
||||||
|
// Block message updates during jumps to prevent list resets
|
||||||
|
if (_isJumping) {
|
||||||
|
talker.log('Blocking message update during jump operation');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
talker.log('Received message update ${remoteMessage.id}');
|
talker.log('Received message update ${remoteMessage.id}');
|
||||||
|
|
||||||
final targetId = remoteMessage.meta['message_id'] ?? remoteMessage.id;
|
final targetId = remoteMessage.meta['message_id'] ?? remoteMessage.id;
|
||||||
@@ -614,6 +695,8 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
remoteMessage.copyWith(
|
remoteMessage.copyWith(
|
||||||
id: targetId,
|
id: targetId,
|
||||||
meta: Map.of(remoteMessage.meta)..remove('message_id'),
|
meta: Map.of(remoteMessage.meta)..remove('message_id'),
|
||||||
|
type: 'text',
|
||||||
|
editedAt: remoteMessage.createdAt,
|
||||||
),
|
),
|
||||||
MessageStatus.sent,
|
MessageStatus.sent,
|
||||||
);
|
);
|
||||||
@@ -630,6 +713,12 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> receiveMessageDeletion(String messageId) async {
|
Future<void> receiveMessageDeletion(String messageId) async {
|
||||||
|
// Block message deletions during jumps to prevent list resets
|
||||||
|
if (_isJumping) {
|
||||||
|
talker.log('Blocking message deletion during jump operation');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
talker.log('Received message deletion $messageId');
|
talker.log('Received message deletion $messageId');
|
||||||
_pendingMessages.remove(messageId);
|
_pendingMessages.remove(messageId);
|
||||||
|
|
||||||
@@ -668,13 +757,34 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
|
|
||||||
Future<void> deleteMessage(String messageId) async {
|
Future<void> deleteMessage(String messageId) async {
|
||||||
talker.log('Deleting message $messageId');
|
talker.log('Deleting message $messageId');
|
||||||
|
|
||||||
|
// Fetch message to check its status before attempting server delete
|
||||||
|
final message = await fetchMessageById(messageId);
|
||||||
|
if (message == null) {
|
||||||
|
talker.log('Message $messageId not found for deletion');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip server delete for failed messages (never successfully sent)
|
||||||
|
if (message.status == MessageStatus.failed) {
|
||||||
|
talker.log('Skipping server delete for failed message $messageId');
|
||||||
|
// For failed messages, remove them completely from the active list
|
||||||
|
_pendingMessages.remove(messageId);
|
||||||
|
await _database.deleteMessage(messageId);
|
||||||
|
|
||||||
|
final currentMessages = state.value ?? [];
|
||||||
|
final newMessages =
|
||||||
|
currentMessages.where((m) => m.id != messageId).toList();
|
||||||
|
state = AsyncValue.data(newMessages);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await _apiClient.delete('/sphere/chat/$_roomId/messages/$messageId');
|
await _apiClient.delete('/sphere/chat/$_roomId/messages/$messageId');
|
||||||
await receiveMessageDeletion(messageId);
|
await receiveMessageDeletion(messageId);
|
||||||
} catch (err, stackTrace) {
|
} catch (err, stackTrace) {
|
||||||
talker.log(
|
talker.log(
|
||||||
'Error deleting message $messageId',
|
'Error deleting message $messageId',
|
||||||
|
|
||||||
exception: err,
|
exception: err,
|
||||||
stackTrace: stackTrace,
|
stackTrace: stackTrace,
|
||||||
);
|
);
|
||||||
@@ -682,11 +792,78 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void searchMessages(String query, {bool? withLinks, bool? withAttachments}) {
|
/// Get search results without updating shared state
|
||||||
|
Future<List<LocalChatMessage>> getSearchResults(
|
||||||
|
String query, {
|
||||||
|
bool? withLinks,
|
||||||
|
bool? withAttachments,
|
||||||
|
}) async {
|
||||||
|
final trimmedQuery = query.trim();
|
||||||
|
final hasFilters = [withLinks, withAttachments].any((e) => e == true);
|
||||||
|
|
||||||
|
if (trimmedQuery.isEmpty && !hasFilters) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
talker.log(
|
||||||
|
'Getting search results for query: $trimmedQuery, filters: links=$withLinks, attachments=$withAttachments',
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// When filtering without query, get more messages to ensure we find all matches
|
||||||
|
final take = (trimmedQuery.isEmpty && hasFilters) ? 1000 : 50;
|
||||||
|
final messages = await _getCachedMessages(
|
||||||
|
offset: 0,
|
||||||
|
take: take,
|
||||||
|
searchQuery: trimmedQuery.isNotEmpty ? trimmedQuery : null,
|
||||||
|
withLinks: withLinks,
|
||||||
|
withAttachments: withAttachments,
|
||||||
|
); // Limit initial search results
|
||||||
|
return messages;
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
talker.log(
|
||||||
|
'Error getting search results',
|
||||||
|
exception: e,
|
||||||
|
stackTrace: stackTrace,
|
||||||
|
);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> searchMessages(
|
||||||
|
String query, {
|
||||||
|
bool? withLinks,
|
||||||
|
bool? withAttachments,
|
||||||
|
}) async {
|
||||||
_searchQuery = query.trim();
|
_searchQuery = query.trim();
|
||||||
_withLinks = withLinks;
|
_withLinks = withLinks;
|
||||||
_withAttachments = withAttachments;
|
_withAttachments = withAttachments;
|
||||||
loadInitial();
|
|
||||||
|
if (_searchQuery!.isEmpty) {
|
||||||
|
state = AsyncValue.data([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
talker.log('Searching messages with query: $_searchQuery');
|
||||||
|
state = const AsyncValue.loading();
|
||||||
|
|
||||||
|
try {
|
||||||
|
final messages = await _getCachedMessages(
|
||||||
|
offset: 0,
|
||||||
|
take: 50,
|
||||||
|
searchQuery: _searchQuery,
|
||||||
|
withLinks: _withLinks,
|
||||||
|
withAttachments: _withAttachments,
|
||||||
|
); // Limit initial search results
|
||||||
|
state = AsyncValue.data(messages);
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
talker.log(
|
||||||
|
'Error searching messages',
|
||||||
|
exception: e,
|
||||||
|
stackTrace: stackTrace,
|
||||||
|
);
|
||||||
|
state = AsyncValue.error(e, stackTrace);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void clearSearch() {
|
void clearSearch() {
|
||||||
@@ -731,6 +908,9 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
}
|
}
|
||||||
_isJumping = true;
|
_isJumping = true;
|
||||||
|
|
||||||
|
// Clear flashing messages when starting a new jump
|
||||||
|
ref.read(flashingMessagesProvider.notifier).state = {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
talker.log('Fetching message $messageId');
|
talker.log('Fetching message $messageId');
|
||||||
final message = await fetchMessageById(messageId);
|
final message = await fetchMessageById(messageId);
|
||||||
@@ -753,10 +933,11 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
talker.log(
|
talker.log(
|
||||||
'Message $messageId not in current state, loading messages around it',
|
'Message $messageId not in current state, calculating position and loading messages around it',
|
||||||
);
|
);
|
||||||
|
|
||||||
// Count messages newer than this one
|
// Count messages newer than the target message to calculate optimal offset
|
||||||
|
// Use full message list (not filtered by search) for accurate position calculation
|
||||||
final query = _database.customSelect(
|
final query = _database.customSelect(
|
||||||
'SELECT COUNT(*) as count FROM chat_messages WHERE room_id = ? AND created_at > ?',
|
'SELECT COUNT(*) as count FROM chat_messages WHERE room_id = ? AND created_at > ?',
|
||||||
variables: [
|
variables: [
|
||||||
@@ -768,13 +949,17 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
final result = await query.getSingle();
|
final result = await query.getSingle();
|
||||||
final newerCount = result.read<int>('count');
|
final newerCount = result.read<int>('count');
|
||||||
|
|
||||||
// Load messages around this position
|
// Calculate offset to position target message in the middle of the loaded chunk
|
||||||
|
const chunkSize = 100; // Load 100 messages around the target
|
||||||
final offset =
|
final offset =
|
||||||
(newerCount - _pageSize ~/ 2).clamp(0, double.infinity).toInt();
|
(newerCount - chunkSize ~/ 2).clamp(0, double.infinity).toInt();
|
||||||
talker.log('Loading messages with offset $offset, take $_pageSize');
|
talker.log(
|
||||||
final loadedMessages = await _getCachedMessages(
|
'Calculated offset $offset for target message (newer: $newerCount, chunk: $chunkSize)',
|
||||||
|
);
|
||||||
|
// Use full message list (not filtered by search) for jump operations
|
||||||
|
final loadedMessages = await _getAllMessagesForJump(
|
||||||
offset: offset,
|
offset: offset,
|
||||||
take: _pageSize,
|
take: chunkSize,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check if loaded messages are already in current state
|
// Check if loaded messages are already in current state
|
||||||
@@ -786,7 +971,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (newMessages.isNotEmpty) {
|
if (newMessages.isNotEmpty) {
|
||||||
// Merge with current messages
|
// Merge with current messages more safely
|
||||||
final allMessages = [...currentMessages, ...newMessages];
|
final allMessages = [...currentMessages, ...newMessages];
|
||||||
final uniqueMessages = <LocalChatMessage>[];
|
final uniqueMessages = <LocalChatMessage>[];
|
||||||
final seenIds = <String>{};
|
final seenIds = <String>{};
|
||||||
@@ -795,17 +980,37 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
uniqueMessages.add(message);
|
uniqueMessages.add(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_sortMessages(uniqueMessages);
|
await _updateStateSafely(uniqueMessages);
|
||||||
state = AsyncValue.data(uniqueMessages);
|
|
||||||
talker.log(
|
talker.log(
|
||||||
'Updated state with ${uniqueMessages.length} total messages',
|
'Updated state with ${uniqueMessages.length} total messages',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wait a bit for the UI to rebuild with new messages
|
||||||
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
final finalIndex = (state.value ?? []).indexWhere(
|
final finalIndex = (state.value ?? []).indexWhere(
|
||||||
(m) => m.id == messageId,
|
(m) => m.id == messageId,
|
||||||
);
|
);
|
||||||
talker.log('Final index for message $messageId is $finalIndex');
|
talker.log('Final index for message $messageId is $finalIndex');
|
||||||
|
|
||||||
|
// Verify the message is actually in the list before returning
|
||||||
|
if (finalIndex == -1) {
|
||||||
|
talker.log(
|
||||||
|
'Message $messageId still not found after loading, trying direct fetch',
|
||||||
|
);
|
||||||
|
// Try to fetch and add the specific message if it's still not found
|
||||||
|
final directMessage = await fetchMessageById(messageId);
|
||||||
|
if (directMessage != null) {
|
||||||
|
final currentList = state.value ?? [];
|
||||||
|
final updatedList = [...currentList, directMessage];
|
||||||
|
await _updateStateSafely(updatedList);
|
||||||
|
final newIndex = updatedList.indexWhere((m) => m.id == messageId);
|
||||||
|
talker.log('Added message directly, new index: $newIndex');
|
||||||
|
return newIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return finalIndex;
|
return finalIndex;
|
||||||
} finally {
|
} finally {
|
||||||
_isJumping = false;
|
_isJumping = false;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ part of 'messages_notifier.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$messagesNotifierHash() => r'3aad1491b777570913f3867abd280fa59949b1f1';
|
String _$messagesNotifierHash() => r'6adefd9152cdd686c2a863964993f24c42d405b5';
|
||||||
|
|
||||||
/// Copied from Dart SDK
|
/// Copied from Dart SDK
|
||||||
class _SystemHash {
|
class _SystemHash {
|
||||||
|
|||||||
476
lib/pods/chat/webrtc_manager.dart
Normal file
476
lib/pods/chat/webrtc_manager.dart
Normal file
@@ -0,0 +1,476 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||||
|
import 'package:island/models/account.dart';
|
||||||
|
import 'package:island/pods/chat/webrtc_signaling.dart';
|
||||||
|
import 'package:island/pods/userinfo.dart';
|
||||||
|
import 'package:island/talker.dart';
|
||||||
|
|
||||||
|
class WebRTCParticipant {
|
||||||
|
final String id;
|
||||||
|
final String name;
|
||||||
|
final SnAccount userinfo;
|
||||||
|
RTCPeerConnection? peerConnection;
|
||||||
|
MediaStream? remoteStream;
|
||||||
|
List<RTCIceCandidate> remoteCandidates = [];
|
||||||
|
bool isAudioEnabled = true;
|
||||||
|
bool isVideoEnabled = false;
|
||||||
|
bool isConnected = false;
|
||||||
|
bool isLocal = false;
|
||||||
|
double audioLevel = 0.0;
|
||||||
|
|
||||||
|
WebRTCParticipant({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.userinfo,
|
||||||
|
this.isAudioEnabled = true,
|
||||||
|
this.isVideoEnabled = false,
|
||||||
|
this.isLocal = false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class WebRTCManager {
|
||||||
|
final String roomId;
|
||||||
|
final String serverUrl;
|
||||||
|
late WebRTCSignaling _signaling;
|
||||||
|
final Map<String, WebRTCParticipant> _participants = {};
|
||||||
|
final Map<String, RTCPeerConnection> _peerConnections = {};
|
||||||
|
|
||||||
|
MediaStream? _localStream;
|
||||||
|
Timer? _audioLevelTimer;
|
||||||
|
|
||||||
|
MediaStream? get localStream => _localStream;
|
||||||
|
final StreamController<WebRTCParticipant> _participantController =
|
||||||
|
StreamController<WebRTCParticipant>.broadcast();
|
||||||
|
final StreamController<String> _participantLeftController =
|
||||||
|
StreamController<String>.broadcast();
|
||||||
|
|
||||||
|
Stream<WebRTCParticipant> get onParticipantJoined =>
|
||||||
|
_participantController.stream;
|
||||||
|
Stream<String> get onParticipantLeft => _participantLeftController.stream;
|
||||||
|
|
||||||
|
WebRTCManager({required this.roomId, required this.serverUrl}) {
|
||||||
|
_signaling = WebRTCSignaling(roomId: roomId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> initialize(Ref ref) async {
|
||||||
|
final user = ref.watch(userInfoProvider).value!;
|
||||||
|
_signaling.userId = user.id;
|
||||||
|
_signaling.userName = user.name;
|
||||||
|
_signaling.user = user;
|
||||||
|
await _initializeLocalStream();
|
||||||
|
_setupSignalingListeners();
|
||||||
|
await _signaling.connect(ref);
|
||||||
|
_startAudioLevelMonitoring();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _initializeLocalStream() async {
|
||||||
|
try {
|
||||||
|
_localStream = await navigator.mediaDevices.getUserMedia({
|
||||||
|
'audio': true,
|
||||||
|
'video': true,
|
||||||
|
});
|
||||||
|
talker.info('[WebRTC] Local stream initialized');
|
||||||
|
} catch (e) {
|
||||||
|
talker.error('[WebRTC] Failed to initialize local stream: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setupSignalingListeners() {
|
||||||
|
_signaling.messages.listen((message) async {
|
||||||
|
switch (message.type) {
|
||||||
|
case 'offer':
|
||||||
|
await _handleOffer(message.accountId, message.account, message.data);
|
||||||
|
break;
|
||||||
|
case 'answer':
|
||||||
|
await _handleAnswer(message.accountId, message.data);
|
||||||
|
break;
|
||||||
|
case 'ice-candidate':
|
||||||
|
await _handleIceCandidate(message.accountId, message.data);
|
||||||
|
break;
|
||||||
|
// CHANGED: Listen for new users joining the room.
|
||||||
|
case 'user-joined':
|
||||||
|
await _handleUserJoined(message.accountId, message.account);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
talker.warning(
|
||||||
|
'[WebRTC Manager] Receieved an unknown type singaling message: ${message.type} with ${message.data}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// CHANGED: The welcome message now drives connection initiation.
|
||||||
|
_signaling.welcomeMessages.listen((welcome) {
|
||||||
|
talker.info('[WebRTC Manager] Connected to room: ${welcome.roomId}');
|
||||||
|
final existingParticipants =
|
||||||
|
welcome.participants; // Assuming the server sends this.
|
||||||
|
talker.info(
|
||||||
|
'[WebRTC Manager] Existing participants: $existingParticipants',
|
||||||
|
);
|
||||||
|
|
||||||
|
// The newcomer is responsible for initiating the connection to everyone else.
|
||||||
|
for (final participant in existingParticipants) {
|
||||||
|
if (participant.identity != _signaling.userId) {
|
||||||
|
if (!_participants.containsKey(participant.identity)) {
|
||||||
|
final webrtcParticipant = WebRTCParticipant(
|
||||||
|
id: participant.identity,
|
||||||
|
name: participant.name,
|
||||||
|
userinfo: participant.account!,
|
||||||
|
);
|
||||||
|
_participants[participant.identity] = webrtcParticipant;
|
||||||
|
_participantController.add(webrtcParticipant);
|
||||||
|
}
|
||||||
|
_createPeerConnection(participant.identity, isInitiator: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// CHANGED: New handler for when an existing user is notified of a new peer.
|
||||||
|
Future<void> _handleUserJoined(
|
||||||
|
String participantId,
|
||||||
|
SnAccount account,
|
||||||
|
) async {
|
||||||
|
talker.info(
|
||||||
|
'[WebRTC Manager] User joined: $participantId. Waiting for their offer.',
|
||||||
|
);
|
||||||
|
// We don't need to be the initiator here. The newcomer will send us an offer.
|
||||||
|
// We just create the peer connection to be ready for it.
|
||||||
|
if (!_peerConnections.containsKey(participantId)) {
|
||||||
|
// Create a participant object to represent the new user
|
||||||
|
if (!_participants.containsKey(participantId)) {
|
||||||
|
final participant = WebRTCParticipant(
|
||||||
|
id: participantId,
|
||||||
|
name: participantId,
|
||||||
|
userinfo: account,
|
||||||
|
); // Placeholder name
|
||||||
|
_participants[participantId] = participant;
|
||||||
|
_participantController.add(participant);
|
||||||
|
}
|
||||||
|
await _createPeerConnection(participantId, isInitiator: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _createPeerConnection(
|
||||||
|
String participantId, {
|
||||||
|
bool isInitiator = false,
|
||||||
|
}) async {
|
||||||
|
talker.info(
|
||||||
|
'[WebRTC] Creating peer connection to $participantId (initiator: $isInitiator)',
|
||||||
|
);
|
||||||
|
final configuration = {
|
||||||
|
'iceServers': [
|
||||||
|
{'urls': 'stun:stun.l.google.com:19302'},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
final peerConnection = await createPeerConnection(configuration);
|
||||||
|
_peerConnections[participantId] = peerConnection;
|
||||||
|
_participants[participantId]!.peerConnection = peerConnection;
|
||||||
|
|
||||||
|
if (_localStream != null) {
|
||||||
|
for (final track in _localStream!.getTracks()) {
|
||||||
|
await peerConnection.addTrack(track, _localStream!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
peerConnection.onTrack = (event) {
|
||||||
|
if (event.streams.isNotEmpty) {
|
||||||
|
final participant = _participants[participantId];
|
||||||
|
if (participant != null) {
|
||||||
|
participant.remoteStream = event.streams[0];
|
||||||
|
participant.isConnected = true;
|
||||||
|
|
||||||
|
// Detect video tracks and update video enabled state
|
||||||
|
final videoTracks = event.streams[0].getVideoTracks();
|
||||||
|
if (videoTracks.isNotEmpty) {
|
||||||
|
participant.isVideoEnabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_participantController.add(participant);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
peerConnection.onIceCandidate = (candidate) {
|
||||||
|
// CHANGED: Send candidate to the specific participant
|
||||||
|
_signaling.sendIceCandidate(participantId, candidate);
|
||||||
|
};
|
||||||
|
|
||||||
|
peerConnection.onConnectionState = (state) {
|
||||||
|
talker.info('[WebRTC] Connection state for $participantId: $state');
|
||||||
|
final participant = _participants[participantId];
|
||||||
|
if (participant != null) {
|
||||||
|
participant.isConnected =
|
||||||
|
state == RTCPeerConnectionState.RTCPeerConnectionStateConnected;
|
||||||
|
_participantController.add(participant);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isInitiator) {
|
||||||
|
final offer = await peerConnection.createOffer();
|
||||||
|
await peerConnection.setLocalDescription(offer);
|
||||||
|
// CHANGED: Send offer to the specific participant
|
||||||
|
_signaling.sendOffer(participantId, offer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handleOffer(
|
||||||
|
String from,
|
||||||
|
SnAccount account,
|
||||||
|
Map<String, dynamic> data,
|
||||||
|
) async {
|
||||||
|
final participantId = from;
|
||||||
|
talker.info('[WebRTC Manager] Handling offer from $participantId');
|
||||||
|
final offer = RTCSessionDescription(data['sdp'], data['type']);
|
||||||
|
|
||||||
|
if (!_peerConnections.containsKey(participantId)) {
|
||||||
|
if (!_participants.containsKey(participantId)) {
|
||||||
|
final participant = WebRTCParticipant(
|
||||||
|
id: participantId,
|
||||||
|
name: participantId,
|
||||||
|
userinfo: account,
|
||||||
|
);
|
||||||
|
_participants[participantId] = participant;
|
||||||
|
_participantController.add(participant);
|
||||||
|
}
|
||||||
|
await _createPeerConnection(participantId, isInitiator: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
final peerConnection = _peerConnections[participantId]!;
|
||||||
|
await peerConnection.setRemoteDescription(offer);
|
||||||
|
|
||||||
|
final answer = await peerConnection.createAnswer();
|
||||||
|
await peerConnection.setLocalDescription(answer);
|
||||||
|
// CHANGED: Send answer to the specific participant
|
||||||
|
_signaling.sendAnswer(participantId, answer);
|
||||||
|
|
||||||
|
// Process any queued ICE candidates
|
||||||
|
final participant = _participants[participantId];
|
||||||
|
if (participant != null) {
|
||||||
|
for (final candidate in participant.remoteCandidates) {
|
||||||
|
await peerConnection.addCandidate(candidate);
|
||||||
|
}
|
||||||
|
participant.remoteCandidates.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handleAnswer(String from, Map<String, dynamic> data) async {
|
||||||
|
final participantId = from;
|
||||||
|
talker.info('[WebRTC Manager] Handling answer from $participantId');
|
||||||
|
final answer = RTCSessionDescription(data['sdp'], data['type']);
|
||||||
|
|
||||||
|
final peerConnection = _peerConnections[participantId];
|
||||||
|
if (peerConnection != null) {
|
||||||
|
await peerConnection.setRemoteDescription(answer);
|
||||||
|
|
||||||
|
// Process any queued ICE candidates
|
||||||
|
final participant = _participants[participantId];
|
||||||
|
if (participant != null) {
|
||||||
|
for (final candidate in participant.remoteCandidates) {
|
||||||
|
await peerConnection.addCandidate(candidate);
|
||||||
|
}
|
||||||
|
participant.remoteCandidates.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handleIceCandidate(
|
||||||
|
String from,
|
||||||
|
Map<String, dynamic> data,
|
||||||
|
) async {
|
||||||
|
final participantId = from;
|
||||||
|
final candidate = RTCIceCandidate(
|
||||||
|
data['candidate'],
|
||||||
|
data['sdpMid'],
|
||||||
|
data['sdpMLineIndex'],
|
||||||
|
);
|
||||||
|
|
||||||
|
final participant = _participants[participantId];
|
||||||
|
if (participant != null) {
|
||||||
|
final pc = participant.peerConnection;
|
||||||
|
if (pc != null) {
|
||||||
|
await pc.addCandidate(candidate);
|
||||||
|
} else {
|
||||||
|
participant.remoteCandidates.add(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> replaceMediaStream(Map<String, dynamic> constraints) async {
|
||||||
|
try {
|
||||||
|
final newStream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||||
|
final newVideoTrack = newStream.getVideoTracks().firstOrNull;
|
||||||
|
final newAudioTrack = newStream.getAudioTracks().firstOrNull;
|
||||||
|
|
||||||
|
if (_localStream != null) {
|
||||||
|
final oldVideoTrack = _localStream!.getVideoTracks().firstOrNull;
|
||||||
|
final oldAudioTrack = _localStream!.getAudioTracks().firstOrNull;
|
||||||
|
|
||||||
|
// Replace tracks in all existing peer connections
|
||||||
|
for (final pc in _peerConnections.values) {
|
||||||
|
final senders = await pc.getSenders();
|
||||||
|
for (final sender in senders) {
|
||||||
|
if (newVideoTrack != null && sender.track == oldVideoTrack) {
|
||||||
|
await sender.replaceTrack(newVideoTrack);
|
||||||
|
} else if (newAudioTrack != null && sender.track == oldAudioTrack) {
|
||||||
|
await sender.replaceTrack(newAudioTrack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop old tracks and update local stream
|
||||||
|
for (final track in _localStream!.getTracks()) {
|
||||||
|
track.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_localStream = newStream;
|
||||||
|
talker.info('[WebRTC] Media stream replaced with new constraints');
|
||||||
|
} catch (e) {
|
||||||
|
talker.error('[WebRTC] Failed to replace media stream: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> toggleMicrophone(bool enabled) async {
|
||||||
|
if (_localStream != null) {
|
||||||
|
final audioTracks = _localStream!.getAudioTracks();
|
||||||
|
for (final track in audioTracks) {
|
||||||
|
track.enabled = enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> toggleCamera(bool enabled) async {
|
||||||
|
if (_localStream != null) {
|
||||||
|
_localStream!.getVideoTracks().forEach((track) {
|
||||||
|
track.enabled = enabled;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> switchCamera(String deviceId) async {
|
||||||
|
await replaceMediaStream({
|
||||||
|
'audio': _localStream?.getAudioTracks().isNotEmpty ?? true,
|
||||||
|
'video': {'deviceId': deviceId},
|
||||||
|
});
|
||||||
|
talker.info('[WebRTC] Switched to camera device: $deviceId');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> switchMicrophone(String deviceId) async {
|
||||||
|
await replaceMediaStream({
|
||||||
|
'audio': {'deviceId': deviceId},
|
||||||
|
'video': _localStream?.getVideoTracks().isNotEmpty ?? true,
|
||||||
|
});
|
||||||
|
talker.info('[WebRTC] Switched to microphone device: $deviceId');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<MediaDeviceInfo>> getVideoDevices() async {
|
||||||
|
try {
|
||||||
|
final devices = await navigator.mediaDevices.enumerateDevices();
|
||||||
|
return devices.where((device) => device.kind == 'videoinput').toList();
|
||||||
|
} catch (e) {
|
||||||
|
talker.error('[WebRTC] Failed to enumerate video devices: $e');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<MediaDeviceInfo>> getAudioDevices() async {
|
||||||
|
try {
|
||||||
|
final devices = await navigator.mediaDevices.enumerateDevices();
|
||||||
|
return devices.where((device) => device.kind == 'audioinput').toList();
|
||||||
|
} catch (e) {
|
||||||
|
talker.error('[WebRTC] Failed to enumerate audio devices: $e');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _startAudioLevelMonitoring() {
|
||||||
|
_audioLevelTimer?.cancel();
|
||||||
|
_audioLevelTimer = Timer.periodic(const Duration(milliseconds: 100), (_) {
|
||||||
|
_updateAudioLevels();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _stopAudioLevelMonitoring() {
|
||||||
|
_audioLevelTimer?.cancel();
|
||||||
|
_audioLevelTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _updateAudioLevels() async {
|
||||||
|
bool hasUpdates = false;
|
||||||
|
|
||||||
|
for (final participant in _participants.values) {
|
||||||
|
if (participant.remoteStream != null && participant.isAudioEnabled) {
|
||||||
|
final audioTracks = participant.remoteStream!.getAudioTracks();
|
||||||
|
if (audioTracks.isNotEmpty) {
|
||||||
|
try {
|
||||||
|
// Try to get stats for more accurate audio level detection
|
||||||
|
final pc = participant.peerConnection;
|
||||||
|
if (pc != null) {
|
||||||
|
final stats = await pc.getStats();
|
||||||
|
double maxAudioLevel = 0.0;
|
||||||
|
|
||||||
|
// Look for audio receiver stats
|
||||||
|
for (var report in stats) {
|
||||||
|
if (report.type == 'inbound-rtp' &&
|
||||||
|
report.values['mediaType'] == 'audio') {
|
||||||
|
final audioLevel = report.values['audioLevel'] as double?;
|
||||||
|
if (audioLevel != null && audioLevel > maxAudioLevel) {
|
||||||
|
maxAudioLevel = audioLevel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we got stats, use them; otherwise use a simple heuristic
|
||||||
|
if (maxAudioLevel > 0) {
|
||||||
|
participant.audioLevel = maxAudioLevel.clamp(0.0, 1.0);
|
||||||
|
} else {
|
||||||
|
// Simple heuristic: if audio track is enabled, assume some level
|
||||||
|
// In a real app, you'd analyze the actual audio data
|
||||||
|
participant.audioLevel = audioTracks[0].enabled ? 0.5 : 0.0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback for local participant or when no PC available
|
||||||
|
participant.audioLevel = participant.isLocal ? 0.0 : 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasUpdates = true;
|
||||||
|
} catch (e) {
|
||||||
|
talker.warning('[WebRTC] Failed to update audio level for ${participant.id}: $e');
|
||||||
|
participant.audioLevel = 0.0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
participant.audioLevel = 0.0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
participant.audioLevel = 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify listeners if there were updates (throttled to avoid excessive updates)
|
||||||
|
if (hasUpdates) {
|
||||||
|
// This will trigger UI updates for speaking indicators
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<WebRTCParticipant> get participants => _participants.values.toList();
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
_stopAudioLevelMonitoring();
|
||||||
|
_signaling.disconnect();
|
||||||
|
for (final pc in _peerConnections.values) {
|
||||||
|
pc.close();
|
||||||
|
}
|
||||||
|
_peerConnections.clear();
|
||||||
|
for (var p in _participants.values) {
|
||||||
|
p.remoteCandidates.clear();
|
||||||
|
}
|
||||||
|
_participants.clear();
|
||||||
|
_localStream?.dispose();
|
||||||
|
_participantController.close();
|
||||||
|
_participantLeftController.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
211
lib/pods/chat/webrtc_signaling.dart
Normal file
211
lib/pods/chat/webrtc_signaling.dart
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
import 'package:island/models/account.dart';
|
||||||
|
import 'package:island/models/chat.dart';
|
||||||
|
import 'package:island/pods/config.dart';
|
||||||
|
import 'package:island/pods/network.dart';
|
||||||
|
import 'package:island/pods/websocket.dart';
|
||||||
|
import 'package:web_socket_channel/io.dart';
|
||||||
|
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||||
|
import 'package:island/talker.dart';
|
||||||
|
|
||||||
|
part 'webrtc_signaling.freezed.dart';
|
||||||
|
part 'webrtc_signaling.g.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
sealed class SignalingMessage with _$SignalingMessage {
|
||||||
|
const factory SignalingMessage({
|
||||||
|
required String type,
|
||||||
|
// CHANGED: Added 'to' field for directed messaging
|
||||||
|
String? to,
|
||||||
|
required String accountId,
|
||||||
|
required SnAccount account,
|
||||||
|
required Map<String, dynamic> data,
|
||||||
|
}) = _SignalingMessage;
|
||||||
|
|
||||||
|
factory SignalingMessage.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SignalingMessageFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
sealed class WebRTCWelcomeMessage with _$WebRTCWelcomeMessage {
|
||||||
|
const factory WebRTCWelcomeMessage({
|
||||||
|
required String userId,
|
||||||
|
required String roomId,
|
||||||
|
required String message,
|
||||||
|
required String timestamp,
|
||||||
|
// CHANGED: Added participants list
|
||||||
|
@Default([]) List<CallParticipant> participants,
|
||||||
|
}) = _WebRTCWelcomeMessage;
|
||||||
|
|
||||||
|
factory WebRTCWelcomeMessage.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$WebRTCWelcomeMessageFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
class WebRTCSignaling {
|
||||||
|
final String roomId;
|
||||||
|
late final String userId;
|
||||||
|
late final String userName;
|
||||||
|
late SnAccount user;
|
||||||
|
final StreamController<SignalingMessage> _messageController =
|
||||||
|
StreamController<SignalingMessage>.broadcast();
|
||||||
|
final StreamController<WebRTCWelcomeMessage> _welcomeController =
|
||||||
|
StreamController<WebRTCWelcomeMessage>.broadcast();
|
||||||
|
WebSocketChannel? _channel;
|
||||||
|
Timer? _heartbeatTimer;
|
||||||
|
|
||||||
|
Stream<SignalingMessage> get messages => _messageController.stream;
|
||||||
|
Stream<WebRTCWelcomeMessage> get welcomeMessages => _welcomeController.stream;
|
||||||
|
|
||||||
|
WebRTCSignaling({required this.roomId});
|
||||||
|
|
||||||
|
Future<void> connect(Ref ref) async {
|
||||||
|
final baseUrl = ref.watch(serverUrlProvider);
|
||||||
|
final token = await getToken(ref.watch(tokenProvider));
|
||||||
|
|
||||||
|
final url = '$baseUrl/sphere/chat/realtime/$roomId'.replaceFirst(
|
||||||
|
'http',
|
||||||
|
'ws',
|
||||||
|
);
|
||||||
|
|
||||||
|
talker.info('[WebRTC Signaling] Trying connecting to $url');
|
||||||
|
try {
|
||||||
|
if (kIsWeb) {
|
||||||
|
_channel = WebSocketChannel.connect(Uri.parse('$url?tk=$token'));
|
||||||
|
} else {
|
||||||
|
_channel = IOWebSocketChannel.connect(
|
||||||
|
Uri.parse(url),
|
||||||
|
headers: {'Authorization': 'AtField $token'},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await _channel!.ready;
|
||||||
|
|
||||||
|
// Start heartbeat timer
|
||||||
|
_heartbeatTimer = Timer.periodic(const Duration(seconds: 30), (timer) => _sendHeartbeat());
|
||||||
|
|
||||||
|
_channel!.stream.listen(
|
||||||
|
(data) {
|
||||||
|
final dataStr =
|
||||||
|
data is Uint8List ? utf8.decode(data) : data.toString();
|
||||||
|
final packet = WebSocketPacket.fromJson(jsonDecode(dataStr));
|
||||||
|
talker.info(
|
||||||
|
'[WebRTC Signaling] Recieved a singal message with packet type: ${packet.type}',
|
||||||
|
);
|
||||||
|
if (packet.type == 'webrtc') {
|
||||||
|
try {
|
||||||
|
final welcomeMessage = WebRTCWelcomeMessage.fromJson(
|
||||||
|
packet.data!,
|
||||||
|
);
|
||||||
|
_welcomeController.add(welcomeMessage);
|
||||||
|
talker.info(
|
||||||
|
'[WebRTC Signaling] Welcome message received: ${welcomeMessage.message}',
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
talker.error(
|
||||||
|
'[WebRTC Signaling] Failed to parse welcome message: $e',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (packet.type == 'webrtc.signal') {
|
||||||
|
try {
|
||||||
|
final signalingMessage = SignalingMessage.fromJson(packet.data!);
|
||||||
|
// CHANGED: Ensure we only process messages intended for us if the 'to' field is present
|
||||||
|
if (signalingMessage.to == null ||
|
||||||
|
signalingMessage.to == userId) {
|
||||||
|
_messageController.add(signalingMessage);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
talker.error(
|
||||||
|
'[WebRTC Signaling] Failed to parse signaling message: $e',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (error) {
|
||||||
|
talker.error('[WebRTC Signaling] WebSocket error: $error');
|
||||||
|
_messageController.addError(error);
|
||||||
|
_welcomeController.addError(error);
|
||||||
|
},
|
||||||
|
onDone: () {
|
||||||
|
talker.info('[WebRTC Signaling] WebSocket connection closed');
|
||||||
|
_messageController.close();
|
||||||
|
_welcomeController.close();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
talker.error('[WebRTC Signaling] Failed to connect: $err');
|
||||||
|
_messageController.addError(err);
|
||||||
|
_welcomeController.addError(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void sendMessage(SignalingMessage message) {
|
||||||
|
if (_channel == null) return;
|
||||||
|
talker.info(
|
||||||
|
'[WebRTC Signaling] Sending a message with message type: ${message.type} to ${message.to}',
|
||||||
|
);
|
||||||
|
final packet = WebSocketPacket(
|
||||||
|
type: 'webrtc.signal',
|
||||||
|
data: message.toJson(),
|
||||||
|
);
|
||||||
|
_channel!.sink.add(jsonEncode(packet.toJson()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// CHANGED: All send methods now correctly use the `to` parameter
|
||||||
|
void sendOffer(String to, RTCSessionDescription offer) {
|
||||||
|
sendMessage(
|
||||||
|
SignalingMessage(
|
||||||
|
type: 'offer',
|
||||||
|
to: to,
|
||||||
|
accountId: userId,
|
||||||
|
account: user,
|
||||||
|
data: {'sdp': offer.sdp, 'type': offer.type},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void sendAnswer(String to, RTCSessionDescription answer) {
|
||||||
|
sendMessage(
|
||||||
|
SignalingMessage(
|
||||||
|
type: 'answer',
|
||||||
|
to: to,
|
||||||
|
accountId: userId,
|
||||||
|
account: user,
|
||||||
|
data: {'sdp': answer.sdp, 'type': answer.type},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void sendIceCandidate(String to, RTCIceCandidate candidate) {
|
||||||
|
sendMessage(
|
||||||
|
SignalingMessage(
|
||||||
|
type: 'ice-candidate',
|
||||||
|
to: to,
|
||||||
|
accountId: userId,
|
||||||
|
account: user,
|
||||||
|
data: {
|
||||||
|
'candidate': candidate.candidate,
|
||||||
|
'sdpMid': candidate.sdpMid,
|
||||||
|
'sdpMLineIndex': candidate.sdpMLineIndex,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _sendHeartbeat() {
|
||||||
|
if (_channel == null) return;
|
||||||
|
talker.info('[WebRTC Signaling] Sending heartbeat');
|
||||||
|
final packet = WebSocketPacket(type: 'heartbeat', data: null);
|
||||||
|
_channel!.sink.add(jsonEncode(packet.toJson()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void disconnect() {
|
||||||
|
_heartbeatTimer?.cancel();
|
||||||
|
_channel?.sink.close();
|
||||||
|
_messageController.close();
|
||||||
|
_welcomeController.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
611
lib/pods/chat/webrtc_signaling.freezed.dart
Normal file
611
lib/pods/chat/webrtc_signaling.freezed.dart
Normal file
@@ -0,0 +1,611 @@
|
|||||||
|
// 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 'webrtc_signaling.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// FreezedGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// dart format off
|
||||||
|
T _$identity<T>(T value) => value;
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$SignalingMessage implements DiagnosticableTreeMixin {
|
||||||
|
|
||||||
|
String get type;// CHANGED: Added 'to' field for directed messaging
|
||||||
|
String? get to; String get accountId; SnAccount get account; Map<String, dynamic> get data;
|
||||||
|
/// Create a copy of SignalingMessage
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SignalingMessageCopyWith<SignalingMessage> get copyWith => _$SignalingMessageCopyWithImpl<SignalingMessage>(this as SignalingMessage, _$identity);
|
||||||
|
|
||||||
|
/// Serializes this SignalingMessage to a JSON map.
|
||||||
|
Map<String, dynamic> toJson();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||||
|
properties
|
||||||
|
..add(DiagnosticsProperty('type', 'SignalingMessage'))
|
||||||
|
..add(DiagnosticsProperty('type', type))..add(DiagnosticsProperty('to', to))..add(DiagnosticsProperty('accountId', accountId))..add(DiagnosticsProperty('account', account))..add(DiagnosticsProperty('data', data));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is SignalingMessage&&(identical(other.type, type) || other.type == type)&&(identical(other.to, to) || other.to == to)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.account, account) || other.account == account)&&const DeepCollectionEquality().equals(other.data, data));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,type,to,accountId,account,const DeepCollectionEquality().hash(data));
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
|
||||||
|
return 'SignalingMessage(type: $type, to: $to, accountId: $accountId, account: $account, data: $data)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class $SignalingMessageCopyWith<$Res> {
|
||||||
|
factory $SignalingMessageCopyWith(SignalingMessage value, $Res Function(SignalingMessage) _then) = _$SignalingMessageCopyWithImpl;
|
||||||
|
@useResult
|
||||||
|
$Res call({
|
||||||
|
String type, String? to, String accountId, SnAccount account, Map<String, dynamic> data
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
$SnAccountCopyWith<$Res> get account;
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class _$SignalingMessageCopyWithImpl<$Res>
|
||||||
|
implements $SignalingMessageCopyWith<$Res> {
|
||||||
|
_$SignalingMessageCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final SignalingMessage _self;
|
||||||
|
final $Res Function(SignalingMessage) _then;
|
||||||
|
|
||||||
|
/// Create a copy of SignalingMessage
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline') @override $Res call({Object? type = null,Object? to = freezed,Object? accountId = null,Object? account = null,Object? data = null,}) {
|
||||||
|
return _then(_self.copyWith(
|
||||||
|
type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,to: freezed == to ? _self.to : to // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,account: null == account ? _self.account : account // ignore: cast_nullable_to_non_nullable
|
||||||
|
as SnAccount,data: null == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<String, dynamic>,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
/// Create a copy of SignalingMessage
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnAccountCopyWith<$Res> get account {
|
||||||
|
|
||||||
|
return $SnAccountCopyWith<$Res>(_self.account, (value) {
|
||||||
|
return _then(_self.copyWith(account: value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Adds pattern-matching-related methods to [SignalingMessage].
|
||||||
|
extension SignalingMessagePatterns on SignalingMessage {
|
||||||
|
/// 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( _SignalingMessage value)? $default,{required TResult orElse(),}){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SignalingMessage() 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( _SignalingMessage value) $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SignalingMessage():
|
||||||
|
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( _SignalingMessage value)? $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SignalingMessage() 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 type, String? to, String accountId, SnAccount account, Map<String, dynamic> data)? $default,{required TResult orElse(),}) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SignalingMessage() when $default != null:
|
||||||
|
return $default(_that.type,_that.to,_that.accountId,_that.account,_that.data);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 type, String? to, String accountId, SnAccount account, Map<String, dynamic> data) $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SignalingMessage():
|
||||||
|
return $default(_that.type,_that.to,_that.accountId,_that.account,_that.data);}
|
||||||
|
}
|
||||||
|
/// 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 type, String? to, String accountId, SnAccount account, Map<String, dynamic> data)? $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SignalingMessage() when $default != null:
|
||||||
|
return $default(_that.type,_that.to,_that.accountId,_that.account,_that.data);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
|
||||||
|
class _SignalingMessage with DiagnosticableTreeMixin implements SignalingMessage {
|
||||||
|
const _SignalingMessage({required this.type, this.to, required this.accountId, required this.account, required final Map<String, dynamic> data}): _data = data;
|
||||||
|
factory _SignalingMessage.fromJson(Map<String, dynamic> json) => _$SignalingMessageFromJson(json);
|
||||||
|
|
||||||
|
@override final String type;
|
||||||
|
// CHANGED: Added 'to' field for directed messaging
|
||||||
|
@override final String? to;
|
||||||
|
@override final String accountId;
|
||||||
|
@override final SnAccount account;
|
||||||
|
final Map<String, dynamic> _data;
|
||||||
|
@override Map<String, dynamic> get data {
|
||||||
|
if (_data is EqualUnmodifiableMapView) return _data;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableMapView(_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Create a copy of SignalingMessage
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$SignalingMessageCopyWith<_SignalingMessage> get copyWith => __$SignalingMessageCopyWithImpl<_SignalingMessage>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$SignalingMessageToJson(this, );
|
||||||
|
}
|
||||||
|
@override
|
||||||
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||||
|
properties
|
||||||
|
..add(DiagnosticsProperty('type', 'SignalingMessage'))
|
||||||
|
..add(DiagnosticsProperty('type', type))..add(DiagnosticsProperty('to', to))..add(DiagnosticsProperty('accountId', accountId))..add(DiagnosticsProperty('account', account))..add(DiagnosticsProperty('data', data));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SignalingMessage&&(identical(other.type, type) || other.type == type)&&(identical(other.to, to) || other.to == to)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.account, account) || other.account == account)&&const DeepCollectionEquality().equals(other._data, _data));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,type,to,accountId,account,const DeepCollectionEquality().hash(_data));
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
|
||||||
|
return 'SignalingMessage(type: $type, to: $to, accountId: $accountId, account: $account, data: $data)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class _$SignalingMessageCopyWith<$Res> implements $SignalingMessageCopyWith<$Res> {
|
||||||
|
factory _$SignalingMessageCopyWith(_SignalingMessage value, $Res Function(_SignalingMessage) _then) = __$SignalingMessageCopyWithImpl;
|
||||||
|
@override @useResult
|
||||||
|
$Res call({
|
||||||
|
String type, String? to, String accountId, SnAccount account, Map<String, dynamic> data
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
@override $SnAccountCopyWith<$Res> get account;
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class __$SignalingMessageCopyWithImpl<$Res>
|
||||||
|
implements _$SignalingMessageCopyWith<$Res> {
|
||||||
|
__$SignalingMessageCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final _SignalingMessage _self;
|
||||||
|
final $Res Function(_SignalingMessage) _then;
|
||||||
|
|
||||||
|
/// Create a copy of SignalingMessage
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @pragma('vm:prefer-inline') $Res call({Object? type = null,Object? to = freezed,Object? accountId = null,Object? account = null,Object? data = null,}) {
|
||||||
|
return _then(_SignalingMessage(
|
||||||
|
type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,to: freezed == to ? _self.to : to // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,account: null == account ? _self.account : account // ignore: cast_nullable_to_non_nullable
|
||||||
|
as SnAccount,data: null == data ? _self._data : data // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<String, dynamic>,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a copy of SignalingMessage
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnAccountCopyWith<$Res> get account {
|
||||||
|
|
||||||
|
return $SnAccountCopyWith<$Res>(_self.account, (value) {
|
||||||
|
return _then(_self.copyWith(account: value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$WebRTCWelcomeMessage implements DiagnosticableTreeMixin {
|
||||||
|
|
||||||
|
String get userId; String get roomId; String get message; String get timestamp;// CHANGED: Added participants list
|
||||||
|
List<CallParticipant> get participants;
|
||||||
|
/// Create a copy of WebRTCWelcomeMessage
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$WebRTCWelcomeMessageCopyWith<WebRTCWelcomeMessage> get copyWith => _$WebRTCWelcomeMessageCopyWithImpl<WebRTCWelcomeMessage>(this as WebRTCWelcomeMessage, _$identity);
|
||||||
|
|
||||||
|
/// Serializes this WebRTCWelcomeMessage to a JSON map.
|
||||||
|
Map<String, dynamic> toJson();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||||
|
properties
|
||||||
|
..add(DiagnosticsProperty('type', 'WebRTCWelcomeMessage'))
|
||||||
|
..add(DiagnosticsProperty('userId', userId))..add(DiagnosticsProperty('roomId', roomId))..add(DiagnosticsProperty('message', message))..add(DiagnosticsProperty('timestamp', timestamp))..add(DiagnosticsProperty('participants', participants));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is WebRTCWelcomeMessage&&(identical(other.userId, userId) || other.userId == userId)&&(identical(other.roomId, roomId) || other.roomId == roomId)&&(identical(other.message, message) || other.message == message)&&(identical(other.timestamp, timestamp) || other.timestamp == timestamp)&&const DeepCollectionEquality().equals(other.participants, participants));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,userId,roomId,message,timestamp,const DeepCollectionEquality().hash(participants));
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
|
||||||
|
return 'WebRTCWelcomeMessage(userId: $userId, roomId: $roomId, message: $message, timestamp: $timestamp, participants: $participants)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class $WebRTCWelcomeMessageCopyWith<$Res> {
|
||||||
|
factory $WebRTCWelcomeMessageCopyWith(WebRTCWelcomeMessage value, $Res Function(WebRTCWelcomeMessage) _then) = _$WebRTCWelcomeMessageCopyWithImpl;
|
||||||
|
@useResult
|
||||||
|
$Res call({
|
||||||
|
String userId, String roomId, String message, String timestamp, List<CallParticipant> participants
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class _$WebRTCWelcomeMessageCopyWithImpl<$Res>
|
||||||
|
implements $WebRTCWelcomeMessageCopyWith<$Res> {
|
||||||
|
_$WebRTCWelcomeMessageCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final WebRTCWelcomeMessage _self;
|
||||||
|
final $Res Function(WebRTCWelcomeMessage) _then;
|
||||||
|
|
||||||
|
/// Create a copy of WebRTCWelcomeMessage
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline') @override $Res call({Object? userId = null,Object? roomId = null,Object? message = null,Object? timestamp = null,Object? participants = null,}) {
|
||||||
|
return _then(_self.copyWith(
|
||||||
|
userId: null == userId ? _self.userId : userId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,roomId: null == roomId ? _self.roomId : roomId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,message: null == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,timestamp: null == timestamp ? _self.timestamp : timestamp // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,participants: null == participants ? _self.participants : participants // ignore: cast_nullable_to_non_nullable
|
||||||
|
as List<CallParticipant>,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Adds pattern-matching-related methods to [WebRTCWelcomeMessage].
|
||||||
|
extension WebRTCWelcomeMessagePatterns on WebRTCWelcomeMessage {
|
||||||
|
/// 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( _WebRTCWelcomeMessage value)? $default,{required TResult orElse(),}){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _WebRTCWelcomeMessage() 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( _WebRTCWelcomeMessage value) $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _WebRTCWelcomeMessage():
|
||||||
|
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( _WebRTCWelcomeMessage value)? $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _WebRTCWelcomeMessage() 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 userId, String roomId, String message, String timestamp, List<CallParticipant> participants)? $default,{required TResult orElse(),}) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _WebRTCWelcomeMessage() when $default != null:
|
||||||
|
return $default(_that.userId,_that.roomId,_that.message,_that.timestamp,_that.participants);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 userId, String roomId, String message, String timestamp, List<CallParticipant> participants) $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _WebRTCWelcomeMessage():
|
||||||
|
return $default(_that.userId,_that.roomId,_that.message,_that.timestamp,_that.participants);}
|
||||||
|
}
|
||||||
|
/// 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 userId, String roomId, String message, String timestamp, List<CallParticipant> participants)? $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _WebRTCWelcomeMessage() when $default != null:
|
||||||
|
return $default(_that.userId,_that.roomId,_that.message,_that.timestamp,_that.participants);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
|
||||||
|
class _WebRTCWelcomeMessage with DiagnosticableTreeMixin implements WebRTCWelcomeMessage {
|
||||||
|
const _WebRTCWelcomeMessage({required this.userId, required this.roomId, required this.message, required this.timestamp, final List<CallParticipant> participants = const []}): _participants = participants;
|
||||||
|
factory _WebRTCWelcomeMessage.fromJson(Map<String, dynamic> json) => _$WebRTCWelcomeMessageFromJson(json);
|
||||||
|
|
||||||
|
@override final String userId;
|
||||||
|
@override final String roomId;
|
||||||
|
@override final String message;
|
||||||
|
@override final String timestamp;
|
||||||
|
// CHANGED: Added participants list
|
||||||
|
final List<CallParticipant> _participants;
|
||||||
|
// CHANGED: Added participants list
|
||||||
|
@override@JsonKey() List<CallParticipant> get participants {
|
||||||
|
if (_participants is EqualUnmodifiableListView) return _participants;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableListView(_participants);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Create a copy of WebRTCWelcomeMessage
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$WebRTCWelcomeMessageCopyWith<_WebRTCWelcomeMessage> get copyWith => __$WebRTCWelcomeMessageCopyWithImpl<_WebRTCWelcomeMessage>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$WebRTCWelcomeMessageToJson(this, );
|
||||||
|
}
|
||||||
|
@override
|
||||||
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||||
|
properties
|
||||||
|
..add(DiagnosticsProperty('type', 'WebRTCWelcomeMessage'))
|
||||||
|
..add(DiagnosticsProperty('userId', userId))..add(DiagnosticsProperty('roomId', roomId))..add(DiagnosticsProperty('message', message))..add(DiagnosticsProperty('timestamp', timestamp))..add(DiagnosticsProperty('participants', participants));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is _WebRTCWelcomeMessage&&(identical(other.userId, userId) || other.userId == userId)&&(identical(other.roomId, roomId) || other.roomId == roomId)&&(identical(other.message, message) || other.message == message)&&(identical(other.timestamp, timestamp) || other.timestamp == timestamp)&&const DeepCollectionEquality().equals(other._participants, _participants));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,userId,roomId,message,timestamp,const DeepCollectionEquality().hash(_participants));
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
|
||||||
|
return 'WebRTCWelcomeMessage(userId: $userId, roomId: $roomId, message: $message, timestamp: $timestamp, participants: $participants)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class _$WebRTCWelcomeMessageCopyWith<$Res> implements $WebRTCWelcomeMessageCopyWith<$Res> {
|
||||||
|
factory _$WebRTCWelcomeMessageCopyWith(_WebRTCWelcomeMessage value, $Res Function(_WebRTCWelcomeMessage) _then) = __$WebRTCWelcomeMessageCopyWithImpl;
|
||||||
|
@override @useResult
|
||||||
|
$Res call({
|
||||||
|
String userId, String roomId, String message, String timestamp, List<CallParticipant> participants
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class __$WebRTCWelcomeMessageCopyWithImpl<$Res>
|
||||||
|
implements _$WebRTCWelcomeMessageCopyWith<$Res> {
|
||||||
|
__$WebRTCWelcomeMessageCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final _WebRTCWelcomeMessage _self;
|
||||||
|
final $Res Function(_WebRTCWelcomeMessage) _then;
|
||||||
|
|
||||||
|
/// Create a copy of WebRTCWelcomeMessage
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @pragma('vm:prefer-inline') $Res call({Object? userId = null,Object? roomId = null,Object? message = null,Object? timestamp = null,Object? participants = null,}) {
|
||||||
|
return _then(_WebRTCWelcomeMessage(
|
||||||
|
userId: null == userId ? _self.userId : userId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,roomId: null == roomId ? _self.roomId : roomId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,message: null == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,timestamp: null == timestamp ? _self.timestamp : timestamp // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,participants: null == participants ? _self._participants : participants // ignore: cast_nullable_to_non_nullable
|
||||||
|
as List<CallParticipant>,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// dart format on
|
||||||
49
lib/pods/chat/webrtc_signaling.g.dart
Normal file
49
lib/pods/chat/webrtc_signaling.g.dart
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'webrtc_signaling.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
_SignalingMessage _$SignalingMessageFromJson(Map<String, dynamic> json) =>
|
||||||
|
_SignalingMessage(
|
||||||
|
type: json['type'] as String,
|
||||||
|
to: json['to'] as String?,
|
||||||
|
accountId: json['account_id'] as String,
|
||||||
|
account: SnAccount.fromJson(json['account'] as Map<String, dynamic>),
|
||||||
|
data: json['data'] as Map<String, dynamic>,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$SignalingMessageToJson(_SignalingMessage instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'type': instance.type,
|
||||||
|
'to': instance.to,
|
||||||
|
'account_id': instance.accountId,
|
||||||
|
'account': instance.account.toJson(),
|
||||||
|
'data': instance.data,
|
||||||
|
};
|
||||||
|
|
||||||
|
_WebRTCWelcomeMessage _$WebRTCWelcomeMessageFromJson(
|
||||||
|
Map<String, dynamic> json,
|
||||||
|
) => _WebRTCWelcomeMessage(
|
||||||
|
userId: json['user_id'] as String,
|
||||||
|
roomId: json['room_id'] as String,
|
||||||
|
message: json['message'] as String,
|
||||||
|
timestamp: json['timestamp'] as String,
|
||||||
|
participants:
|
||||||
|
(json['participants'] as List<dynamic>?)
|
||||||
|
?.map((e) => CallParticipant.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList() ??
|
||||||
|
const [],
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$WebRTCWelcomeMessageToJson(
|
||||||
|
_WebRTCWelcomeMessage instance,
|
||||||
|
) => <String, dynamic>{
|
||||||
|
'user_id': instance.userId,
|
||||||
|
'room_id': instance.roomId,
|
||||||
|
'message': instance.message,
|
||||||
|
'timestamp': instance.timestamp,
|
||||||
|
'participants': instance.participants.map((e) => e.toJson()).toList(),
|
||||||
|
};
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:island/pods/theme.dart';
|
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
@@ -18,6 +19,7 @@ const kAppbarTransparentStoreKey = 'app_bar_transparent';
|
|||||||
const kAppBackgroundStoreKey = 'app_has_background';
|
const kAppBackgroundStoreKey = 'app_has_background';
|
||||||
const kAppShowBackgroundImage = 'app_show_background_image';
|
const kAppShowBackgroundImage = 'app_show_background_image';
|
||||||
const kAppColorSchemeStoreKey = 'app_color_scheme';
|
const kAppColorSchemeStoreKey = 'app_color_scheme';
|
||||||
|
const kAppCustomColorsStoreKey = 'app_custom_colors';
|
||||||
const kAppNotifyWithHaptic = 'app_notify_with_haptic';
|
const kAppNotifyWithHaptic = 'app_notify_with_haptic';
|
||||||
const kAppCustomFonts = 'app_custom_fonts';
|
const kAppCustomFonts = 'app_custom_fonts';
|
||||||
const kAppAutoTranslate = 'app_auto_translate';
|
const kAppAutoTranslate = 'app_auto_translate';
|
||||||
@@ -26,10 +28,13 @@ const kAppSoundEffects = 'app_sound_effects';
|
|||||||
const kAppAprilFoolFeatures = 'app_april_fool_features';
|
const kAppAprilFoolFeatures = 'app_april_fool_features';
|
||||||
const kAppWindowSize = 'app_window_size';
|
const kAppWindowSize = 'app_window_size';
|
||||||
const kAppWindowOpacity = 'app_window_opacity';
|
const kAppWindowOpacity = 'app_window_opacity';
|
||||||
|
const kAppCardTransparent = 'app_card_transparent';
|
||||||
const kAppEnterToSend = 'app_enter_to_send';
|
const kAppEnterToSend = 'app_enter_to_send';
|
||||||
const kAppDefaultPoolId = 'app_default_pool_id';
|
const kAppDefaultPoolId = 'app_default_pool_id';
|
||||||
const kAppMessageDisplayStyle = 'app_message_display_style';
|
const kAppMessageDisplayStyle = 'app_message_display_style';
|
||||||
const kAppThemeMode = 'app_theme_mode';
|
const kAppThemeMode = 'app_theme_mode';
|
||||||
|
const kMaterialYouToggleStoreKey = 'app_theme_material_you';
|
||||||
|
const kAppDisableAnimation = 'app_disable_animation';
|
||||||
const kFeaturedPostsCollapsedId =
|
const kFeaturedPostsCollapsedId =
|
||||||
'featured_posts_collapsed_id'; // Key for storing the ID of the collapsed featured post
|
'featured_posts_collapsed_id'; // Key for storing the ID of the collapsed featured post
|
||||||
|
|
||||||
@@ -57,6 +62,21 @@ final serverUrlProvider = Provider<String>((ref) {
|
|||||||
return prefs.getString(kNetworkServerStoreKey) ?? kNetworkServerDefault;
|
return prefs.getString(kNetworkServerStoreKey) ?? kNetworkServerDefault;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
sealed class ThemeColors with _$ThemeColors {
|
||||||
|
factory ThemeColors({
|
||||||
|
int? primary,
|
||||||
|
int? secondary,
|
||||||
|
int? tertiary,
|
||||||
|
int? surface,
|
||||||
|
int? background,
|
||||||
|
int? error,
|
||||||
|
}) = _ThemeColors;
|
||||||
|
|
||||||
|
factory ThemeColors.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$ThemeColorsFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
sealed class AppSettings with _$AppSettings {
|
sealed class AppSettings with _$AppSettings {
|
||||||
const factory AppSettings({
|
const factory AppSettings({
|
||||||
@@ -69,11 +89,15 @@ sealed class AppSettings with _$AppSettings {
|
|||||||
required bool showBackgroundImage,
|
required bool showBackgroundImage,
|
||||||
required String? customFonts,
|
required String? customFonts,
|
||||||
required int? appColorScheme, // The color stored via the int type
|
required int? appColorScheme, // The color stored via the int type
|
||||||
|
required ThemeColors? customColors,
|
||||||
required Size? windowSize, // The window size for desktop platforms
|
required Size? windowSize, // The window size for desktop platforms
|
||||||
required double windowOpacity, // The window opacity for desktop platforms
|
required double windowOpacity, // The window opacity for desktop platforms
|
||||||
|
required double cardTransparency, // The card background opacity
|
||||||
required String? defaultPoolId,
|
required String? defaultPoolId,
|
||||||
required String messageDisplayStyle,
|
required String messageDisplayStyle,
|
||||||
required String? themeMode,
|
required String? themeMode,
|
||||||
|
required bool useMaterial3,
|
||||||
|
required bool disableAnimation,
|
||||||
}) = _AppSettings;
|
}) = _AppSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,11 +116,15 @@ class AppSettingsNotifier extends _$AppSettingsNotifier {
|
|||||||
showBackgroundImage: prefs.getBool(kAppShowBackgroundImage) ?? true,
|
showBackgroundImage: prefs.getBool(kAppShowBackgroundImage) ?? true,
|
||||||
customFonts: prefs.getString(kAppCustomFonts),
|
customFonts: prefs.getString(kAppCustomFonts),
|
||||||
appColorScheme: prefs.getInt(kAppColorSchemeStoreKey),
|
appColorScheme: prefs.getInt(kAppColorSchemeStoreKey),
|
||||||
|
customColors: _getThemeColorsFromPrefs(prefs),
|
||||||
windowSize: _getWindowSizeFromPrefs(prefs),
|
windowSize: _getWindowSizeFromPrefs(prefs),
|
||||||
windowOpacity: prefs.getDouble(kAppWindowOpacity) ?? 1.0,
|
windowOpacity: prefs.getDouble(kAppWindowOpacity) ?? 1.0,
|
||||||
|
cardTransparency: prefs.getDouble(kAppCardTransparent) ?? 1.0,
|
||||||
defaultPoolId: prefs.getString(kAppDefaultPoolId),
|
defaultPoolId: prefs.getString(kAppDefaultPoolId),
|
||||||
messageDisplayStyle: prefs.getString(kAppMessageDisplayStyle) ?? 'bubble',
|
messageDisplayStyle: prefs.getString(kAppMessageDisplayStyle) ?? 'bubble',
|
||||||
themeMode: prefs.getString(kAppThemeMode) ?? 'system',
|
themeMode: prefs.getString(kAppThemeMode) ?? 'system',
|
||||||
|
useMaterial3: prefs.getBool(kMaterialYouToggleStoreKey) ?? true,
|
||||||
|
disableAnimation: prefs.getBool(kAppDisableAnimation) ?? false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,6 +145,18 @@ class AppSettingsNotifier extends _$AppSettingsNotifier {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ThemeColors? _getThemeColorsFromPrefs(SharedPreferences prefs) {
|
||||||
|
final jsonString = prefs.getString(kAppCustomColorsStoreKey);
|
||||||
|
if (jsonString == null) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final json = jsonDecode(jsonString);
|
||||||
|
return ThemeColors.fromJson(json);
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void setDefaultPoolId(String? value) {
|
void setDefaultPoolId(String? value) {
|
||||||
final prefs = ref.read(sharedPreferencesProvider);
|
final prefs = ref.read(sharedPreferencesProvider);
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
@@ -161,7 +201,6 @@ class AppSettingsNotifier extends _$AppSettingsNotifier {
|
|||||||
final prefs = ref.read(sharedPreferencesProvider);
|
final prefs = ref.read(sharedPreferencesProvider);
|
||||||
prefs.setBool(kAppbarTransparentStoreKey, value);
|
prefs.setBool(kAppbarTransparentStoreKey, value);
|
||||||
state = state.copyWith(appBarTransparent: value);
|
state = state.copyWith(appBarTransparent: value);
|
||||||
ref.read(themeProvider.notifier).reloadTheme();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void setShowBackgroundImage(bool value) {
|
void setShowBackgroundImage(bool value) {
|
||||||
@@ -174,14 +213,12 @@ class AppSettingsNotifier extends _$AppSettingsNotifier {
|
|||||||
final prefs = ref.read(sharedPreferencesProvider);
|
final prefs = ref.read(sharedPreferencesProvider);
|
||||||
prefs.setString(kAppCustomFonts, value ?? '');
|
prefs.setString(kAppCustomFonts, value ?? '');
|
||||||
state = state.copyWith(customFonts: value);
|
state = state.copyWith(customFonts: value);
|
||||||
ref.read(themeProvider.notifier).reloadTheme();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void setAppColorScheme(int? value) {
|
void setAppColorScheme(int? value) {
|
||||||
final prefs = ref.read(sharedPreferencesProvider);
|
final prefs = ref.read(sharedPreferencesProvider);
|
||||||
prefs.setInt(kAppColorSchemeStoreKey, value ?? 0);
|
prefs.setInt(kAppColorSchemeStoreKey, value ?? 0);
|
||||||
state = state.copyWith(appColorScheme: value);
|
state = state.copyWith(appColorScheme: value);
|
||||||
ref.read(themeProvider.notifier).reloadTheme();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void setWindowSize(Size? size) {
|
void setWindowSize(Size? size) {
|
||||||
@@ -216,6 +253,35 @@ class AppSettingsNotifier extends _$AppSettingsNotifier {
|
|||||||
prefs.setString(kAppThemeMode, value);
|
prefs.setString(kAppThemeMode, value);
|
||||||
state = state.copyWith(themeMode: value);
|
state = state.copyWith(themeMode: value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setAppTransparentBackground(double value) {
|
||||||
|
final prefs = ref.read(sharedPreferencesProvider);
|
||||||
|
prefs.setDouble(kAppCardTransparent, value);
|
||||||
|
state = state.copyWith(cardTransparency: value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setUseMaterial3(bool value) {
|
||||||
|
final prefs = ref.read(sharedPreferencesProvider);
|
||||||
|
prefs.setBool(kMaterialYouToggleStoreKey, value);
|
||||||
|
state = state.copyWith(useMaterial3: value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setCustomColors(ThemeColors? value) {
|
||||||
|
final prefs = ref.read(sharedPreferencesProvider);
|
||||||
|
if (value != null) {
|
||||||
|
final json = jsonEncode(value.toJson());
|
||||||
|
prefs.setString(kAppCustomColorsStoreKey, json);
|
||||||
|
} else {
|
||||||
|
prefs.remove(kAppCustomColorsStoreKey);
|
||||||
|
}
|
||||||
|
state = state.copyWith(customColors: value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setDisableAnimation(bool value) {
|
||||||
|
final prefs = ref.read(sharedPreferencesProvider);
|
||||||
|
prefs.setBool(kAppDisableAnimation, value);
|
||||||
|
state = state.copyWith(disableAnimation: value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final updateInfoProvider =
|
final updateInfoProvider =
|
||||||
|
|||||||
@@ -11,13 +11,286 @@ part of 'config.dart';
|
|||||||
|
|
||||||
// dart format off
|
// dart format off
|
||||||
T _$identity<T>(T value) => value;
|
T _$identity<T>(T value) => value;
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$ThemeColors {
|
||||||
|
|
||||||
|
int? get primary; int? get secondary; int? get tertiary; int? get surface; int? get background; int? get error;
|
||||||
|
/// Create a copy of ThemeColors
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$ThemeColorsCopyWith<ThemeColors> get copyWith => _$ThemeColorsCopyWithImpl<ThemeColors>(this as ThemeColors, _$identity);
|
||||||
|
|
||||||
|
/// Serializes this ThemeColors to a JSON map.
|
||||||
|
Map<String, dynamic> toJson();
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is ThemeColors&&(identical(other.primary, primary) || other.primary == primary)&&(identical(other.secondary, secondary) || other.secondary == secondary)&&(identical(other.tertiary, tertiary) || other.tertiary == tertiary)&&(identical(other.surface, surface) || other.surface == surface)&&(identical(other.background, background) || other.background == background)&&(identical(other.error, error) || other.error == error));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,primary,secondary,tertiary,surface,background,error);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'ThemeColors(primary: $primary, secondary: $secondary, tertiary: $tertiary, surface: $surface, background: $background, error: $error)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class $ThemeColorsCopyWith<$Res> {
|
||||||
|
factory $ThemeColorsCopyWith(ThemeColors value, $Res Function(ThemeColors) _then) = _$ThemeColorsCopyWithImpl;
|
||||||
|
@useResult
|
||||||
|
$Res call({
|
||||||
|
int? primary, int? secondary, int? tertiary, int? surface, int? background, int? error
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class _$ThemeColorsCopyWithImpl<$Res>
|
||||||
|
implements $ThemeColorsCopyWith<$Res> {
|
||||||
|
_$ThemeColorsCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final ThemeColors _self;
|
||||||
|
final $Res Function(ThemeColors) _then;
|
||||||
|
|
||||||
|
/// Create a copy of ThemeColors
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline') @override $Res call({Object? primary = freezed,Object? secondary = freezed,Object? tertiary = freezed,Object? surface = freezed,Object? background = freezed,Object? error = freezed,}) {
|
||||||
|
return _then(_self.copyWith(
|
||||||
|
primary: freezed == primary ? _self.primary : primary // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int?,secondary: freezed == secondary ? _self.secondary : secondary // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int?,tertiary: freezed == tertiary ? _self.tertiary : tertiary // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int?,surface: freezed == surface ? _self.surface : surface // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int?,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Adds pattern-matching-related methods to [ThemeColors].
|
||||||
|
extension ThemeColorsPatterns on ThemeColors {
|
||||||
|
/// 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( _ThemeColors value)? $default,{required TResult orElse(),}){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _ThemeColors() 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( _ThemeColors value) $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _ThemeColors():
|
||||||
|
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( _ThemeColors value)? $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _ThemeColors() 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( int? primary, int? secondary, int? tertiary, int? surface, int? background, int? error)? $default,{required TResult orElse(),}) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _ThemeColors() when $default != null:
|
||||||
|
return $default(_that.primary,_that.secondary,_that.tertiary,_that.surface,_that.background,_that.error);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( int? primary, int? secondary, int? tertiary, int? surface, int? background, int? error) $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _ThemeColors():
|
||||||
|
return $default(_that.primary,_that.secondary,_that.tertiary,_that.surface,_that.background,_that.error);}
|
||||||
|
}
|
||||||
|
/// 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( int? primary, int? secondary, int? tertiary, int? surface, int? background, int? error)? $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _ThemeColors() when $default != null:
|
||||||
|
return $default(_that.primary,_that.secondary,_that.tertiary,_that.surface,_that.background,_that.error);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
|
||||||
|
class _ThemeColors implements ThemeColors {
|
||||||
|
_ThemeColors({this.primary, this.secondary, this.tertiary, this.surface, this.background, this.error});
|
||||||
|
factory _ThemeColors.fromJson(Map<String, dynamic> json) => _$ThemeColorsFromJson(json);
|
||||||
|
|
||||||
|
@override final int? primary;
|
||||||
|
@override final int? secondary;
|
||||||
|
@override final int? tertiary;
|
||||||
|
@override final int? surface;
|
||||||
|
@override final int? background;
|
||||||
|
@override final int? error;
|
||||||
|
|
||||||
|
/// Create a copy of ThemeColors
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$ThemeColorsCopyWith<_ThemeColors> get copyWith => __$ThemeColorsCopyWithImpl<_ThemeColors>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$ThemeColorsToJson(this, );
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ThemeColors&&(identical(other.primary, primary) || other.primary == primary)&&(identical(other.secondary, secondary) || other.secondary == secondary)&&(identical(other.tertiary, tertiary) || other.tertiary == tertiary)&&(identical(other.surface, surface) || other.surface == surface)&&(identical(other.background, background) || other.background == background)&&(identical(other.error, error) || other.error == error));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,primary,secondary,tertiary,surface,background,error);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'ThemeColors(primary: $primary, secondary: $secondary, tertiary: $tertiary, surface: $surface, background: $background, error: $error)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class _$ThemeColorsCopyWith<$Res> implements $ThemeColorsCopyWith<$Res> {
|
||||||
|
factory _$ThemeColorsCopyWith(_ThemeColors value, $Res Function(_ThemeColors) _then) = __$ThemeColorsCopyWithImpl;
|
||||||
|
@override @useResult
|
||||||
|
$Res call({
|
||||||
|
int? primary, int? secondary, int? tertiary, int? surface, int? background, int? error
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class __$ThemeColorsCopyWithImpl<$Res>
|
||||||
|
implements _$ThemeColorsCopyWith<$Res> {
|
||||||
|
__$ThemeColorsCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final _ThemeColors _self;
|
||||||
|
final $Res Function(_ThemeColors) _then;
|
||||||
|
|
||||||
|
/// Create a copy of ThemeColors
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @pragma('vm:prefer-inline') $Res call({Object? primary = freezed,Object? secondary = freezed,Object? tertiary = freezed,Object? surface = freezed,Object? background = freezed,Object? error = freezed,}) {
|
||||||
|
return _then(_ThemeColors(
|
||||||
|
primary: freezed == primary ? _self.primary : primary // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int?,secondary: freezed == secondary ? _self.secondary : secondary // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int?,tertiary: freezed == tertiary ? _self.tertiary : tertiary // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int?,surface: freezed == surface ? _self.surface : surface // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int?,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
mixin _$AppSettings {
|
mixin _$AppSettings {
|
||||||
|
|
||||||
bool get autoTranslate; bool get dataSavingMode; bool get soundEffects; bool get aprilFoolFeatures; bool get enterToSend; bool get appBarTransparent; bool get showBackgroundImage; String? get customFonts; int? get appColorScheme;// The color stored via the int type
|
bool get autoTranslate; bool get dataSavingMode; bool get soundEffects; bool get aprilFoolFeatures; bool get enterToSend; bool get appBarTransparent; bool get showBackgroundImage; String? get customFonts; int? get appColorScheme;// The color stored via the int type
|
||||||
Size? get windowSize;// The window size for desktop platforms
|
ThemeColors? get customColors; Size? get windowSize;// The window size for desktop platforms
|
||||||
double get windowOpacity;// The window opacity for desktop platforms
|
double get windowOpacity;// The window opacity for desktop platforms
|
||||||
String? get defaultPoolId; String get messageDisplayStyle; String? get themeMode;
|
double get cardTransparency;// The card background opacity
|
||||||
|
String? get defaultPoolId; String get messageDisplayStyle; String? get themeMode; bool get useMaterial3; bool get disableAnimation;
|
||||||
/// Create a copy of AppSettings
|
/// Create a copy of AppSettings
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@@ -28,16 +301,16 @@ $AppSettingsCopyWith<AppSettings> get copyWith => _$AppSettingsCopyWithImpl<AppS
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is AppSettings&&(identical(other.autoTranslate, autoTranslate) || other.autoTranslate == autoTranslate)&&(identical(other.dataSavingMode, dataSavingMode) || other.dataSavingMode == dataSavingMode)&&(identical(other.soundEffects, soundEffects) || other.soundEffects == soundEffects)&&(identical(other.aprilFoolFeatures, aprilFoolFeatures) || other.aprilFoolFeatures == aprilFoolFeatures)&&(identical(other.enterToSend, enterToSend) || other.enterToSend == enterToSend)&&(identical(other.appBarTransparent, appBarTransparent) || other.appBarTransparent == appBarTransparent)&&(identical(other.showBackgroundImage, showBackgroundImage) || other.showBackgroundImage == showBackgroundImage)&&(identical(other.customFonts, customFonts) || other.customFonts == customFonts)&&(identical(other.appColorScheme, appColorScheme) || other.appColorScheme == appColorScheme)&&(identical(other.windowSize, windowSize) || other.windowSize == windowSize)&&(identical(other.windowOpacity, windowOpacity) || other.windowOpacity == windowOpacity)&&(identical(other.defaultPoolId, defaultPoolId) || other.defaultPoolId == defaultPoolId)&&(identical(other.messageDisplayStyle, messageDisplayStyle) || other.messageDisplayStyle == messageDisplayStyle)&&(identical(other.themeMode, themeMode) || other.themeMode == themeMode));
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is AppSettings&&(identical(other.autoTranslate, autoTranslate) || other.autoTranslate == autoTranslate)&&(identical(other.dataSavingMode, dataSavingMode) || other.dataSavingMode == dataSavingMode)&&(identical(other.soundEffects, soundEffects) || other.soundEffects == soundEffects)&&(identical(other.aprilFoolFeatures, aprilFoolFeatures) || other.aprilFoolFeatures == aprilFoolFeatures)&&(identical(other.enterToSend, enterToSend) || other.enterToSend == enterToSend)&&(identical(other.appBarTransparent, appBarTransparent) || other.appBarTransparent == appBarTransparent)&&(identical(other.showBackgroundImage, showBackgroundImage) || other.showBackgroundImage == showBackgroundImage)&&(identical(other.customFonts, customFonts) || other.customFonts == customFonts)&&(identical(other.appColorScheme, appColorScheme) || other.appColorScheme == appColorScheme)&&(identical(other.customColors, customColors) || other.customColors == customColors)&&(identical(other.windowSize, windowSize) || other.windowSize == windowSize)&&(identical(other.windowOpacity, windowOpacity) || other.windowOpacity == windowOpacity)&&(identical(other.cardTransparency, cardTransparency) || other.cardTransparency == cardTransparency)&&(identical(other.defaultPoolId, defaultPoolId) || other.defaultPoolId == defaultPoolId)&&(identical(other.messageDisplayStyle, messageDisplayStyle) || other.messageDisplayStyle == messageDisplayStyle)&&(identical(other.themeMode, themeMode) || other.themeMode == themeMode)&&(identical(other.useMaterial3, useMaterial3) || other.useMaterial3 == useMaterial3)&&(identical(other.disableAnimation, disableAnimation) || other.disableAnimation == disableAnimation));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType,autoTranslate,dataSavingMode,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,showBackgroundImage,customFonts,appColorScheme,windowSize,windowOpacity,defaultPoolId,messageDisplayStyle,themeMode);
|
int get hashCode => Object.hash(runtimeType,autoTranslate,dataSavingMode,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,showBackgroundImage,customFonts,appColorScheme,customColors,windowSize,windowOpacity,cardTransparency,defaultPoolId,messageDisplayStyle,themeMode,useMaterial3,disableAnimation);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'AppSettings(autoTranslate: $autoTranslate, dataSavingMode: $dataSavingMode, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, showBackgroundImage: $showBackgroundImage, customFonts: $customFonts, appColorScheme: $appColorScheme, windowSize: $windowSize, windowOpacity: $windowOpacity, defaultPoolId: $defaultPoolId, messageDisplayStyle: $messageDisplayStyle, themeMode: $themeMode)';
|
return 'AppSettings(autoTranslate: $autoTranslate, dataSavingMode: $dataSavingMode, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, showBackgroundImage: $showBackgroundImage, customFonts: $customFonts, appColorScheme: $appColorScheme, customColors: $customColors, windowSize: $windowSize, windowOpacity: $windowOpacity, cardTransparency: $cardTransparency, defaultPoolId: $defaultPoolId, messageDisplayStyle: $messageDisplayStyle, themeMode: $themeMode, useMaterial3: $useMaterial3, disableAnimation: $disableAnimation)';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -48,11 +321,11 @@ abstract mixin class $AppSettingsCopyWith<$Res> {
|
|||||||
factory $AppSettingsCopyWith(AppSettings value, $Res Function(AppSettings) _then) = _$AppSettingsCopyWithImpl;
|
factory $AppSettingsCopyWith(AppSettings value, $Res Function(AppSettings) _then) = _$AppSettingsCopyWithImpl;
|
||||||
@useResult
|
@useResult
|
||||||
$Res call({
|
$Res call({
|
||||||
bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize, double windowOpacity, String? defaultPoolId, String messageDisplayStyle, String? themeMode
|
bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, ThemeColors? customColors, Size? windowSize, double windowOpacity, double cardTransparency, String? defaultPoolId, String messageDisplayStyle, String? themeMode, bool useMaterial3, bool disableAnimation
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
$ThemeColorsCopyWith<$Res>? get customColors;
|
||||||
|
|
||||||
}
|
}
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@@ -65,7 +338,7 @@ class _$AppSettingsCopyWithImpl<$Res>
|
|||||||
|
|
||||||
/// Create a copy of AppSettings
|
/// Create a copy of AppSettings
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@pragma('vm:prefer-inline') @override $Res call({Object? autoTranslate = null,Object? dataSavingMode = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,Object? appBarTransparent = null,Object? showBackgroundImage = null,Object? customFonts = freezed,Object? appColorScheme = freezed,Object? windowSize = freezed,Object? windowOpacity = null,Object? defaultPoolId = freezed,Object? messageDisplayStyle = null,Object? themeMode = freezed,}) {
|
@pragma('vm:prefer-inline') @override $Res call({Object? autoTranslate = null,Object? dataSavingMode = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,Object? appBarTransparent = null,Object? showBackgroundImage = null,Object? customFonts = freezed,Object? appColorScheme = freezed,Object? customColors = freezed,Object? windowSize = freezed,Object? windowOpacity = null,Object? cardTransparency = null,Object? defaultPoolId = freezed,Object? messageDisplayStyle = null,Object? themeMode = freezed,Object? useMaterial3 = null,Object? disableAnimation = null,}) {
|
||||||
return _then(_self.copyWith(
|
return _then(_self.copyWith(
|
||||||
autoTranslate: null == autoTranslate ? _self.autoTranslate : autoTranslate // ignore: cast_nullable_to_non_nullable
|
autoTranslate: null == autoTranslate ? _self.autoTranslate : autoTranslate // ignore: cast_nullable_to_non_nullable
|
||||||
as bool,dataSavingMode: null == dataSavingMode ? _self.dataSavingMode : dataSavingMode // ignore: cast_nullable_to_non_nullable
|
as bool,dataSavingMode: null == dataSavingMode ? _self.dataSavingMode : dataSavingMode // ignore: cast_nullable_to_non_nullable
|
||||||
@@ -76,15 +349,31 @@ as bool,appBarTransparent: null == appBarTransparent ? _self.appBarTransparent :
|
|||||||
as bool,showBackgroundImage: null == showBackgroundImage ? _self.showBackgroundImage : showBackgroundImage // ignore: cast_nullable_to_non_nullable
|
as bool,showBackgroundImage: null == showBackgroundImage ? _self.showBackgroundImage : showBackgroundImage // ignore: cast_nullable_to_non_nullable
|
||||||
as bool,customFonts: freezed == customFonts ? _self.customFonts : customFonts // ignore: cast_nullable_to_non_nullable
|
as bool,customFonts: freezed == customFonts ? _self.customFonts : customFonts // ignore: cast_nullable_to_non_nullable
|
||||||
as String?,appColorScheme: freezed == appColorScheme ? _self.appColorScheme : appColorScheme // ignore: cast_nullable_to_non_nullable
|
as String?,appColorScheme: freezed == appColorScheme ? _self.appColorScheme : appColorScheme // ignore: cast_nullable_to_non_nullable
|
||||||
as int?,windowSize: freezed == windowSize ? _self.windowSize : windowSize // ignore: cast_nullable_to_non_nullable
|
as int?,customColors: freezed == customColors ? _self.customColors : customColors // ignore: cast_nullable_to_non_nullable
|
||||||
|
as ThemeColors?,windowSize: freezed == windowSize ? _self.windowSize : windowSize // ignore: cast_nullable_to_non_nullable
|
||||||
as Size?,windowOpacity: null == windowOpacity ? _self.windowOpacity : windowOpacity // ignore: cast_nullable_to_non_nullable
|
as Size?,windowOpacity: null == windowOpacity ? _self.windowOpacity : windowOpacity // ignore: cast_nullable_to_non_nullable
|
||||||
|
as double,cardTransparency: null == cardTransparency ? _self.cardTransparency : cardTransparency // ignore: cast_nullable_to_non_nullable
|
||||||
as double,defaultPoolId: freezed == defaultPoolId ? _self.defaultPoolId : defaultPoolId // ignore: cast_nullable_to_non_nullable
|
as double,defaultPoolId: freezed == defaultPoolId ? _self.defaultPoolId : defaultPoolId // ignore: cast_nullable_to_non_nullable
|
||||||
as String?,messageDisplayStyle: null == messageDisplayStyle ? _self.messageDisplayStyle : messageDisplayStyle // ignore: cast_nullable_to_non_nullable
|
as String?,messageDisplayStyle: null == messageDisplayStyle ? _self.messageDisplayStyle : messageDisplayStyle // ignore: cast_nullable_to_non_nullable
|
||||||
as String,themeMode: freezed == themeMode ? _self.themeMode : themeMode // ignore: cast_nullable_to_non_nullable
|
as String,themeMode: freezed == themeMode ? _self.themeMode : themeMode // ignore: cast_nullable_to_non_nullable
|
||||||
as String?,
|
as String?,useMaterial3: null == useMaterial3 ? _self.useMaterial3 : useMaterial3 // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,disableAnimation: null == disableAnimation ? _self.disableAnimation : disableAnimation // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
/// Create a copy of AppSettings
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$ThemeColorsCopyWith<$Res>? get customColors {
|
||||||
|
if (_self.customColors == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $ThemeColorsCopyWith<$Res>(_self.customColors!, (value) {
|
||||||
|
return _then(_self.copyWith(customColors: value));
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -163,10 +452,10 @@ return $default(_that);case _:
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize, double windowOpacity, String? defaultPoolId, String messageDisplayStyle, String? themeMode)? $default,{required TResult orElse(),}) {final _that = this;
|
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, ThemeColors? customColors, Size? windowSize, double windowOpacity, double cardTransparency, String? defaultPoolId, String messageDisplayStyle, String? themeMode, bool useMaterial3, bool disableAnimation)? $default,{required TResult orElse(),}) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _AppSettings() when $default != null:
|
case _AppSettings() when $default != null:
|
||||||
return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.windowSize,_that.windowOpacity,_that.defaultPoolId,_that.messageDisplayStyle,_that.themeMode);case _:
|
return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.customColors,_that.windowSize,_that.windowOpacity,_that.cardTransparency,_that.defaultPoolId,_that.messageDisplayStyle,_that.themeMode,_that.useMaterial3,_that.disableAnimation);case _:
|
||||||
return orElse();
|
return orElse();
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -184,10 +473,10 @@ return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_tha
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize, double windowOpacity, String? defaultPoolId, String messageDisplayStyle, String? themeMode) $default,) {final _that = this;
|
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, ThemeColors? customColors, Size? windowSize, double windowOpacity, double cardTransparency, String? defaultPoolId, String messageDisplayStyle, String? themeMode, bool useMaterial3, bool disableAnimation) $default,) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _AppSettings():
|
case _AppSettings():
|
||||||
return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.windowSize,_that.windowOpacity,_that.defaultPoolId,_that.messageDisplayStyle,_that.themeMode);}
|
return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.customColors,_that.windowSize,_that.windowOpacity,_that.cardTransparency,_that.defaultPoolId,_that.messageDisplayStyle,_that.themeMode,_that.useMaterial3,_that.disableAnimation);}
|
||||||
}
|
}
|
||||||
/// A variant of `when` that fallback to returning `null`
|
/// A variant of `when` that fallback to returning `null`
|
||||||
///
|
///
|
||||||
@@ -201,10 +490,10 @@ return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_tha
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize, double windowOpacity, String? defaultPoolId, String messageDisplayStyle, String? themeMode)? $default,) {final _that = this;
|
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, ThemeColors? customColors, Size? windowSize, double windowOpacity, double cardTransparency, String? defaultPoolId, String messageDisplayStyle, String? themeMode, bool useMaterial3, bool disableAnimation)? $default,) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _AppSettings() when $default != null:
|
case _AppSettings() when $default != null:
|
||||||
return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.windowSize,_that.windowOpacity,_that.defaultPoolId,_that.messageDisplayStyle,_that.themeMode);case _:
|
return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.customColors,_that.windowSize,_that.windowOpacity,_that.cardTransparency,_that.defaultPoolId,_that.messageDisplayStyle,_that.themeMode,_that.useMaterial3,_that.disableAnimation);case _:
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -216,7 +505,7 @@ return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_tha
|
|||||||
|
|
||||||
|
|
||||||
class _AppSettings implements AppSettings {
|
class _AppSettings implements AppSettings {
|
||||||
const _AppSettings({required this.autoTranslate, required this.dataSavingMode, required this.soundEffects, required this.aprilFoolFeatures, required this.enterToSend, required this.appBarTransparent, required this.showBackgroundImage, required this.customFonts, required this.appColorScheme, required this.windowSize, required this.windowOpacity, required this.defaultPoolId, required this.messageDisplayStyle, required this.themeMode});
|
const _AppSettings({required this.autoTranslate, required this.dataSavingMode, required this.soundEffects, required this.aprilFoolFeatures, required this.enterToSend, required this.appBarTransparent, required this.showBackgroundImage, required this.customFonts, required this.appColorScheme, required this.customColors, required this.windowSize, required this.windowOpacity, required this.cardTransparency, required this.defaultPoolId, required this.messageDisplayStyle, required this.themeMode, required this.useMaterial3, required this.disableAnimation});
|
||||||
|
|
||||||
|
|
||||||
@override final bool autoTranslate;
|
@override final bool autoTranslate;
|
||||||
@@ -229,13 +518,18 @@ class _AppSettings implements AppSettings {
|
|||||||
@override final String? customFonts;
|
@override final String? customFonts;
|
||||||
@override final int? appColorScheme;
|
@override final int? appColorScheme;
|
||||||
// The color stored via the int type
|
// The color stored via the int type
|
||||||
|
@override final ThemeColors? customColors;
|
||||||
@override final Size? windowSize;
|
@override final Size? windowSize;
|
||||||
// The window size for desktop platforms
|
// The window size for desktop platforms
|
||||||
@override final double windowOpacity;
|
@override final double windowOpacity;
|
||||||
// The window opacity for desktop platforms
|
// The window opacity for desktop platforms
|
||||||
|
@override final double cardTransparency;
|
||||||
|
// The card background opacity
|
||||||
@override final String? defaultPoolId;
|
@override final String? defaultPoolId;
|
||||||
@override final String messageDisplayStyle;
|
@override final String messageDisplayStyle;
|
||||||
@override final String? themeMode;
|
@override final String? themeMode;
|
||||||
|
@override final bool useMaterial3;
|
||||||
|
@override final bool disableAnimation;
|
||||||
|
|
||||||
/// Create a copy of AppSettings
|
/// Create a copy of AppSettings
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@@ -247,16 +541,16 @@ _$AppSettingsCopyWith<_AppSettings> get copyWith => __$AppSettingsCopyWithImpl<_
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppSettings&&(identical(other.autoTranslate, autoTranslate) || other.autoTranslate == autoTranslate)&&(identical(other.dataSavingMode, dataSavingMode) || other.dataSavingMode == dataSavingMode)&&(identical(other.soundEffects, soundEffects) || other.soundEffects == soundEffects)&&(identical(other.aprilFoolFeatures, aprilFoolFeatures) || other.aprilFoolFeatures == aprilFoolFeatures)&&(identical(other.enterToSend, enterToSend) || other.enterToSend == enterToSend)&&(identical(other.appBarTransparent, appBarTransparent) || other.appBarTransparent == appBarTransparent)&&(identical(other.showBackgroundImage, showBackgroundImage) || other.showBackgroundImage == showBackgroundImage)&&(identical(other.customFonts, customFonts) || other.customFonts == customFonts)&&(identical(other.appColorScheme, appColorScheme) || other.appColorScheme == appColorScheme)&&(identical(other.windowSize, windowSize) || other.windowSize == windowSize)&&(identical(other.windowOpacity, windowOpacity) || other.windowOpacity == windowOpacity)&&(identical(other.defaultPoolId, defaultPoolId) || other.defaultPoolId == defaultPoolId)&&(identical(other.messageDisplayStyle, messageDisplayStyle) || other.messageDisplayStyle == messageDisplayStyle)&&(identical(other.themeMode, themeMode) || other.themeMode == themeMode));
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppSettings&&(identical(other.autoTranslate, autoTranslate) || other.autoTranslate == autoTranslate)&&(identical(other.dataSavingMode, dataSavingMode) || other.dataSavingMode == dataSavingMode)&&(identical(other.soundEffects, soundEffects) || other.soundEffects == soundEffects)&&(identical(other.aprilFoolFeatures, aprilFoolFeatures) || other.aprilFoolFeatures == aprilFoolFeatures)&&(identical(other.enterToSend, enterToSend) || other.enterToSend == enterToSend)&&(identical(other.appBarTransparent, appBarTransparent) || other.appBarTransparent == appBarTransparent)&&(identical(other.showBackgroundImage, showBackgroundImage) || other.showBackgroundImage == showBackgroundImage)&&(identical(other.customFonts, customFonts) || other.customFonts == customFonts)&&(identical(other.appColorScheme, appColorScheme) || other.appColorScheme == appColorScheme)&&(identical(other.customColors, customColors) || other.customColors == customColors)&&(identical(other.windowSize, windowSize) || other.windowSize == windowSize)&&(identical(other.windowOpacity, windowOpacity) || other.windowOpacity == windowOpacity)&&(identical(other.cardTransparency, cardTransparency) || other.cardTransparency == cardTransparency)&&(identical(other.defaultPoolId, defaultPoolId) || other.defaultPoolId == defaultPoolId)&&(identical(other.messageDisplayStyle, messageDisplayStyle) || other.messageDisplayStyle == messageDisplayStyle)&&(identical(other.themeMode, themeMode) || other.themeMode == themeMode)&&(identical(other.useMaterial3, useMaterial3) || other.useMaterial3 == useMaterial3)&&(identical(other.disableAnimation, disableAnimation) || other.disableAnimation == disableAnimation));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType,autoTranslate,dataSavingMode,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,showBackgroundImage,customFonts,appColorScheme,windowSize,windowOpacity,defaultPoolId,messageDisplayStyle,themeMode);
|
int get hashCode => Object.hash(runtimeType,autoTranslate,dataSavingMode,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,showBackgroundImage,customFonts,appColorScheme,customColors,windowSize,windowOpacity,cardTransparency,defaultPoolId,messageDisplayStyle,themeMode,useMaterial3,disableAnimation);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'AppSettings(autoTranslate: $autoTranslate, dataSavingMode: $dataSavingMode, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, showBackgroundImage: $showBackgroundImage, customFonts: $customFonts, appColorScheme: $appColorScheme, windowSize: $windowSize, windowOpacity: $windowOpacity, defaultPoolId: $defaultPoolId, messageDisplayStyle: $messageDisplayStyle, themeMode: $themeMode)';
|
return 'AppSettings(autoTranslate: $autoTranslate, dataSavingMode: $dataSavingMode, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, showBackgroundImage: $showBackgroundImage, customFonts: $customFonts, appColorScheme: $appColorScheme, customColors: $customColors, windowSize: $windowSize, windowOpacity: $windowOpacity, cardTransparency: $cardTransparency, defaultPoolId: $defaultPoolId, messageDisplayStyle: $messageDisplayStyle, themeMode: $themeMode, useMaterial3: $useMaterial3, disableAnimation: $disableAnimation)';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -267,11 +561,11 @@ abstract mixin class _$AppSettingsCopyWith<$Res> implements $AppSettingsCopyWith
|
|||||||
factory _$AppSettingsCopyWith(_AppSettings value, $Res Function(_AppSettings) _then) = __$AppSettingsCopyWithImpl;
|
factory _$AppSettingsCopyWith(_AppSettings value, $Res Function(_AppSettings) _then) = __$AppSettingsCopyWithImpl;
|
||||||
@override @useResult
|
@override @useResult
|
||||||
$Res call({
|
$Res call({
|
||||||
bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize, double windowOpacity, String? defaultPoolId, String messageDisplayStyle, String? themeMode
|
bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, ThemeColors? customColors, Size? windowSize, double windowOpacity, double cardTransparency, String? defaultPoolId, String messageDisplayStyle, String? themeMode, bool useMaterial3, bool disableAnimation
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
@override $ThemeColorsCopyWith<$Res>? get customColors;
|
||||||
|
|
||||||
}
|
}
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@@ -284,7 +578,7 @@ class __$AppSettingsCopyWithImpl<$Res>
|
|||||||
|
|
||||||
/// Create a copy of AppSettings
|
/// Create a copy of AppSettings
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@override @pragma('vm:prefer-inline') $Res call({Object? autoTranslate = null,Object? dataSavingMode = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,Object? appBarTransparent = null,Object? showBackgroundImage = null,Object? customFonts = freezed,Object? appColorScheme = freezed,Object? windowSize = freezed,Object? windowOpacity = null,Object? defaultPoolId = freezed,Object? messageDisplayStyle = null,Object? themeMode = freezed,}) {
|
@override @pragma('vm:prefer-inline') $Res call({Object? autoTranslate = null,Object? dataSavingMode = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,Object? appBarTransparent = null,Object? showBackgroundImage = null,Object? customFonts = freezed,Object? appColorScheme = freezed,Object? customColors = freezed,Object? windowSize = freezed,Object? windowOpacity = null,Object? cardTransparency = null,Object? defaultPoolId = freezed,Object? messageDisplayStyle = null,Object? themeMode = freezed,Object? useMaterial3 = null,Object? disableAnimation = null,}) {
|
||||||
return _then(_AppSettings(
|
return _then(_AppSettings(
|
||||||
autoTranslate: null == autoTranslate ? _self.autoTranslate : autoTranslate // ignore: cast_nullable_to_non_nullable
|
autoTranslate: null == autoTranslate ? _self.autoTranslate : autoTranslate // ignore: cast_nullable_to_non_nullable
|
||||||
as bool,dataSavingMode: null == dataSavingMode ? _self.dataSavingMode : dataSavingMode // ignore: cast_nullable_to_non_nullable
|
as bool,dataSavingMode: null == dataSavingMode ? _self.dataSavingMode : dataSavingMode // ignore: cast_nullable_to_non_nullable
|
||||||
@@ -295,16 +589,32 @@ as bool,appBarTransparent: null == appBarTransparent ? _self.appBarTransparent :
|
|||||||
as bool,showBackgroundImage: null == showBackgroundImage ? _self.showBackgroundImage : showBackgroundImage // ignore: cast_nullable_to_non_nullable
|
as bool,showBackgroundImage: null == showBackgroundImage ? _self.showBackgroundImage : showBackgroundImage // ignore: cast_nullable_to_non_nullable
|
||||||
as bool,customFonts: freezed == customFonts ? _self.customFonts : customFonts // ignore: cast_nullable_to_non_nullable
|
as bool,customFonts: freezed == customFonts ? _self.customFonts : customFonts // ignore: cast_nullable_to_non_nullable
|
||||||
as String?,appColorScheme: freezed == appColorScheme ? _self.appColorScheme : appColorScheme // ignore: cast_nullable_to_non_nullable
|
as String?,appColorScheme: freezed == appColorScheme ? _self.appColorScheme : appColorScheme // ignore: cast_nullable_to_non_nullable
|
||||||
as int?,windowSize: freezed == windowSize ? _self.windowSize : windowSize // ignore: cast_nullable_to_non_nullable
|
as int?,customColors: freezed == customColors ? _self.customColors : customColors // ignore: cast_nullable_to_non_nullable
|
||||||
|
as ThemeColors?,windowSize: freezed == windowSize ? _self.windowSize : windowSize // ignore: cast_nullable_to_non_nullable
|
||||||
as Size?,windowOpacity: null == windowOpacity ? _self.windowOpacity : windowOpacity // ignore: cast_nullable_to_non_nullable
|
as Size?,windowOpacity: null == windowOpacity ? _self.windowOpacity : windowOpacity // ignore: cast_nullable_to_non_nullable
|
||||||
|
as double,cardTransparency: null == cardTransparency ? _self.cardTransparency : cardTransparency // ignore: cast_nullable_to_non_nullable
|
||||||
as double,defaultPoolId: freezed == defaultPoolId ? _self.defaultPoolId : defaultPoolId // ignore: cast_nullable_to_non_nullable
|
as double,defaultPoolId: freezed == defaultPoolId ? _self.defaultPoolId : defaultPoolId // ignore: cast_nullable_to_non_nullable
|
||||||
as String?,messageDisplayStyle: null == messageDisplayStyle ? _self.messageDisplayStyle : messageDisplayStyle // ignore: cast_nullable_to_non_nullable
|
as String?,messageDisplayStyle: null == messageDisplayStyle ? _self.messageDisplayStyle : messageDisplayStyle // ignore: cast_nullable_to_non_nullable
|
||||||
as String,themeMode: freezed == themeMode ? _self.themeMode : themeMode // ignore: cast_nullable_to_non_nullable
|
as String,themeMode: freezed == themeMode ? _self.themeMode : themeMode // ignore: cast_nullable_to_non_nullable
|
||||||
as String?,
|
as String?,useMaterial3: null == useMaterial3 ? _self.useMaterial3 : useMaterial3 // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,disableAnimation: null == disableAnimation ? _self.disableAnimation : disableAnimation // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a copy of AppSettings
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$ThemeColorsCopyWith<$Res>? get customColors {
|
||||||
|
if (_self.customColors == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $ThemeColorsCopyWith<$Res>(_self.customColors!, (value) {
|
||||||
|
return _then(_self.copyWith(customColors: value));
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// dart format on
|
// dart format on
|
||||||
|
|||||||
@@ -2,12 +2,35 @@
|
|||||||
|
|
||||||
part of 'config.dart';
|
part of 'config.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
_ThemeColors _$ThemeColorsFromJson(Map<String, dynamic> json) => _ThemeColors(
|
||||||
|
primary: (json['primary'] as num?)?.toInt(),
|
||||||
|
secondary: (json['secondary'] as num?)?.toInt(),
|
||||||
|
tertiary: (json['tertiary'] as num?)?.toInt(),
|
||||||
|
surface: (json['surface'] as num?)?.toInt(),
|
||||||
|
background: (json['background'] as num?)?.toInt(),
|
||||||
|
error: (json['error'] as num?)?.toInt(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$ThemeColorsToJson(_ThemeColors instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'primary': instance.primary,
|
||||||
|
'secondary': instance.secondary,
|
||||||
|
'tertiary': instance.tertiary,
|
||||||
|
'surface': instance.surface,
|
||||||
|
'background': instance.background,
|
||||||
|
'error': instance.error,
|
||||||
|
};
|
||||||
|
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$appSettingsNotifierHash() =>
|
String _$appSettingsNotifierHash() =>
|
||||||
r'c3042f77067b5f36102f17277e174a756121ac74';
|
r'10ebd893c39d24ae588a4c0bf4144ee4656465a4';
|
||||||
|
|
||||||
/// See also [AppSettingsNotifier].
|
/// See also [AppSettingsNotifier].
|
||||||
@ProviderFor(AppSettingsNotifier)
|
@ProviderFor(AppSettingsNotifier)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:dio_smart_retry/dio_smart_retry.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
@@ -109,6 +110,16 @@ final apiClientProvider = Provider<Dio>((ref) {
|
|||||||
printResponseData: false,
|
printResponseData: false,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
RetryInterceptor(
|
||||||
|
dio: dio,
|
||||||
|
retries: 3,
|
||||||
|
retryDelays: const [
|
||||||
|
Duration(milliseconds: 300),
|
||||||
|
Duration(milliseconds: 500),
|
||||||
|
Duration(milliseconds: 1000),
|
||||||
|
],
|
||||||
|
retryEvaluator: (err, _) => err.requestOptions.method == 'GET',
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return dio;
|
return dio;
|
||||||
|
|||||||
@@ -1,38 +1,16 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:island/pods/config.dart';
|
import 'package:island/pods/config.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
final themeProvider = StateNotifierProvider<ThemeNotifier, ThemeSet?>((ref) {
|
part 'theme.g.dart';
|
||||||
return ThemeNotifier();
|
|
||||||
});
|
|
||||||
|
|
||||||
class ThemeNotifier extends StateNotifier<ThemeSet?> {
|
@riverpod
|
||||||
ThemeNotifier() : super(null) {
|
ThemeSet theme(Ref ref) {
|
||||||
_loadTheme();
|
final settings = ref.watch(appSettingsNotifierProvider);
|
||||||
}
|
return createAppThemeSet(settings);
|
||||||
|
|
||||||
Future<void> _loadTheme() async {
|
|
||||||
final theme = await createAppThemeSet();
|
|
||||||
state = theme;
|
|
||||||
}
|
|
||||||
|
|
||||||
void reloadTheme({
|
|
||||||
Color? seedColorOverride,
|
|
||||||
bool? useMaterial3,
|
|
||||||
String? customFonts,
|
|
||||||
}) async {
|
|
||||||
final theme = await createAppThemeSet(
|
|
||||||
seedColorOverride: seedColorOverride,
|
|
||||||
useMaterial3: useMaterial3,
|
|
||||||
customFonts: customFonts,
|
|
||||||
);
|
|
||||||
state = theme;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const kMaterialYouToggleStoreKey = 'app_theme_material_you';
|
|
||||||
|
|
||||||
class ThemeSet {
|
class ThemeSet {
|
||||||
ThemeData light;
|
ThemeData light;
|
||||||
ThemeData dark;
|
ThemeData dark;
|
||||||
@@ -40,52 +18,50 @@ class ThemeSet {
|
|||||||
ThemeSet({required this.light, required this.dark});
|
ThemeSet({required this.light, required this.dark});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<ThemeSet> createAppThemeSet({
|
ThemeSet createAppThemeSet(AppSettings settings) {
|
||||||
Color? seedColorOverride,
|
|
||||||
bool? useMaterial3,
|
|
||||||
String? customFonts,
|
|
||||||
}) async {
|
|
||||||
return ThemeSet(
|
return ThemeSet(
|
||||||
light: await createAppTheme(
|
light: createAppTheme(Brightness.light, settings),
|
||||||
Brightness.light,
|
dark: createAppTheme(Brightness.dark, settings),
|
||||||
useMaterial3: useMaterial3,
|
|
||||||
customFonts: customFonts,
|
|
||||||
),
|
|
||||||
dark: await createAppTheme(
|
|
||||||
Brightness.dark,
|
|
||||||
useMaterial3: useMaterial3,
|
|
||||||
customFonts: customFonts,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<ThemeData> createAppTheme(
|
ThemeData createAppTheme(Brightness brightness, AppSettings settings) {
|
||||||
Brightness brightness, {
|
|
||||||
Color? seedColorOverride,
|
|
||||||
bool? useMaterial3,
|
|
||||||
String? customFonts,
|
|
||||||
}) async {
|
|
||||||
final prefs = await SharedPreferences.getInstance();
|
|
||||||
|
|
||||||
final seedColorString = prefs.getInt(kAppColorSchemeStoreKey);
|
|
||||||
final seedColor =
|
final seedColor =
|
||||||
seedColorString != null ? Color(seedColorString) : Colors.indigo;
|
settings.appColorScheme != null
|
||||||
|
? Color(settings.appColorScheme!)
|
||||||
|
: Colors.indigo;
|
||||||
|
|
||||||
final colorScheme = ColorScheme.fromSeed(
|
var colorScheme = ColorScheme.fromSeed(
|
||||||
seedColor: seedColorOverride ?? seedColor,
|
seedColor: seedColor,
|
||||||
brightness: brightness,
|
brightness: brightness,
|
||||||
);
|
);
|
||||||
|
|
||||||
final hasAppBarTransparent =
|
final customColors = settings.customColors;
|
||||||
prefs.getBool(kAppbarTransparentStoreKey) ?? false;
|
if (customColors != null) {
|
||||||
final useM3 =
|
colorScheme = colorScheme.copyWith(
|
||||||
useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? true);
|
primary:
|
||||||
|
customColors.primary != null ? Color(customColors.primary!) : null,
|
||||||
|
secondary:
|
||||||
|
customColors.secondary != null
|
||||||
|
? Color(customColors.secondary!)
|
||||||
|
: null,
|
||||||
|
tertiary:
|
||||||
|
customColors.tertiary != null ? Color(customColors.tertiary!) : null,
|
||||||
|
surface:
|
||||||
|
customColors.surface != null ? Color(customColors.surface!) : null,
|
||||||
|
background:
|
||||||
|
customColors.background != null
|
||||||
|
? Color(customColors.background!)
|
||||||
|
: null,
|
||||||
|
error: customColors.error != null ? Color(customColors.error!) : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final hasAppBarTransparent = settings.appBarTransparent;
|
||||||
|
final useM3 = settings.useMaterial3;
|
||||||
|
|
||||||
final inUseFonts =
|
final inUseFonts =
|
||||||
(customFonts ?? prefs.getString(kAppCustomFonts))
|
settings.customFonts?.split(',').map((ele) => ele.trim()).toList() ??
|
||||||
?.split(',')
|
|
||||||
.map((ele) => ele.trim())
|
|
||||||
.toList() ??
|
|
||||||
['Nunito'];
|
['Nunito'];
|
||||||
|
|
||||||
return ThemeData(
|
return ThemeData(
|
||||||
@@ -100,10 +76,6 @@ Future<ThemeData> createAppTheme(
|
|||||||
opticalSize: 20,
|
opticalSize: 20,
|
||||||
color: colorScheme.onSurface,
|
color: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
snackBarTheme: SnackBarThemeData(
|
|
||||||
behavior: useM3 ? SnackBarBehavior.floating : SnackBarBehavior.fixed,
|
|
||||||
width: 480,
|
|
||||||
),
|
|
||||||
appBarTheme: AppBarTheme(
|
appBarTheme: AppBarTheme(
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
elevation: hasAppBarTransparent ? 0 : null,
|
elevation: hasAppBarTransparent ? 0 : null,
|
||||||
@@ -112,6 +84,12 @@ Future<ThemeData> createAppTheme(
|
|||||||
foregroundColor:
|
foregroundColor:
|
||||||
hasAppBarTransparent ? colorScheme.onSurface : colorScheme.onPrimary,
|
hasAppBarTransparent ? colorScheme.onSurface : colorScheme.onPrimary,
|
||||||
),
|
),
|
||||||
|
cardTheme: CardThemeData(
|
||||||
|
color: colorScheme.surfaceContainer.withOpacity(
|
||||||
|
settings.cardTransparency,
|
||||||
|
),
|
||||||
|
elevation: settings.cardTransparency <= 1 ? 0 : null,
|
||||||
|
),
|
||||||
pageTransitionsTheme: PageTransitionsTheme(
|
pageTransitionsTheme: PageTransitionsTheme(
|
||||||
builders: {
|
builders: {
|
||||||
TargetPlatform.android: ZoomPageTransitionsBuilder(),
|
TargetPlatform.android: ZoomPageTransitionsBuilder(),
|
||||||
|
|||||||
26
lib/pods/theme.g.dart
Normal file
26
lib/pods/theme.g.dart
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'theme.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
String _$themeHash() => r'a12dbf8b83d75713b7ae4c68e9cdd1a1cc3a35f0';
|
||||||
|
|
||||||
|
/// See also [theme].
|
||||||
|
@ProviderFor(theme)
|
||||||
|
final themeProvider = AutoDisposeProvider<ThemeSet>.internal(
|
||||||
|
theme,
|
||||||
|
name: r'themeProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product') ? null : _$themeHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
typedef ThemeRef = AutoDisposeProviderRef<ThemeSet>;
|
||||||
|
// 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
|
||||||
@@ -100,12 +100,16 @@ class WebSocketService {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDone: () {
|
onDone: () {
|
||||||
talker.info('[WebSocket] Connection closed, attempting to reconnect...');
|
talker.info(
|
||||||
|
'[WebSocket] Connection closed, attempting to reconnect...',
|
||||||
|
);
|
||||||
_scheduleReconnect();
|
_scheduleReconnect();
|
||||||
_statusStreamController.sink.add(WebSocketState.disconnected());
|
_statusStreamController.sink.add(WebSocketState.disconnected());
|
||||||
},
|
},
|
||||||
onError: (error) {
|
onError: (error) {
|
||||||
talker.error('[WebSocket] Error occurred: $error, attempting to reconnect...');
|
talker.error(
|
||||||
|
'[WebSocket] Error occurred: $error, attempting to reconnect...',
|
||||||
|
);
|
||||||
_scheduleReconnect();
|
_scheduleReconnect();
|
||||||
_statusStreamController.sink.add(
|
_statusStreamController.sink.add(
|
||||||
WebSocketState.error(error.toString()),
|
WebSocketState.error(error.toString()),
|
||||||
|
|||||||
460
lib/route.dart
460
lib/route.dart
@@ -10,13 +10,11 @@ import 'package:island/screens/developers/app_detail.dart';
|
|||||||
import 'package:island/screens/developers/bot_detail.dart';
|
import 'package:island/screens/developers/bot_detail.dart';
|
||||||
import 'package:island/screens/developers/edit_app.dart';
|
import 'package:island/screens/developers/edit_app.dart';
|
||||||
import 'package:island/screens/developers/edit_bot.dart';
|
import 'package:island/screens/developers/edit_bot.dart';
|
||||||
import 'package:island/screens/developers/new_app.dart';
|
|
||||||
import 'package:island/screens/developers/hub.dart';
|
import 'package:island/screens/developers/hub.dart';
|
||||||
|
import 'package:island/screens/developers/new_app.dart';
|
||||||
import 'package:island/screens/developers/new_bot.dart';
|
import 'package:island/screens/developers/new_bot.dart';
|
||||||
import 'package:island/screens/developers/projects.dart';
|
|
||||||
import 'package:island/screens/developers/edit_project.dart';
|
import 'package:island/screens/developers/edit_project.dart';
|
||||||
import 'package:island/screens/developers/new_project.dart';
|
import 'package:island/screens/developers/new_project.dart';
|
||||||
import 'package:island/screens/developers/project_detail.dart';
|
|
||||||
import 'package:island/screens/discovery/articles.dart';
|
import 'package:island/screens/discovery/articles.dart';
|
||||||
import 'package:island/screens/files/file_list.dart';
|
import 'package:island/screens/files/file_list.dart';
|
||||||
import 'package:island/screens/posts/post_categories_list.dart';
|
import 'package:island/screens/posts/post_categories_list.dart';
|
||||||
@@ -27,7 +25,6 @@ import 'package:island/screens/tabs.dart';
|
|||||||
import 'package:island/screens/explore.dart';
|
import 'package:island/screens/explore.dart';
|
||||||
import 'package:island/screens/discovery/article_detail.dart';
|
import 'package:island/screens/discovery/article_detail.dart';
|
||||||
import 'package:island/screens/account.dart';
|
import 'package:island/screens/account.dart';
|
||||||
import 'package:island/screens/notification.dart';
|
|
||||||
import 'package:island/screens/wallet.dart';
|
import 'package:island/screens/wallet.dart';
|
||||||
import 'package:island/screens/account/relationship.dart';
|
import 'package:island/screens/account/relationship.dart';
|
||||||
import 'package:island/screens/account/profile.dart';
|
import 'package:island/screens/account/profile.dart';
|
||||||
@@ -35,6 +32,7 @@ import 'package:island/screens/account/me/profile_update.dart';
|
|||||||
import 'package:island/screens/account/leveling.dart';
|
import 'package:island/screens/account/leveling.dart';
|
||||||
import 'package:island/screens/account/me/account_settings.dart';
|
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/chat_form.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/call.dart';
|
||||||
@@ -42,15 +40,13 @@ import 'package:island/screens/chat/search_messages.dart';
|
|||||||
import 'package:island/screens/creators/hub.dart';
|
import 'package:island/screens/creators/hub.dart';
|
||||||
import 'package:island/screens/creators/posts/post_manage_list.dart';
|
import 'package:island/screens/creators/posts/post_manage_list.dart';
|
||||||
import 'package:island/screens/creators/stickers/stickers.dart';
|
import 'package:island/screens/creators/stickers/stickers.dart';
|
||||||
import 'package:island/screens/creators/stickers/pack_detail.dart';
|
|
||||||
import 'package:island/screens/stickers/sticker_marketplace.dart';
|
import 'package:island/screens/stickers/sticker_marketplace.dart';
|
||||||
import 'package:island/screens/stickers/pack_detail.dart';
|
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/publishers.dart';
|
import 'package:island/screens/creators/publishers_form.dart';
|
||||||
import 'package:island/screens/creators/webfeed/webfeed_list.dart';
|
import 'package:island/screens/creators/webfeed/webfeed_list.dart';
|
||||||
import 'package:island/screens/creators/webfeed/webfeed_edit.dart';
|
|
||||||
import 'package:island/screens/poll/poll_editor.dart';
|
import 'package:island/screens/poll/poll_editor.dart';
|
||||||
import 'package:island/screens/posts/compose.dart';
|
import 'package:island/screens/posts/compose.dart';
|
||||||
import 'package:island/screens/posts/post_detail.dart';
|
import 'package:island/screens/posts/post_detail.dart';
|
||||||
@@ -59,8 +55,8 @@ import 'package:island/screens/auth/login.dart';
|
|||||||
import 'package:island/screens/auth/create_account.dart';
|
import 'package:island/screens/auth/create_account.dart';
|
||||||
import 'package:island/screens/settings.dart';
|
import 'package:island/screens/settings.dart';
|
||||||
import 'package:island/screens/realm/realms.dart';
|
import 'package:island/screens/realm/realms.dart';
|
||||||
|
import 'package:island/screens/realm/realm_form.dart';
|
||||||
import 'package:island/screens/realm/realm_detail.dart';
|
import 'package:island/screens/realm/realm_detail.dart';
|
||||||
import 'package:island/screens/account/event_calendar.dart';
|
|
||||||
import 'package:island/screens/discovery/realms.dart';
|
import 'package:island/screens/discovery/realms.dart';
|
||||||
import 'package:island/screens/reports/report_detail.dart';
|
import 'package:island/screens/reports/report_detail.dart';
|
||||||
import 'package:island/screens/reports/report_list.dart';
|
import 'package:island/screens/reports/report_list.dart';
|
||||||
@@ -140,264 +136,6 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
path: '/logs',
|
path: '/logs',
|
||||||
builder: (context, state) => TalkerScreen(talker: talker),
|
builder: (context, state) => TalkerScreen(talker: talker),
|
||||||
),
|
),
|
||||||
GoRoute(
|
|
||||||
name: 'accountCalendar',
|
|
||||||
path: '/account/:name/calendar',
|
|
||||||
builder: (context, state) {
|
|
||||||
final name = state.pathParameters['name']!;
|
|
||||||
return EventCalanderScreen(name: name);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ShellRoute(
|
|
||||||
builder:
|
|
||||||
(context, state, child) => CreatorHubShellScreen(child: child),
|
|
||||||
routes: [
|
|
||||||
GoRoute(
|
|
||||||
name: 'creatorHub',
|
|
||||||
path: '/creators',
|
|
||||||
builder: (context, state) => const CreatorHubScreen(),
|
|
||||||
),
|
|
||||||
// Web Feed Routes
|
|
||||||
GoRoute(
|
|
||||||
name: 'creatorFeeds',
|
|
||||||
path: '/creators/:name/feeds',
|
|
||||||
builder: (context, state) {
|
|
||||||
final name = state.pathParameters['name']!;
|
|
||||||
return WebFeedListScreen(pubName: name);
|
|
||||||
},
|
|
||||||
routes: [
|
|
||||||
GoRoute(
|
|
||||||
name: 'creatorFeedNew',
|
|
||||||
path: 'new',
|
|
||||||
builder: (context, state) {
|
|
||||||
return WebFeedNewScreen(
|
|
||||||
pubName: state.pathParameters['name']!,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
name: 'creatorFeedEdit',
|
|
||||||
path: ':feedId',
|
|
||||||
builder: (context, state) {
|
|
||||||
return WebFeedEditScreen(
|
|
||||||
pubName: state.pathParameters['name']!,
|
|
||||||
feedId: state.pathParameters['feedId'],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
name: 'creatorPosts',
|
|
||||||
path: '/creators/:name/posts',
|
|
||||||
builder: (context, state) {
|
|
||||||
final name = state.pathParameters['name']!;
|
|
||||||
return CreatorPostListScreen(pubName: name);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
// Poll list route
|
|
||||||
GoRoute(
|
|
||||||
name: 'creatorPolls',
|
|
||||||
path: '/creators/:name/polls',
|
|
||||||
builder: (context, state) {
|
|
||||||
final name = state.pathParameters['name']!;
|
|
||||||
return CreatorPollListScreen(pubName: name);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
// Poll routes
|
|
||||||
GoRoute(
|
|
||||||
name: 'creatorPollNew',
|
|
||||||
path: '/creators/:name/polls/new',
|
|
||||||
builder: (context, state) {
|
|
||||||
final name = state.pathParameters['name']!;
|
|
||||||
// initialPollId left null for create; initialPublisher prefilled
|
|
||||||
return PollEditorScreen(initialPublisher: name);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
name: 'creatorPollEdit',
|
|
||||||
path: '/creators/:name/polls/:id/edit',
|
|
||||||
builder: (context, state) {
|
|
||||||
final name = state.pathParameters['name']!;
|
|
||||||
final id = state.pathParameters['id']!;
|
|
||||||
return PollEditorScreen(
|
|
||||||
initialPollId: id,
|
|
||||||
initialPublisher: name,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
name: 'creatorStickers',
|
|
||||||
path: '/creators/:name/stickers',
|
|
||||||
builder: (context, state) {
|
|
||||||
final name = state.pathParameters['name']!;
|
|
||||||
return StickersScreen(pubName: name);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
name: 'creatorStickerPackNew',
|
|
||||||
path: '/creators/:name/stickers/new',
|
|
||||||
builder: (context, state) {
|
|
||||||
final name = state.pathParameters['name']!;
|
|
||||||
return NewStickerPacksScreen(pubName: name);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
name: 'creatorStickerPackEdit',
|
|
||||||
path: '/creators/:name/stickers/:packId/edit',
|
|
||||||
builder: (context, state) {
|
|
||||||
final name = state.pathParameters['name']!;
|
|
||||||
final packId = state.pathParameters['packId']!;
|
|
||||||
return EditStickerPacksScreen(pubName: name, packId: packId);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
name: 'creatorStickerPackDetail',
|
|
||||||
path: '/creators/:name/stickers/:packId',
|
|
||||||
builder: (context, state) {
|
|
||||||
final name = state.pathParameters['name']!;
|
|
||||||
final packId = state.pathParameters['packId']!;
|
|
||||||
return StickerPackDetailScreen(pubName: name, id: packId);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
name: 'creatorStickerNew',
|
|
||||||
path: '/creators/:name/stickers/:packId/new',
|
|
||||||
builder: (context, state) {
|
|
||||||
final packId = state.pathParameters['packId']!;
|
|
||||||
return NewStickersScreen(packId: packId);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
name: 'creatorStickerEdit',
|
|
||||||
path: '/creators/:name/stickers/:packId/:id/edit',
|
|
||||||
builder: (context, state) {
|
|
||||||
final packId = state.pathParameters['packId']!;
|
|
||||||
final id = state.pathParameters['id']!;
|
|
||||||
return EditStickersScreen(id: id, packId: packId);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
name: 'creatorNew',
|
|
||||||
path: '/creators/new',
|
|
||||||
builder: (context, state) => const NewPublisherScreen(),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
name: 'creatorEdit',
|
|
||||||
path: '/creators/:name/edit',
|
|
||||||
builder: (context, state) {
|
|
||||||
final name = state.pathParameters['name']!;
|
|
||||||
return EditPublisherScreen(name: name);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
ShellRoute(
|
|
||||||
builder:
|
|
||||||
(context, state, child) =>
|
|
||||||
DeveloperHubShellScreen(child: child),
|
|
||||||
routes: [
|
|
||||||
GoRoute(
|
|
||||||
name: 'developerHub',
|
|
||||||
path: '/developers',
|
|
||||||
builder: (context, state) => const DeveloperHubScreen(),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
name: 'developerProjects',
|
|
||||||
path: '/developers/:name/projects',
|
|
||||||
builder:
|
|
||||||
(context, state) => DevProjectsScreen(
|
|
||||||
publisherName: state.pathParameters['name']!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
name: 'developerProjectNew',
|
|
||||||
path: '/developers/:name/projects/new',
|
|
||||||
builder:
|
|
||||||
(context, state) => NewProjectScreen(
|
|
||||||
publisherName: state.pathParameters['name']!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
name: 'developerProjectEdit',
|
|
||||||
path: '/developers/:name/projects/:id/edit',
|
|
||||||
builder:
|
|
||||||
(context, state) => EditProjectScreen(
|
|
||||||
publisherName: state.pathParameters['name']!,
|
|
||||||
id: state.pathParameters['id']!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
name: 'developerProjectDetail',
|
|
||||||
path: '/developers/:name/projects/:projectId',
|
|
||||||
builder:
|
|
||||||
(context, state) => ProjectDetailScreen(
|
|
||||||
publisherName: state.pathParameters['name']!,
|
|
||||||
projectId: state.pathParameters['projectId']!,
|
|
||||||
),
|
|
||||||
routes: [
|
|
||||||
GoRoute(
|
|
||||||
name: 'developerAppNew',
|
|
||||||
path: 'apps/new',
|
|
||||||
builder:
|
|
||||||
(context, state) => NewCustomAppScreen(
|
|
||||||
publisherName: state.pathParameters['name']!,
|
|
||||||
projectId: state.pathParameters['projectId']!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
name: 'developerAppEdit',
|
|
||||||
path: 'apps/:id/edit',
|
|
||||||
builder:
|
|
||||||
(context, state) => EditAppScreen(
|
|
||||||
publisherName: state.pathParameters['name']!,
|
|
||||||
projectId: state.pathParameters['projectId']!,
|
|
||||||
id: state.pathParameters['id']!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
name: 'developerAppDetail',
|
|
||||||
path: 'apps/:appId',
|
|
||||||
builder:
|
|
||||||
(context, state) => AppDetailScreen(
|
|
||||||
publisherName: state.pathParameters['name']!,
|
|
||||||
projectId: state.pathParameters['projectId']!,
|
|
||||||
appId: state.pathParameters['appId']!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
name: 'developerBotDetail',
|
|
||||||
path: 'bots/:botId',
|
|
||||||
builder:
|
|
||||||
(context, state) => BotDetailScreen(
|
|
||||||
publisherName: state.pathParameters['name']!,
|
|
||||||
projectId: state.pathParameters['projectId']!,
|
|
||||||
botId: state.pathParameters['botId']!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
name: 'developerBotNew',
|
|
||||||
path: 'bots/new',
|
|
||||||
builder:
|
|
||||||
(context, state) => NewBotScreen(
|
|
||||||
publisherName: state.pathParameters['name']!,
|
|
||||||
projectId: state.pathParameters['projectId']!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
name: 'developerBotEdit',
|
|
||||||
path: 'bots/:id/edit',
|
|
||||||
builder:
|
|
||||||
(context, state) => EditBotScreen(
|
|
||||||
publisherName: state.pathParameters['name']!,
|
|
||||||
projectId: state.pathParameters['projectId']!,
|
|
||||||
id: state.pathParameters['id']!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
|
|
||||||
// Web articles
|
// Web articles
|
||||||
GoRoute(
|
GoRoute(
|
||||||
@@ -653,11 +391,6 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
GoRoute(
|
|
||||||
name: 'notifications',
|
|
||||||
path: '/account/notifications',
|
|
||||||
builder: (context, state) => const NotificationScreen(),
|
|
||||||
),
|
|
||||||
GoRoute(
|
GoRoute(
|
||||||
name: 'wallet',
|
name: 'wallet',
|
||||||
path: '/account/wallet',
|
path: '/account/wallet',
|
||||||
@@ -706,12 +439,195 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
|
|
||||||
GoRoute(
|
GoRoute(
|
||||||
name: 'accountProfile',
|
name: 'accountProfile',
|
||||||
path: '/account/:name',
|
path: '/accounts/:name',
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final name = state.pathParameters['name']!;
|
final name = state.pathParameters['name']!;
|
||||||
return AccountProfileScreen(name: name);
|
return AccountProfileScreen(name: name);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
// Creator hub tab
|
||||||
|
GoRoute(
|
||||||
|
name: 'creatorHub',
|
||||||
|
path: '/creators',
|
||||||
|
builder: (context, state) => const CreatorHubScreen(),
|
||||||
|
routes: [
|
||||||
|
// Web Feed Routes
|
||||||
|
GoRoute(
|
||||||
|
name: 'creatorFeeds',
|
||||||
|
path: ':name/feeds',
|
||||||
|
builder: (context, state) {
|
||||||
|
final name = state.pathParameters['name']!;
|
||||||
|
return WebFeedListScreen(pubName: name);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
name: 'creatorPosts',
|
||||||
|
path: ':name/posts',
|
||||||
|
builder: (context, state) {
|
||||||
|
final name = state.pathParameters['name']!;
|
||||||
|
return CreatorPostListScreen(pubName: name);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
// Poll list route
|
||||||
|
GoRoute(
|
||||||
|
name: 'creatorPolls',
|
||||||
|
path: ':name/polls',
|
||||||
|
builder: (context, state) {
|
||||||
|
final name = state.pathParameters['name']!;
|
||||||
|
return CreatorPollListScreen(pubName: name);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
// Poll routes
|
||||||
|
GoRoute(
|
||||||
|
name: 'creatorPollNew',
|
||||||
|
path: ':name/polls/new',
|
||||||
|
builder: (context, state) {
|
||||||
|
final name = state.pathParameters['name']!;
|
||||||
|
// initialPollId left null for create; initialPublisher prefilled
|
||||||
|
return PollEditorScreen(initialPublisher: name);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
name: 'creatorPollEdit',
|
||||||
|
path: ':name/polls/:id/edit',
|
||||||
|
builder: (context, state) {
|
||||||
|
final name = state.pathParameters['name']!;
|
||||||
|
final id = state.pathParameters['id']!;
|
||||||
|
return PollEditorScreen(
|
||||||
|
initialPollId: id,
|
||||||
|
initialPublisher: name,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
name: 'creatorStickers',
|
||||||
|
path: ':name/stickers',
|
||||||
|
builder: (context, state) {
|
||||||
|
final name = state.pathParameters['name']!;
|
||||||
|
return StickersScreen(pubName: name);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
name: 'creatorNew',
|
||||||
|
path: 'new',
|
||||||
|
builder: (context, state) => const NewPublisherScreen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
name: 'creatorEdit',
|
||||||
|
path: ':name/edit',
|
||||||
|
builder: (context, state) {
|
||||||
|
final name = state.pathParameters['name']!;
|
||||||
|
return EditPublisherScreen(name: name);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Developer hub tab
|
||||||
|
GoRoute(
|
||||||
|
name: 'developerHub',
|
||||||
|
path: '/developers',
|
||||||
|
builder:
|
||||||
|
(context, state) => DeveloperHubScreen(
|
||||||
|
initialPublisherName:
|
||||||
|
state.uri.queryParameters['publisher'],
|
||||||
|
initialProjectId: state.uri.queryParameters['project'],
|
||||||
|
),
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
name: 'developerProjectNew',
|
||||||
|
path: ':name/projects/new',
|
||||||
|
builder:
|
||||||
|
(context, state) => NewProjectScreen(
|
||||||
|
publisherName: state.pathParameters['name']!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
name: 'developerProjectEdit',
|
||||||
|
path: ':name/projects/:id/edit',
|
||||||
|
builder:
|
||||||
|
(context, state) => EditProjectScreen(
|
||||||
|
publisherName: state.pathParameters['name']!,
|
||||||
|
id: state.pathParameters['id']!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
name: 'developerProjectDetail',
|
||||||
|
path: ':name/projects/:projectId',
|
||||||
|
builder: (context, state) {
|
||||||
|
final name = state.pathParameters['name']!;
|
||||||
|
final projectId = state.pathParameters['projectId']!;
|
||||||
|
// Redirect to hub with project selected
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
context.go(
|
||||||
|
'/developers?publisher=$name&project=$projectId',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return const SizedBox.shrink(); // Temporary placeholder
|
||||||
|
},
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
name: 'developerAppNew',
|
||||||
|
path: 'apps/new',
|
||||||
|
builder:
|
||||||
|
(context, state) => NewCustomAppScreen(
|
||||||
|
publisherName: state.pathParameters['name']!,
|
||||||
|
projectId: state.pathParameters['projectId']!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
name: 'developerAppEdit',
|
||||||
|
path: 'apps/:id/edit',
|
||||||
|
builder:
|
||||||
|
(context, state) => EditAppScreen(
|
||||||
|
publisherName: state.pathParameters['name']!,
|
||||||
|
projectId: state.pathParameters['projectId']!,
|
||||||
|
id: state.pathParameters['id']!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
name: 'developerAppDetail',
|
||||||
|
path: 'apps/:appId',
|
||||||
|
builder:
|
||||||
|
(context, state) => AppDetailScreen(
|
||||||
|
publisherName: state.pathParameters['name']!,
|
||||||
|
projectId: state.pathParameters['projectId']!,
|
||||||
|
appId: state.pathParameters['appId']!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
name: 'developerBotNew',
|
||||||
|
path: 'bots/new',
|
||||||
|
builder:
|
||||||
|
(context, state) => NewBotScreen(
|
||||||
|
publisherName: state.pathParameters['name']!,
|
||||||
|
projectId: state.pathParameters['projectId']!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
name: 'developerBotDetail',
|
||||||
|
path: 'bots/:botId',
|
||||||
|
builder:
|
||||||
|
(context, state) => BotDetailScreen(
|
||||||
|
publisherName: state.pathParameters['name']!,
|
||||||
|
projectId: state.pathParameters['projectId']!,
|
||||||
|
botId: state.pathParameters['botId']!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
name: 'developerBotEdit',
|
||||||
|
path: 'bots/:id/edit',
|
||||||
|
builder:
|
||||||
|
(context, state) => EditBotScreen(
|
||||||
|
publisherName: state.pathParameters['name']!,
|
||||||
|
projectId: state.pathParameters['projectId']!,
|
||||||
|
id: state.pathParameters['id']!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -66,7 +66,6 @@ class AccountScreen extends HookConsumerWidget {
|
|||||||
isNoBackground: isWide,
|
isNoBackground: isWide,
|
||||||
appBar: AppBar(backgroundColor: Colors.transparent, toolbarHeight: 0),
|
appBar: AppBar(backgroundColor: Colors.transparent, toolbarHeight: 0),
|
||||||
body: SingleChildScrollView(
|
body: SingleChildScrollView(
|
||||||
padding: getTabbedPadding(context),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
spacing: 4,
|
spacing: 4,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
@@ -150,7 +149,8 @@ class AccountScreen extends HookConsumerWidget {
|
|||||||
context.pushNamed('leveling');
|
context.pushNamed('leveling');
|
||||||
},
|
},
|
||||||
).padding(horizontal: 12),
|
).padding(horizontal: 12),
|
||||||
const SizedBox.shrink(),
|
if (!isWideScreen(context)) const SizedBox.shrink(),
|
||||||
|
if (!isWideScreen(context))
|
||||||
Row(
|
Row(
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
children: [
|
children: [
|
||||||
@@ -176,7 +176,7 @@ class AccountScreen extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
).padding(horizontal: 16, vertical: 12),
|
).padding(horizontal: 16, vertical: 12),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context.pushNamed('creatorHub');
|
context.goNamed('creatorHub');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
).height(140),
|
).height(140),
|
||||||
@@ -211,71 +211,62 @@ class AccountScreen extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
).padding(horizontal: 12),
|
).padding(horizontal: 12),
|
||||||
const SizedBox.shrink(),
|
const SizedBox.shrink(),
|
||||||
Row(
|
SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: Row(
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Card(
|
||||||
child: Card(
|
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
child: Column(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
spacing: 8,
|
||||||
children: [
|
children: [
|
||||||
Icon(Symbols.settings, size: 28).padding(bottom: 8),
|
Icon(Symbols.settings, size: 20),
|
||||||
Text('appSettings').tr().fontSize(16).bold(),
|
Text('appSettings').tr().fontSize(13).bold(),
|
||||||
],
|
],
|
||||||
).padding(horizontal: 16, vertical: 12),
|
).padding(horizontal: 16, vertical: 12),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context.pushNamed('settings');
|
context.pushNamed('settings');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
).height(120),
|
|
||||||
),
|
),
|
||||||
Expanded(
|
Card(
|
||||||
child: Card(
|
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
child: Column(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
spacing: 8,
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(Symbols.person_edit, size: 20),
|
||||||
Symbols.person_edit,
|
Text('updateYourProfile').tr().fontSize(13).bold(),
|
||||||
size: 28,
|
|
||||||
).padding(bottom: 8),
|
|
||||||
Text('updateYourProfile').tr().fontSize(16).bold(),
|
|
||||||
],
|
],
|
||||||
).padding(horizontal: 16, vertical: 12),
|
).padding(horizontal: 16, vertical: 12),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context.pushNamed('profileUpdate');
|
context.pushNamed('profileUpdate');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
).height(120),
|
|
||||||
),
|
),
|
||||||
Expanded(
|
Card(
|
||||||
child: Card(
|
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
child: Column(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
spacing: 8,
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(Symbols.manage_accounts, size: 20),
|
||||||
Symbols.manage_accounts,
|
Text('accountSettings').tr().fontSize(13).bold(),
|
||||||
size: 28,
|
|
||||||
).padding(bottom: 8),
|
|
||||||
Text('accountSettings').tr().fontSize(16).bold(),
|
|
||||||
],
|
],
|
||||||
).padding(horizontal: 16, vertical: 12),
|
).padding(horizontal: 16, vertical: 12),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context.pushNamed('accountSettings');
|
context.pushNamed('accountSettings');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
).height(120),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
).padding(horizontal: 12),
|
).padding(horizontal: 12),
|
||||||
|
).height(48),
|
||||||
ListTile(
|
ListTile(
|
||||||
minTileHeight: 48,
|
minTileHeight: 48,
|
||||||
leading: const Icon(Symbols.notifications),
|
leading: const Icon(Symbols.notifications),
|
||||||
@@ -291,7 +282,12 @@ class AccountScreen extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context.pushNamed('notifications');
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
useRootNavigator: true,
|
||||||
|
builder: (context) => const NotificationSheet(),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
|
|||||||
@@ -1,42 +1,21 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.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/account.dart';
|
||||||
import 'package:island/models/wallet.dart';
|
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
import 'package:island/pods/userinfo.dart';
|
import 'package:island/pods/userinfo.dart';
|
||||||
import 'package:island/screens/account/credits.dart';
|
import 'package:island/screens/account/credits.dart';
|
||||||
import 'package:island/services/responsive.dart';
|
|
||||||
import 'package:island/services/time.dart';
|
import 'package:island/services/time.dart';
|
||||||
import 'package:island/widgets/account/leveling_progress.dart';
|
import 'package:island/widgets/account/leveling_progress.dart';
|
||||||
import 'package:island/widgets/account/restore_purchase_sheet.dart';
|
import 'package:island/widgets/account/stellar_program_tab.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/payment/payment_overlay.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
|
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
|
||||||
part 'leveling.g.dart';
|
part 'leveling.g.dart';
|
||||||
|
|
||||||
@riverpod
|
|
||||||
Future<SnWalletSubscription?> accountStellarSubscription(Ref ref) async {
|
|
||||||
try {
|
|
||||||
final client = ref.watch(apiClientProvider);
|
|
||||||
final resp = await client.get('/id/subscriptions/fuzzy/solian.stellar');
|
|
||||||
return SnWalletSubscription.fromJson(resp.data);
|
|
||||||
} catch (err) {
|
|
||||||
if (err is DioException && err.response?.statusCode == 404) return null;
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
class LevelingHistoryNotifier extends _$LevelingHistoryNotifier
|
class LevelingHistoryNotifier extends _$LevelingHistoryNotifier
|
||||||
with CursorPagingNotifierMixin<SnExperienceRecord> {
|
with CursorPagingNotifierMixin<SnExperienceRecord> {
|
||||||
@@ -129,7 +108,7 @@ class LevelingScreen extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
_buildLevelingTab(context, ref, user.value!),
|
_buildLevelingTab(context, ref, user.value!),
|
||||||
const SocialCreditsTab(),
|
const SocialCreditsTab(),
|
||||||
_buildStellarProgramTab(context, ref),
|
const StellarProgramTab(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -259,457 +238,12 @@ class LevelingScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
SliverGap(getTabbedPadding(context, vertical: 20).vertical),
|
SliverGap(20),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildStellarProgramTab(BuildContext context, WidgetRef ref) {
|
|
||||||
final stellarSubscription = ref.watch(accountStellarSubscriptionProvider);
|
|
||||||
|
|
||||||
return SingleChildScrollView(
|
|
||||||
padding: getTabbedPadding(context, horizontal: 20, vertical: 20),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
_buildMembershipSection(context, ref, stellarSubscription),
|
|
||||||
const Gap(16),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildMembershipSection(
|
|
||||||
BuildContext context,
|
|
||||||
WidgetRef ref,
|
|
||||||
AsyncValue<SnWalletSubscription?> stellarSubscriptionAsync,
|
|
||||||
) {
|
|
||||||
return stellarSubscriptionAsync.when(
|
|
||||||
data: (membership) => _buildMembershipContent(context, ref, membership),
|
|
||||||
loading:
|
|
||||||
() => Container(
|
|
||||||
width: double.infinity,
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
gradient: LinearGradient(
|
|
||||||
colors: [
|
|
||||||
Theme.of(context).colorScheme.primaryContainer,
|
|
||||||
Theme.of(context).colorScheme.secondaryContainer,
|
|
||||||
],
|
|
||||||
begin: Alignment.topLeft,
|
|
||||||
end: Alignment.bottomRight,
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: const Center(child: CircularProgressIndicator()),
|
|
||||||
),
|
|
||||||
error:
|
|
||||||
(error, stack) => Container(
|
|
||||||
width: double.infinity,
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
gradient: LinearGradient(
|
|
||||||
colors: [
|
|
||||||
Theme.of(context).colorScheme.primaryContainer,
|
|
||||||
Theme.of(context).colorScheme.secondaryContainer,
|
|
||||||
],
|
|
||||||
begin: Alignment.topLeft,
|
|
||||||
end: Alignment.bottomRight,
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: Text('Error loading membership: $error'),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildMembershipContent(
|
|
||||||
BuildContext context,
|
|
||||||
WidgetRef ref,
|
|
||||||
SnWalletSubscription? membership,
|
|
||||||
) {
|
|
||||||
final isActive = membership?.isActive ?? false;
|
|
||||||
|
|
||||||
Future<void> membershipCancel() async {
|
|
||||||
if (!isActive || membership == null) return;
|
|
||||||
|
|
||||||
final confirm = await showConfirmAlert(
|
|
||||||
'membershipCancelHint'.tr(),
|
|
||||||
'membershipCancelConfirm'.tr(),
|
|
||||||
);
|
|
||||||
if (!confirm || !context.mounted) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
showLoadingModal(context);
|
|
||||||
final client = ref.watch(apiClientProvider);
|
|
||||||
await client.post('/id/subscriptions/${membership.identifier}/cancel');
|
|
||||||
ref.invalidate(accountStellarSubscriptionProvider);
|
|
||||||
ref.read(userInfoProvider.notifier).fetchUser();
|
|
||||||
if (context.mounted) {
|
|
||||||
hideLoadingModal(context);
|
|
||||||
showSnackBar('membershipCancelSuccess'.tr());
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
if (context.mounted) hideLoadingModal(context);
|
|
||||||
showErrorAlert(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
width: double.infinity,
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
gradient: LinearGradient(
|
|
||||||
colors: [
|
|
||||||
Theme.of(context).colorScheme.primaryContainer,
|
|
||||||
Theme.of(context).colorScheme.secondaryContainer,
|
|
||||||
],
|
|
||||||
begin: Alignment.topLeft,
|
|
||||||
end: Alignment.bottomRight,
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
isActive ? Icons.star : Icons.star_border,
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
size: 24,
|
|
||||||
),
|
|
||||||
const Gap(8),
|
|
||||||
Text(
|
|
||||||
'stellarMembership'.tr(),
|
|
||||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const Gap(12),
|
|
||||||
|
|
||||||
if (isActive) ...[
|
|
||||||
_buildCurrentMembershipCard(context, membership!),
|
|
||||||
const Gap(12),
|
|
||||||
FilledButton.icon(
|
|
||||||
style: ButtonStyle(
|
|
||||||
backgroundColor: WidgetStateProperty.all(
|
|
||||||
Theme.of(context).colorScheme.error,
|
|
||||||
),
|
|
||||||
foregroundColor: WidgetStateProperty.all(
|
|
||||||
Theme.of(context).colorScheme.onError,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onPressed: membershipCancel,
|
|
||||||
icon: const Icon(Symbols.cancel),
|
|
||||||
label: Text('membershipCancel'.tr()),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
|
|
||||||
if (!isActive) ...[
|
|
||||||
Text(
|
|
||||||
'chooseYourPlan'.tr(),
|
|
||||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
|
|
||||||
),
|
|
||||||
const Gap(12),
|
|
||||||
_buildMembershipTiers(context, ref, membership),
|
|
||||||
],
|
|
||||||
|
|
||||||
// Restore Purchase Button
|
|
||||||
// As you know Apple platform need IAP
|
|
||||||
if (kIsWeb || !(Platform.isIOS || Platform.isMacOS))
|
|
||||||
OutlinedButton.icon(
|
|
||||||
onPressed: () => _showRestorePurchaseSheet(context, ref),
|
|
||||||
icon: const Icon(Icons.restore),
|
|
||||||
label: Text('restorePurchase'.tr()),
|
|
||||||
style: OutlinedButton.styleFrom(
|
|
||||||
minimumSize: const Size(double.infinity, 48),
|
|
||||||
),
|
|
||||||
).padding(top: 12),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildCurrentMembershipCard(
|
|
||||||
BuildContext context,
|
|
||||||
SnWalletSubscription membership,
|
|
||||||
) {
|
|
||||||
final tierName = _getMembershipTierName(membership.identifier);
|
|
||||||
final tierColor = _getMembershipTierColor(context, membership.identifier);
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: tierColor.withOpacity(0.1),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
border: Border.all(color: tierColor, width: 1),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Icon(Icons.verified, color: tierColor, size: 20),
|
|
||||||
const Gap(8),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'currentMembership'.tr(args: [tierName]),
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: tierColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (membership.endedAt != null)
|
|
||||||
Text(
|
|
||||||
'membershipExpires'.tr(
|
|
||||||
args: [membership.endedAt!.formatSystem()],
|
|
||||||
),
|
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildMembershipTiers(
|
|
||||||
BuildContext context,
|
|
||||||
WidgetRef ref,
|
|
||||||
SnWalletSubscription? currentMembership,
|
|
||||||
) {
|
|
||||||
final tiers = [
|
|
||||||
{
|
|
||||||
'id': 'solian.stellar.primary',
|
|
||||||
'name': 'membershipTierStellar'.tr(),
|
|
||||||
'price': 'membershipPriceStellar'.tr(),
|
|
||||||
'color': Colors.blue,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': 'solian.stellar.nova',
|
|
||||||
'name': 'membershipTierNova'.tr(),
|
|
||||||
'price': 'membershipPriceNova'.tr(),
|
|
||||||
'color': Color.fromRGBO(57, 197, 187, 1),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': 'solian.stellar.supernova',
|
|
||||||
'name': 'membershipTierSupernova'.tr(),
|
|
||||||
'price': 'membershipPriceSupernova'.tr(),
|
|
||||||
'color': Colors.orange,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
children:
|
|
||||||
tiers.map((tier) {
|
|
||||||
final isCurrentTier = currentMembership?.identifier == tier['id'];
|
|
||||||
final tierColor = tier['color'] as Color;
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
margin: const EdgeInsets.only(bottom: 8),
|
|
||||||
child: Material(
|
|
||||||
color: Colors.transparent,
|
|
||||||
child: InkWell(
|
|
||||||
onTap:
|
|
||||||
isCurrentTier
|
|
||||||
? null
|
|
||||||
: () => _purchaseMembership(
|
|
||||||
context,
|
|
||||||
ref,
|
|
||||||
tier['id'] as String,
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color:
|
|
||||||
isCurrentTier
|
|
||||||
? tierColor.withOpacity(0.1)
|
|
||||||
: Theme.of(context).colorScheme.surface,
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
border: Border.all(
|
|
||||||
color:
|
|
||||||
isCurrentTier
|
|
||||||
? tierColor
|
|
||||||
: Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.outline.withOpacity(0.2),
|
|
||||||
width: isCurrentTier ? 2 : 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
width: 4,
|
|
||||||
height: 40,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: tierColor,
|
|
||||||
borderRadius: BorderRadius.circular(2),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Gap(12),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
tier['name'] as String,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: isCurrentTier ? tierColor : null,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Gap(8),
|
|
||||||
if (isCurrentTier)
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 6,
|
|
||||||
vertical: 2,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: tierColor,
|
|
||||||
borderRadius: BorderRadius.circular(4),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
'membershipCurrentBadge'.tr(),
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 10,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
tier['price'] as String,
|
|
||||||
style: Theme.of(
|
|
||||||
context,
|
|
||||||
).textTheme.bodyMedium?.copyWith(
|
|
||||||
color:
|
|
||||||
Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (!isCurrentTier)
|
|
||||||
Icon(
|
|
||||||
Icons.arrow_forward_ios,
|
|
||||||
size: 16,
|
|
||||||
color:
|
|
||||||
Theme.of(context).colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String _getMembershipTierName(String identifier) {
|
|
||||||
switch (identifier) {
|
|
||||||
case 'solian.stellar.primary':
|
|
||||||
return 'membershipTierStellar'.tr();
|
|
||||||
case 'solian.stellar.nova':
|
|
||||||
return 'membershipTierNova'.tr();
|
|
||||||
case 'solian.stellar.supernova':
|
|
||||||
return 'membershipTierSupernova'.tr();
|
|
||||||
default:
|
|
||||||
return 'membershipTierUnknown'.tr();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Color _getMembershipTierColor(BuildContext context, String identifier) {
|
|
||||||
switch (identifier) {
|
|
||||||
case 'solian.stellar.primary':
|
|
||||||
return Colors.blue;
|
|
||||||
case 'solian.stellar.nova':
|
|
||||||
return Colors.purple;
|
|
||||||
case 'solian.stellar.supernova':
|
|
||||||
return Colors.orange;
|
|
||||||
default:
|
|
||||||
return Theme.of(context).colorScheme.primary;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _showRestorePurchaseSheet(
|
|
||||||
BuildContext context,
|
|
||||||
WidgetRef ref,
|
|
||||||
) async {
|
|
||||||
await showModalBottomSheet(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => const RestorePurchaseSheet(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _purchaseMembership(
|
|
||||||
BuildContext context,
|
|
||||||
WidgetRef ref,
|
|
||||||
String tierId,
|
|
||||||
) async {
|
|
||||||
final client = ref.watch(apiClientProvider);
|
|
||||||
try {
|
|
||||||
showLoadingModal(context);
|
|
||||||
final resp = await client.post(
|
|
||||||
'/id/subscriptions',
|
|
||||||
data: {
|
|
||||||
'identifier': tierId,
|
|
||||||
'payment_method': 'solian.wallet',
|
|
||||||
'payment_details': {'currency': 'golds'},
|
|
||||||
'cycle_duration_days': 30,
|
|
||||||
},
|
|
||||||
options: Options(headers: {'X-Noop': true}),
|
|
||||||
);
|
|
||||||
final subscription = SnWalletSubscription.fromJson(resp.data);
|
|
||||||
if (subscription.status == 1) return;
|
|
||||||
final orderResp = await client.post(
|
|
||||||
'/id/subscriptions/${subscription.identifier}/order',
|
|
||||||
);
|
|
||||||
final order = SnWalletOrder.fromJson(orderResp.data);
|
|
||||||
|
|
||||||
if (context.mounted) hideLoadingModal(context);
|
|
||||||
|
|
||||||
// Show payment overlay to complete the payment
|
|
||||||
if (!context.mounted) return;
|
|
||||||
final paidOrder = await PaymentOverlay.show(
|
|
||||||
context: context,
|
|
||||||
order: order,
|
|
||||||
enableBiometric: true,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (context.mounted) showLoadingModal(context);
|
|
||||||
|
|
||||||
if (paidOrder != null) {
|
|
||||||
// Wait for server to handle order
|
|
||||||
await Future.delayed(const Duration(seconds: 1));
|
|
||||||
ref.invalidate(accountStellarSubscriptionProvider);
|
|
||||||
ref.read(userInfoProvider.notifier).fetchUser();
|
|
||||||
if (context.mounted) {
|
|
||||||
showSnackBar('membershipPurchaseSuccess'.tr());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
showErrorAlert(err);
|
|
||||||
} finally {
|
|
||||||
if (context.mounted) hideLoadingModal(context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class LevelStairsPainter extends CustomPainter {
|
class LevelStairsPainter extends CustomPainter {
|
||||||
|
|||||||
@@ -6,27 +6,6 @@ part of 'leveling.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$accountStellarSubscriptionHash() =>
|
|
||||||
r'80abcdefb3868775fd8fe3c980215713efff5948';
|
|
||||||
|
|
||||||
/// See also [accountStellarSubscription].
|
|
||||||
@ProviderFor(accountStellarSubscription)
|
|
||||||
final accountStellarSubscriptionProvider =
|
|
||||||
AutoDisposeFutureProvider<SnWalletSubscription?>.internal(
|
|
||||||
accountStellarSubscription,
|
|
||||||
name: r'accountStellarSubscriptionProvider',
|
|
||||||
debugGetCreateSourceHash:
|
|
||||||
const bool.fromEnvironment('dart.vm.product')
|
|
||||||
? null
|
|
||||||
: _$accountStellarSubscriptionHash,
|
|
||||||
dependencies: null,
|
|
||||||
allTransitiveDependencies: null,
|
|
||||||
);
|
|
||||||
|
|
||||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
|
||||||
// ignore: unused_element
|
|
||||||
typedef AccountStellarSubscriptionRef =
|
|
||||||
AutoDisposeFutureProviderRef<SnWalletSubscription?>;
|
|
||||||
String _$levelingHistoryNotifierHash() =>
|
String _$levelingHistoryNotifierHash() =>
|
||||||
r'e795f9b7911c9e50f15c095ea237cb0e87bf1e89';
|
r'e795f9b7911c9e50f15c095ea237cb0e87bf1e89';
|
||||||
|
|
||||||
|
|||||||
@@ -9,11 +9,12 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:island/models/file.dart';
|
import 'package:island/models/file.dart';
|
||||||
import 'package:island/models/account.dart';
|
import 'package:island/models/account.dart';
|
||||||
import 'package:island/pods/config.dart';
|
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
import 'package:island/pods/userinfo.dart';
|
import 'package:island/pods/userinfo.dart';
|
||||||
import 'package:island/services/file.dart';
|
import 'package:island/services/file.dart';
|
||||||
|
import 'package:island/services/file_uploader.dart';
|
||||||
import 'package:island/services/timezone.dart';
|
import 'package:island/services/timezone.dart';
|
||||||
|
import 'package:island/widgets/account/account_name.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/cloud_files.dart';
|
import 'package:island/widgets/content/cloud_files.dart';
|
||||||
@@ -24,6 +25,17 @@ const kServerSupportedLanguages = {'en-US': 'en-us', 'zh-CN': 'zh-hans'};
|
|||||||
const kServerSupportedRegions = ['US', 'JP', 'CN'];
|
const kServerSupportedRegions = ['US', 'JP', 'CN'];
|
||||||
|
|
||||||
class UpdateProfileScreen extends HookConsumerWidget {
|
class UpdateProfileScreen extends HookConsumerWidget {
|
||||||
|
bool _isValidHexColor(String color) {
|
||||||
|
if (!color.startsWith('#')) return false;
|
||||||
|
if (color.length != 7) return false; // #RRGGBB format
|
||||||
|
try {
|
||||||
|
int.parse(color.substring(1), radix: 16);
|
||||||
|
return true;
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const UpdateProfileScreen({super.key});
|
const UpdateProfileScreen({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -62,19 +74,13 @@ class UpdateProfileScreen extends HookConsumerWidget {
|
|||||||
|
|
||||||
submitting.value = true;
|
submitting.value = true;
|
||||||
try {
|
try {
|
||||||
final baseUrl = ref.watch(serverUrlProvider);
|
|
||||||
final token = await getToken(ref.watch(tokenProvider));
|
|
||||||
if (token == null) throw ArgumentError('Token is null');
|
|
||||||
final cloudFile =
|
final cloudFile =
|
||||||
await putFileToCloud(
|
await FileUploader.createCloudFile(
|
||||||
|
client: ref.read(apiClientProvider),
|
||||||
fileData: UniversalFile(
|
fileData: UniversalFile(
|
||||||
data: result,
|
data: result,
|
||||||
type: UniversalFileType.image,
|
type: UniversalFileType.image,
|
||||||
),
|
),
|
||||||
atk: token,
|
|
||||||
baseUrl: baseUrl,
|
|
||||||
filename: result.name,
|
|
||||||
mimetype: result.mimeType ?? 'image/jpeg',
|
|
||||||
).future;
|
).future;
|
||||||
if (cloudFile == null) {
|
if (cloudFile == null) {
|
||||||
throw ArgumentError('Failed to upload the file...');
|
throw ArgumentError('Failed to upload the file...');
|
||||||
@@ -154,12 +160,39 @@ class UpdateProfileScreen extends HookConsumerWidget {
|
|||||||
text: user.value!.profile.timeZone,
|
text: user.value!.profile.timeZone,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Username color state
|
||||||
|
final usernameColorType = useState<String>(
|
||||||
|
user.value!.profile.usernameColor?.type ?? 'plain',
|
||||||
|
);
|
||||||
|
final usernameColorValue = useTextEditingController(
|
||||||
|
text: user.value!.profile.usernameColor?.value,
|
||||||
|
);
|
||||||
|
final usernameColorDirection = useTextEditingController(
|
||||||
|
text: user.value!.profile.usernameColor?.direction,
|
||||||
|
);
|
||||||
|
final usernameColorColors = useState<List<String>>(
|
||||||
|
user.value!.profile.usernameColor?.colors ?? [],
|
||||||
|
);
|
||||||
|
|
||||||
void updateProfile() async {
|
void updateProfile() async {
|
||||||
if (!formKeyProfile.currentState!.validate()) return;
|
if (!formKeyProfile.currentState!.validate()) return;
|
||||||
|
|
||||||
submitting.value = true;
|
submitting.value = true;
|
||||||
try {
|
try {
|
||||||
final client = ref.watch(apiClientProvider);
|
final client = ref.watch(apiClientProvider);
|
||||||
|
final usernameColorData = {
|
||||||
|
'type': usernameColorType.value,
|
||||||
|
if (usernameColorType.value == 'plain' &&
|
||||||
|
usernameColorValue.text.isNotEmpty)
|
||||||
|
'value': usernameColorValue.text,
|
||||||
|
if (usernameColorType.value == 'gradient') ...{
|
||||||
|
if (usernameColorDirection.text.isNotEmpty)
|
||||||
|
'direction': usernameColorDirection.text,
|
||||||
|
'colors':
|
||||||
|
usernameColorColors.value.where((c) => c.isNotEmpty).toList(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
await client.patch(
|
await client.patch(
|
||||||
'/id/accounts/me/profile',
|
'/id/accounts/me/profile',
|
||||||
data: {
|
data: {
|
||||||
@@ -172,6 +205,7 @@ class UpdateProfileScreen extends HookConsumerWidget {
|
|||||||
'location': locationController.text,
|
'location': locationController.text,
|
||||||
'time_zone': timeZoneController.text,
|
'time_zone': timeZoneController.text,
|
||||||
'birthday': birthday.value?.toUtc().toIso8601String(),
|
'birthday': birthday.value?.toUtc().toIso8601String(),
|
||||||
|
'username_color': usernameColorData,
|
||||||
'links':
|
'links':
|
||||||
links.value
|
links.value
|
||||||
.where((e) => e.name.isNotEmpty && e.url.isNotEmpty)
|
.where((e) => e.name.isNotEmpty && e.url.isNotEmpty)
|
||||||
@@ -599,6 +633,320 @@ class UpdateProfileScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
Text(
|
||||||
|
'usernameColor',
|
||||||
|
).tr().bold().fontSize(18).padding(top: 16),
|
||||||
|
Column(
|
||||||
|
spacing: 16,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Preview section
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color:
|
||||||
|
Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.surfaceContainerHighest,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('preview').tr().bold().fontSize(14),
|
||||||
|
const Gap(8),
|
||||||
|
// Create a preview account with the current settings
|
||||||
|
Builder(
|
||||||
|
builder: (context) {
|
||||||
|
final previewAccount = user.value!.copyWith(
|
||||||
|
profile: user.value!.profile.copyWith(
|
||||||
|
usernameColor: UsernameColor(
|
||||||
|
type: usernameColorType.value,
|
||||||
|
value:
|
||||||
|
usernameColorType.value == 'plain' &&
|
||||||
|
usernameColorValue
|
||||||
|
.text
|
||||||
|
.isNotEmpty
|
||||||
|
? usernameColorValue.text
|
||||||
|
: null,
|
||||||
|
direction:
|
||||||
|
usernameColorType.value ==
|
||||||
|
'gradient' &&
|
||||||
|
usernameColorDirection
|
||||||
|
.text
|
||||||
|
.isNotEmpty
|
||||||
|
? usernameColorDirection.text
|
||||||
|
: null,
|
||||||
|
colors:
|
||||||
|
usernameColorType.value == 'gradient'
|
||||||
|
? usernameColorColors.value
|
||||||
|
.where((c) => c.isNotEmpty)
|
||||||
|
.toList()
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if user can use this color
|
||||||
|
final tier =
|
||||||
|
user.value!.perkSubscription?.identifier;
|
||||||
|
final canUseColor = switch (tier) {
|
||||||
|
'solian.stellar.primary' =>
|
||||||
|
usernameColorType.value == 'plain' &&
|
||||||
|
(kUsernamePlainColors.containsKey(
|
||||||
|
usernameColorValue.text,
|
||||||
|
) ||
|
||||||
|
(usernameColorValue.text.startsWith(
|
||||||
|
'#',
|
||||||
|
) &&
|
||||||
|
_isValidHexColor(
|
||||||
|
usernameColorValue.text,
|
||||||
|
))),
|
||||||
|
'solian.stellar.nova' =>
|
||||||
|
usernameColorType.value == 'plain',
|
||||||
|
'solian.stellar.supernova' => true,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
AccountName(
|
||||||
|
account: previewAccount,
|
||||||
|
style: const TextStyle(fontSize: 18),
|
||||||
|
ignorePermissions: true,
|
||||||
|
),
|
||||||
|
const Gap(4),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
canUseColor
|
||||||
|
? Symbols.check_circle
|
||||||
|
: Symbols.error,
|
||||||
|
size: 16,
|
||||||
|
color:
|
||||||
|
canUseColor
|
||||||
|
? Colors.green
|
||||||
|
: Colors.red,
|
||||||
|
),
|
||||||
|
const Gap(4),
|
||||||
|
Text(
|
||||||
|
canUseColor
|
||||||
|
? 'availableWithYourPlan'.tr()
|
||||||
|
: 'upgradeRequired'.tr(),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color:
|
||||||
|
canUseColor
|
||||||
|
? Colors.green
|
||||||
|
: Colors.red,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DropdownButtonFormField2<String>(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'colorType'.tr(),
|
||||||
|
),
|
||||||
|
items: [
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: 'plain',
|
||||||
|
child: Text('plain'.tr()),
|
||||||
|
),
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: 'gradient',
|
||||||
|
child: Text('gradient'.tr()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
value: usernameColorType.value,
|
||||||
|
onChanged: (value) {
|
||||||
|
usernameColorType.value = value ?? 'plain';
|
||||||
|
},
|
||||||
|
customButton: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(child: Text(usernameColorType.value).tr()),
|
||||||
|
Icon(Symbols.arrow_drop_down),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (usernameColorType.value == 'plain')
|
||||||
|
Autocomplete<String>(
|
||||||
|
optionsBuilder: (TextEditingValue textEditingValue) {
|
||||||
|
final options = kUsernamePlainColors.keys.toList();
|
||||||
|
if (textEditingValue.text == '') {
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
return options.where(
|
||||||
|
(option) => option.toLowerCase().contains(
|
||||||
|
textEditingValue.text.toLowerCase(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onSelected: (String selection) {
|
||||||
|
usernameColorValue.text = selection;
|
||||||
|
},
|
||||||
|
fieldViewBuilder: (
|
||||||
|
context,
|
||||||
|
controller,
|
||||||
|
focusNode,
|
||||||
|
onFieldSubmitted,
|
||||||
|
) {
|
||||||
|
// Initialize the controller with the current value
|
||||||
|
if (controller.text.isEmpty &&
|
||||||
|
usernameColorValue.text.isNotEmpty) {
|
||||||
|
controller.text = usernameColorValue.text;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TextFormField(
|
||||||
|
controller: controller,
|
||||||
|
focusNode: focusNode,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'colorValue'.tr(),
|
||||||
|
hintText: 'e.g. red or #ff6600',
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
usernameColorValue.text = value;
|
||||||
|
},
|
||||||
|
onTapOutside:
|
||||||
|
(_) =>
|
||||||
|
FocusManager.instance.primaryFocus
|
||||||
|
?.unfocus(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (usernameColorType.value == 'gradient') ...[
|
||||||
|
DropdownButtonFormField2<String>(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'gradientDirection'.tr(),
|
||||||
|
),
|
||||||
|
items: [
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: 'to right',
|
||||||
|
child: Text('gradientDirectionToRight'.tr()),
|
||||||
|
),
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: 'to left',
|
||||||
|
child: Text('gradientDirectionToLeft'.tr()),
|
||||||
|
),
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: 'to bottom',
|
||||||
|
child: Text('gradientDirectionToBottom'.tr()),
|
||||||
|
),
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: 'to top',
|
||||||
|
child: Text('gradientDirectionToTop'.tr()),
|
||||||
|
),
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: 'to bottom right',
|
||||||
|
child: Text(
|
||||||
|
'gradientDirectionToBottomRight'.tr(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: 'to bottom left',
|
||||||
|
child: Text('gradientDirectionToBottomLeft'.tr()),
|
||||||
|
),
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: 'to top right',
|
||||||
|
child: Text('gradientDirectionToTopRight'.tr()),
|
||||||
|
),
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: 'to top left',
|
||||||
|
child: Text('gradientDirectionToTopLeft'.tr()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
value:
|
||||||
|
usernameColorDirection.text.isNotEmpty
|
||||||
|
? usernameColorDirection.text
|
||||||
|
: 'to right',
|
||||||
|
onChanged: (value) {
|
||||||
|
usernameColorDirection.text = value ?? 'to right';
|
||||||
|
},
|
||||||
|
customButton: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
usernameColorDirection.text.isNotEmpty
|
||||||
|
? usernameColorDirection.text
|
||||||
|
: 'to right',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Icon(Symbols.arrow_drop_down),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'gradientColors',
|
||||||
|
).tr().bold().fontSize(14).padding(top: 8),
|
||||||
|
Column(
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
for (
|
||||||
|
var i = 0;
|
||||||
|
i < usernameColorColors.value.length;
|
||||||
|
i++
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
key: ValueKey(
|
||||||
|
usernameColorColors.value[i].hashCode,
|
||||||
|
),
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextFormField(
|
||||||
|
initialValue:
|
||||||
|
usernameColorColors.value[i],
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'color'.tr(),
|
||||||
|
hintText: 'e.g. #ff0000',
|
||||||
|
isDense: true,
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
usernameColorColors.value[i] = value;
|
||||||
|
},
|
||||||
|
onTapOutside:
|
||||||
|
(_) =>
|
||||||
|
FocusManager.instance.primaryFocus
|
||||||
|
?.unfocus(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Symbols.delete),
|
||||||
|
onPressed: () {
|
||||||
|
usernameColorColors.value =
|
||||||
|
usernameColorColors.value
|
||||||
|
.whereIndexed(
|
||||||
|
(idx, _) => idx != i,
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: FilledButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
usernameColorColors.value = List.from(
|
||||||
|
usernameColorColors.value,
|
||||||
|
)..add('');
|
||||||
|
},
|
||||||
|
label: Text('addColor').tr(),
|
||||||
|
icon: const Icon(Symbols.add),
|
||||||
|
).padding(top: 8),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
Text('links').tr().bold().fontSize(18).padding(top: 16),
|
Text('links').tr().bold().fontSize(18).padding(top: 16),
|
||||||
Column(
|
Column(
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
|
|||||||
@@ -97,9 +97,7 @@ class _AccountBasicInfo extends StatelessWidget {
|
|||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
SharePlus.instance.share(
|
SharePlus.instance.share(
|
||||||
ShareParams(
|
ShareParams(uri: Uri.parse('https://solian.app/@${data.name}')),
|
||||||
uri: Uri.parse('https://solian.app/@${data.name}'),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: const Icon(Symbols.share),
|
icon: const Icon(Symbols.share),
|
||||||
@@ -879,7 +877,7 @@ class AccountProfileScreen extends HookConsumerWidget {
|
|||||||
child: Card(
|
child: Card(
|
||||||
child: FortuneGraphWidget(
|
child: FortuneGraphWidget(
|
||||||
events: accountEvents,
|
events: accountEvents,
|
||||||
eventCalanderUser: data.name,
|
eventCalandarUser: data.name,
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1004,7 +1002,7 @@ class AccountProfileScreen extends HookConsumerWidget {
|
|||||||
child: Card(
|
child: Card(
|
||||||
child: FortuneGraphWidget(
|
child: FortuneGraphWidget(
|
||||||
events: accountEvents,
|
events: accountEvents,
|
||||||
eventCalanderUser: data.name,
|
eventCalandarUser: data.name,
|
||||||
),
|
),
|
||||||
).padding(horizontal: 4),
|
).padding(horizontal: 4),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:island/pods/network.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
part 'captcha.config.g.dart';
|
part 'captcha.config.g.dart';
|
||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
Future<String> captchaUrl(Ref ref) async {
|
Future<String> captchaUrl(Ref ref) async {
|
||||||
const baseUrl = "https://solian.app";
|
final apiClient = ref.watch(apiClientProvider);
|
||||||
|
final baseUrl = await apiClient.get('/config/site');
|
||||||
return '$baseUrl/auth/captcha';
|
return '$baseUrl/auth/captcha';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ part of 'captcha.config.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$captchaUrlHash() => r'd46bc43032cef504547cd528a40c23cf76f27cc8';
|
String _$captchaUrlHash() => r'5d59de4f26a0544bf4fbd5209943f0b111959ce6';
|
||||||
|
|
||||||
/// See also [captchaUrl].
|
/// See also [captchaUrl].
|
||||||
@ProviderFor(captchaUrl)
|
@ProviderFor(captchaUrl)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart' hide ConnectionState;
|
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';
|
||||||
@@ -9,8 +9,6 @@ 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_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: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';
|
||||||
|
|
||||||
@@ -26,32 +24,13 @@ class CallScreen extends HookConsumerWidget {
|
|||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
talker.info('[Call] Joining the call...');
|
talker.info('[Call] Joining the call...');
|
||||||
callNotifier.joinRoom(roomId).catchError((_) {
|
Future(() {
|
||||||
showConfirmAlert(
|
|
||||||
'Seems there already has a call connected, do you want override it?',
|
|
||||||
'Call already connected',
|
|
||||||
).then((value) {
|
|
||||||
if (value != true) return;
|
|
||||||
talker.info('[Call] Joining the call... with overrides');
|
|
||||||
callNotifier.disconnect();
|
|
||||||
callNotifier.dispose();
|
|
||||||
callNotifier.joinRoom(roomId);
|
callNotifier.joinRoom(roomId);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
return null;
|
return null;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
final allAudioOnly = callNotifier.participants.every(
|
final allAudioOnly = callNotifier.participants.every((p) => !p.hasVideo);
|
||||||
(p) =>
|
|
||||||
!(p.hasVideo &&
|
|
||||||
p.remoteParticipant.trackPublications.values.any(
|
|
||||||
(pub) =>
|
|
||||||
pub.track != null &&
|
|
||||||
pub.kind == TrackType.VIDEO &&
|
|
||||||
!pub.muted &&
|
|
||||||
!pub.isDisposed,
|
|
||||||
)),
|
|
||||||
);
|
|
||||||
|
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
isNoBackground: false,
|
isNoBackground: false,
|
||||||
@@ -67,12 +46,7 @@ class CallScreen extends HookConsumerWidget {
|
|||||||
Text(
|
Text(
|
||||||
callState.isConnected
|
callState.isConnected
|
||||||
? formatDuration(callState.duration)
|
? formatDuration(callState.duration)
|
||||||
: (switch (callNotifier.room?.connectionState) {
|
: 'connecting'.tr(),
|
||||||
ConnectionState.connected => 'connected',
|
|
||||||
ConnectionState.connecting => 'connecting',
|
|
||||||
ConnectionState.reconnecting => 'reconnecting',
|
|
||||||
_ => 'disconnected',
|
|
||||||
}).tr(),
|
|
||||||
style: const TextStyle(fontSize: 14),
|
style: const TextStyle(fontSize: 14),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -159,19 +133,7 @@ class CallScreen extends HookConsumerWidget {
|
|||||||
|
|
||||||
// Stage view: show main speaker(s) large, others in row
|
// Stage view: show main speaker(s) large, others in row
|
||||||
final mainSpeakers =
|
final mainSpeakers =
|
||||||
participants
|
participants.where((p) => p.hasVideo).toList();
|
||||||
.where(
|
|
||||||
(p) => p
|
|
||||||
.remoteParticipant
|
|
||||||
.trackPublications
|
|
||||||
.values
|
|
||||||
.any(
|
|
||||||
(pub) =>
|
|
||||||
pub.track != null &&
|
|
||||||
pub.kind == TrackType.VIDEO,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
if (mainSpeakers.isEmpty && participants.isNotEmpty) {
|
if (mainSpeakers.isEmpty && participants.isNotEmpty) {
|
||||||
mainSpeakers.add(participants.first);
|
mainSpeakers.add(participants.first);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:croppy/croppy.dart' hide cropImage;
|
|
||||||
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';
|
||||||
@@ -7,25 +5,18 @@ import 'package:go_router/go_router.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:image_picker/image_picker.dart';
|
|
||||||
import 'package:island/models/chat.dart';
|
import 'package:island/models/chat.dart';
|
||||||
import 'package:island/models/file.dart';
|
|
||||||
import 'package:island/models/realm.dart';
|
|
||||||
import 'package:island/pods/chat/call.dart';
|
import 'package:island/pods/chat/call.dart';
|
||||||
import 'package:island/pods/chat/chat_summary.dart';
|
import 'package:island/pods/chat/chat_summary.dart';
|
||||||
import 'package:island/pods/config.dart';
|
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
import 'package:island/screens/realm/realms.dart';
|
import 'package:island/screens/realm/realms.dart';
|
||||||
import 'package:island/services/file.dart';
|
|
||||||
import 'package:island/services/responsive.dart';
|
import 'package:island/services/responsive.dart';
|
||||||
import 'package:island/widgets/account/account_picker.dart';
|
import 'package:island/widgets/account/account_picker.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/cloud_files.dart';
|
import 'package:island/widgets/content/cloud_files.dart';
|
||||||
import 'package:island/widgets/content/sheet.dart';
|
import 'package:island/widgets/content/sheet.dart';
|
||||||
import 'package:island/widgets/realm/realm_selection_dropdown.dart';
|
|
||||||
import 'package:island/widgets/response.dart';
|
import 'package:island/widgets/response.dart';
|
||||||
import 'package:island/screens/tabs.dart';
|
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
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';
|
||||||
@@ -219,9 +210,8 @@ class ChatListBodyWidget extends HookConsumerWidget {
|
|||||||
ref.invalidate(chatroomsJoinedProvider);
|
ref.invalidate(chatroomsJoinedProvider);
|
||||||
}),
|
}),
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
padding: getTabbedPadding(
|
padding: EdgeInsets.only(
|
||||||
context,
|
bottom: callState.isConnected ? 96 : 0,
|
||||||
bottom: callState.isConnected ? 96 : null,
|
|
||||||
),
|
),
|
||||||
itemCount:
|
itemCount:
|
||||||
items
|
items
|
||||||
@@ -531,7 +521,6 @@ class ChatListScreen extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
child: const Icon(Symbols.add),
|
child: const Icon(Symbols.add),
|
||||||
),
|
),
|
||||||
floatingActionButtonLocation: TabbedFabLocation(context),
|
|
||||||
body: ChatListBodyWidget(
|
body: ChatListBodyWidget(
|
||||||
isFloating: false,
|
isFloating: false,
|
||||||
tabController: tabController,
|
tabController: tabController,
|
||||||
@@ -571,275 +560,6 @@ Future<SnChatMember?> chatroomIdentity(Ref ref, String? identifier) async {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class NewChatScreen extends StatelessWidget {
|
|
||||||
const NewChatScreen({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return const EditChatScreen();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class EditChatScreen extends HookConsumerWidget {
|
|
||||||
final String? id;
|
|
||||||
const EditChatScreen({super.key, this.id});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final formKey = useMemoized(() => GlobalKey<FormState>(), []);
|
|
||||||
|
|
||||||
final submitting = useState(false);
|
|
||||||
|
|
||||||
final nameController = useTextEditingController();
|
|
||||||
final descriptionController = useTextEditingController();
|
|
||||||
final picture = useState<SnCloudFile?>(null);
|
|
||||||
final background = useState<SnCloudFile?>(null);
|
|
||||||
final isPublic = useState(true);
|
|
||||||
final isCommunity = useState(false);
|
|
||||||
|
|
||||||
final chat = ref.watch(chatroomProvider(id));
|
|
||||||
|
|
||||||
final joinedRealms = ref.watch(realmsJoinedProvider);
|
|
||||||
final currentRealm = useState<SnRealm?>(null);
|
|
||||||
|
|
||||||
useEffect(() {
|
|
||||||
if (chat.value != null) {
|
|
||||||
nameController.text = chat.value!.name ?? '';
|
|
||||||
descriptionController.text = chat.value!.description ?? '';
|
|
||||||
picture.value = chat.value!.picture;
|
|
||||||
background.value = chat.value!.background;
|
|
||||||
isPublic.value = chat.value!.isPublic;
|
|
||||||
isCommunity.value = chat.value!.isCommunity;
|
|
||||||
currentRealm.value = joinedRealms.value?.firstWhereOrNull(
|
|
||||||
(realm) => realm.id == chat.value!.realmId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}, [chat, joinedRealms]);
|
|
||||||
|
|
||||||
void setPicture(String position) async {
|
|
||||||
showLoadingModal(context);
|
|
||||||
var result = await ref
|
|
||||||
.read(imagePickerProvider)
|
|
||||||
.pickImage(source: ImageSource.gallery);
|
|
||||||
if (result == null) {
|
|
||||||
if (context.mounted) hideLoadingModal(context);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!context.mounted) return;
|
|
||||||
hideLoadingModal(context);
|
|
||||||
result = await cropImage(
|
|
||||||
context,
|
|
||||||
image: result,
|
|
||||||
allowedAspectRatios: [
|
|
||||||
if (position == 'background')
|
|
||||||
const CropAspectRatio(height: 7, width: 16)
|
|
||||||
else
|
|
||||||
const CropAspectRatio(height: 1, width: 1),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
if (result == null) {
|
|
||||||
if (context.mounted) hideLoadingModal(context);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!context.mounted) return;
|
|
||||||
showLoadingModal(context);
|
|
||||||
|
|
||||||
submitting.value = true;
|
|
||||||
try {
|
|
||||||
final baseUrl = ref.watch(serverUrlProvider);
|
|
||||||
final token = await getToken(ref.watch(tokenProvider));
|
|
||||||
if (token == null) throw ArgumentError('Token is null');
|
|
||||||
final cloudFile =
|
|
||||||
await putFileToCloud(
|
|
||||||
fileData: UniversalFile(
|
|
||||||
data: result,
|
|
||||||
type: UniversalFileType.image,
|
|
||||||
),
|
|
||||||
atk: token,
|
|
||||||
baseUrl: baseUrl,
|
|
||||||
filename: result.name,
|
|
||||||
mimetype: result.mimeType ?? 'image/jpeg',
|
|
||||||
).future;
|
|
||||||
if (cloudFile == null) {
|
|
||||||
throw ArgumentError('Failed to upload the file...');
|
|
||||||
}
|
|
||||||
switch (position) {
|
|
||||||
case 'picture':
|
|
||||||
picture.value = cloudFile;
|
|
||||||
case 'background':
|
|
||||||
background.value = cloudFile;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
showErrorAlert(err);
|
|
||||||
} finally {
|
|
||||||
if (context.mounted) hideLoadingModal(context);
|
|
||||||
submitting.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> performAction() async {
|
|
||||||
if (!formKey.currentState!.validate()) return;
|
|
||||||
|
|
||||||
submitting.value = true;
|
|
||||||
try {
|
|
||||||
final client = ref.watch(apiClientProvider);
|
|
||||||
final resp = await client.request(
|
|
||||||
id == null ? '/sphere/chat' : '/sphere/chat/$id',
|
|
||||||
data: {
|
|
||||||
'name': nameController.text,
|
|
||||||
'description': descriptionController.text,
|
|
||||||
'background_id': background.value?.id,
|
|
||||||
'picture_id': picture.value?.id,
|
|
||||||
'realm_id': currentRealm.value?.id,
|
|
||||||
'is_public': isPublic.value,
|
|
||||||
'is_community': isCommunity.value,
|
|
||||||
},
|
|
||||||
options: Options(method: id == null ? 'POST' : 'PATCH'),
|
|
||||||
);
|
|
||||||
if (context.mounted) {
|
|
||||||
context.pop(SnChatRoom.fromJson(resp.data));
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
showErrorAlert(err);
|
|
||||||
} finally {
|
|
||||||
submitting.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return AppScaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title: Text(id == null ? 'createChatRoom' : 'editChatRoom').tr(),
|
|
||||||
leading: const PageBackButton(),
|
|
||||||
),
|
|
||||||
body: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
RealmSelectionDropdown(
|
|
||||||
value: currentRealm.value,
|
|
||||||
realms: joinedRealms.when(
|
|
||||||
data: (realms) => realms,
|
|
||||||
loading: () => [],
|
|
||||||
error: (_, _) => [],
|
|
||||||
),
|
|
||||||
onChanged: (SnRealm? value) {
|
|
||||||
currentRealm.value = value;
|
|
||||||
},
|
|
||||||
isLoading: joinedRealms.isLoading,
|
|
||||||
error: joinedRealms.error?.toString(),
|
|
||||||
),
|
|
||||||
AspectRatio(
|
|
||||||
aspectRatio: 16 / 7,
|
|
||||||
child: Stack(
|
|
||||||
clipBehavior: Clip.none,
|
|
||||||
fit: StackFit.expand,
|
|
||||||
children: [
|
|
||||||
GestureDetector(
|
|
||||||
child: Container(
|
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
|
||||||
child:
|
|
||||||
background.value != null
|
|
||||||
? CloudFileWidget(
|
|
||||||
item: background.value!,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
)
|
|
||||||
: const SizedBox.shrink(),
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
setPicture('background');
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
left: 20,
|
|
||||||
bottom: -32,
|
|
||||||
child: GestureDetector(
|
|
||||||
child: ProfilePictureWidget(
|
|
||||||
fileId: picture.value?.id,
|
|
||||||
radius: 40,
|
|
||||||
fallbackIcon: Symbols.group,
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
setPicture('picture');
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
).padding(bottom: 32),
|
|
||||||
Form(
|
|
||||||
key: formKey,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
TextFormField(
|
|
||||||
controller: nameController,
|
|
||||||
decoration: const InputDecoration(labelText: 'Name'),
|
|
||||||
onTapOutside:
|
|
||||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
TextFormField(
|
|
||||||
controller: descriptionController,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Description',
|
|
||||||
alignLabelWithHint: true,
|
|
||||||
),
|
|
||||||
minLines: 3,
|
|
||||||
maxLines: null,
|
|
||||||
onTapOutside:
|
|
||||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Card(
|
|
||||||
margin: EdgeInsets.zero,
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
CheckboxListTile(
|
|
||||||
secondary: const Icon(Symbols.public),
|
|
||||||
title: Text('publicChat').tr(),
|
|
||||||
subtitle: Text('publicChatDescription').tr(),
|
|
||||||
value: isPublic.value,
|
|
||||||
onChanged: (value) {
|
|
||||||
isPublic.value = value ?? true;
|
|
||||||
},
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
CheckboxListTile(
|
|
||||||
secondary: const Icon(Symbols.travel_explore),
|
|
||||||
title: Text('communityChat').tr(),
|
|
||||||
subtitle: Text('communityChatDescription').tr(),
|
|
||||||
value: isCommunity.value,
|
|
||||||
onChanged: (value) {
|
|
||||||
isCommunity.value = value ?? false;
|
|
||||||
},
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.centerRight,
|
|
||||||
child: TextButton.icon(
|
|
||||||
onPressed: submitting.value ? null : performAction,
|
|
||||||
label: const Text('Save'),
|
|
||||||
icon: const Icon(Symbols.save),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).padding(all: 24),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
Future<List<SnChatMember>> chatroomInvites(Ref ref) async {
|
Future<List<SnChatMember>> chatroomInvites(Ref ref) async {
|
||||||
final client = ref.watch(apiClientProvider);
|
final client = ref.watch(apiClientProvider);
|
||||||
|
|||||||
299
lib/screens/chat/chat_form.dart
Normal file
299
lib/screens/chat/chat_form.dart
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:croppy/croppy.dart' hide cropImage;
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
import 'package:island/models/chat.dart';
|
||||||
|
import 'package:island/models/file.dart';
|
||||||
|
import 'package:island/models/realm.dart';
|
||||||
|
import 'package:island/pods/network.dart';
|
||||||
|
import 'package:island/screens/chat/chat.dart';
|
||||||
|
import 'package:island/screens/realm/realms.dart';
|
||||||
|
import 'package:island/services/file.dart';
|
||||||
|
import 'package:island/services/file_uploader.dart';
|
||||||
|
import 'package:island/widgets/alert.dart';
|
||||||
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
|
import 'package:island/widgets/content/cloud_files.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
|
||||||
|
class NewChatScreen extends StatelessWidget {
|
||||||
|
const NewChatScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const EditChatScreen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EditChatScreen extends HookConsumerWidget {
|
||||||
|
final String? id;
|
||||||
|
const EditChatScreen({super.key, this.id});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final formKey = useMemoized(() => GlobalKey<FormState>(), []);
|
||||||
|
|
||||||
|
final submitting = useState(false);
|
||||||
|
|
||||||
|
final nameController = useTextEditingController();
|
||||||
|
final descriptionController = useTextEditingController();
|
||||||
|
final picture = useState<SnCloudFile?>(null);
|
||||||
|
final background = useState<SnCloudFile?>(null);
|
||||||
|
final isPublic = useState(true);
|
||||||
|
final isCommunity = useState(false);
|
||||||
|
|
||||||
|
final chat = ref.watch(chatroomProvider(id));
|
||||||
|
|
||||||
|
final joinedRealms = ref.watch(realmsJoinedProvider);
|
||||||
|
final currentRealm = useState<SnRealm?>(null);
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
if (chat.value != null) {
|
||||||
|
nameController.text = chat.value!.name ?? '';
|
||||||
|
descriptionController.text = chat.value!.description ?? '';
|
||||||
|
picture.value = chat.value!.picture;
|
||||||
|
background.value = chat.value!.background;
|
||||||
|
isPublic.value = chat.value!.isPublic;
|
||||||
|
isCommunity.value = chat.value!.isCommunity;
|
||||||
|
currentRealm.value = joinedRealms.value?.firstWhereOrNull(
|
||||||
|
(realm) => realm.id == chat.value!.realmId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}, [chat, joinedRealms]);
|
||||||
|
|
||||||
|
void setPicture(String position) async {
|
||||||
|
showLoadingModal(context);
|
||||||
|
var result = await ref
|
||||||
|
.read(imagePickerProvider)
|
||||||
|
.pickImage(source: ImageSource.gallery);
|
||||||
|
if (result == null) {
|
||||||
|
if (context.mounted) hideLoadingModal(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!context.mounted) return;
|
||||||
|
hideLoadingModal(context);
|
||||||
|
result = await cropImage(
|
||||||
|
context,
|
||||||
|
image: result,
|
||||||
|
allowedAspectRatios: [
|
||||||
|
if (position == 'background')
|
||||||
|
const CropAspectRatio(height: 7, width: 16)
|
||||||
|
else
|
||||||
|
const CropAspectRatio(height: 1, width: 1),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
if (result == null) {
|
||||||
|
if (context.mounted) hideLoadingModal(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!context.mounted) return;
|
||||||
|
showLoadingModal(context);
|
||||||
|
|
||||||
|
submitting.value = true;
|
||||||
|
try {
|
||||||
|
final cloudFile =
|
||||||
|
await FileUploader.createCloudFile(
|
||||||
|
client: ref.read(apiClientProvider),
|
||||||
|
fileData: UniversalFile(
|
||||||
|
data: result,
|
||||||
|
type: UniversalFileType.image,
|
||||||
|
),
|
||||||
|
).future;
|
||||||
|
if (cloudFile == null) {
|
||||||
|
throw ArgumentError('Failed to upload the file...');
|
||||||
|
}
|
||||||
|
switch (position) {
|
||||||
|
case 'picture':
|
||||||
|
picture.value = cloudFile;
|
||||||
|
case 'background':
|
||||||
|
background.value = cloudFile;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
showErrorAlert(err);
|
||||||
|
} finally {
|
||||||
|
if (context.mounted) hideLoadingModal(context);
|
||||||
|
submitting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> performAction() async {
|
||||||
|
if (!formKey.currentState!.validate()) return;
|
||||||
|
|
||||||
|
submitting.value = true;
|
||||||
|
try {
|
||||||
|
final client = ref.watch(apiClientProvider);
|
||||||
|
final resp = await client.request(
|
||||||
|
id == null ? '/sphere/chat' : '/sphere/chat/$id',
|
||||||
|
data: {
|
||||||
|
'name': nameController.text,
|
||||||
|
'description': descriptionController.text,
|
||||||
|
'background_id': background.value?.id,
|
||||||
|
'picture_id': picture.value?.id,
|
||||||
|
'realm_id': currentRealm.value?.id,
|
||||||
|
'is_public': isPublic.value,
|
||||||
|
'is_community': isCommunity.value,
|
||||||
|
},
|
||||||
|
options: Options(method: id == null ? 'POST' : 'PATCH'),
|
||||||
|
);
|
||||||
|
if (context.mounted) {
|
||||||
|
context.pop(SnChatRoom.fromJson(resp.data));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
showErrorAlert(err);
|
||||||
|
} finally {
|
||||||
|
submitting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return AppScaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(id == null ? 'createChatRoom' : 'editChatRoom').tr(),
|
||||||
|
leading: const PageBackButton(),
|
||||||
|
),
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
AspectRatio(
|
||||||
|
aspectRatio: 16 / 7,
|
||||||
|
child: Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
GestureDetector(
|
||||||
|
child: Container(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
|
child:
|
||||||
|
background.value != null
|
||||||
|
? CloudFileWidget(
|
||||||
|
item: background.value!,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
setPicture('background');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
left: 20,
|
||||||
|
bottom: -32,
|
||||||
|
child: GestureDetector(
|
||||||
|
child: ProfilePictureWidget(
|
||||||
|
fileId: picture.value?.id,
|
||||||
|
radius: 40,
|
||||||
|
fallbackIcon: Symbols.group,
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
setPicture('picture');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
).padding(bottom: 32),
|
||||||
|
Form(
|
||||||
|
key: formKey,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
TextFormField(
|
||||||
|
controller: nameController,
|
||||||
|
decoration: const InputDecoration(labelText: 'Name'),
|
||||||
|
onTapOutside:
|
||||||
|
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
controller: descriptionController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Description',
|
||||||
|
alignLabelWithHint: true,
|
||||||
|
),
|
||||||
|
minLines: 3,
|
||||||
|
maxLines: null,
|
||||||
|
onTapOutside:
|
||||||
|
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
DropdownButtonFormField<SnRealm>(
|
||||||
|
value: currentRealm.value,
|
||||||
|
decoration: InputDecoration(labelText: 'realm'.tr()),
|
||||||
|
items: [
|
||||||
|
DropdownMenuItem<SnRealm>(
|
||||||
|
value: null,
|
||||||
|
child: Text('none'.tr()),
|
||||||
|
),
|
||||||
|
...joinedRealms.maybeWhen(
|
||||||
|
data:
|
||||||
|
(realms) => realms.map(
|
||||||
|
(realm) => DropdownMenuItem(
|
||||||
|
value: realm,
|
||||||
|
child: Text(realm.name),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
orElse: () => [],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onChanged:
|
||||||
|
joinedRealms.isLoading
|
||||||
|
? null
|
||||||
|
: (SnRealm? value) {
|
||||||
|
currentRealm.value = value;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Card(
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
CheckboxListTile(
|
||||||
|
secondary: const Icon(Symbols.public),
|
||||||
|
title: Text('publicChat').tr(),
|
||||||
|
subtitle: Text('publicChatDescription').tr(),
|
||||||
|
value: isPublic.value,
|
||||||
|
onChanged: (value) {
|
||||||
|
isPublic.value = value ?? true;
|
||||||
|
},
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
CheckboxListTile(
|
||||||
|
secondary: const Icon(Symbols.travel_explore),
|
||||||
|
title: Text('communityChat').tr(),
|
||||||
|
subtitle: Text('communityChatDescription').tr(),
|
||||||
|
value: isCommunity.value,
|
||||||
|
onChanged: (value) {
|
||||||
|
isCommunity.value = value ?? false;
|
||||||
|
},
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: TextButton.icon(
|
||||||
|
onPressed: submitting.value ? null : performAction,
|
||||||
|
label: const Text('Save'),
|
||||||
|
icon: const Icon(Symbols.save),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(all: 24),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,11 +11,12 @@ import "package:island/models/chat.dart";
|
|||||||
import "package:island/models/file.dart";
|
import "package:island/models/file.dart";
|
||||||
import "package:island/pods/chat/chat_rooms.dart";
|
import "package:island/pods/chat/chat_rooms.dart";
|
||||||
import "package:island/pods/chat/chat_subscribe.dart";
|
import "package:island/pods/chat/chat_subscribe.dart";
|
||||||
import "package:island/pods/config.dart";
|
|
||||||
import "package:island/pods/chat/messages_notifier.dart";
|
import "package:island/pods/chat/messages_notifier.dart";
|
||||||
import "package:island/pods/network.dart";
|
import "package:island/pods/network.dart";
|
||||||
import "package:island/pods/chat/chat_online_count.dart";
|
import "package:island/pods/chat/chat_online_count.dart";
|
||||||
import "package:island/services/file.dart";
|
import "package:island/pods/config.dart";
|
||||||
|
import "package:island/screens/chat/search_messages.dart";
|
||||||
|
import "package:island/services/file_uploader.dart";
|
||||||
import "package:island/screens/chat/chat.dart";
|
import "package:island/screens/chat/chat.dart";
|
||||||
import "package:island/services/responsive.dart";
|
import "package:island/services/responsive.dart";
|
||||||
import "package:island/widgets/alert.dart";
|
import "package:island/widgets/alert.dart";
|
||||||
@@ -24,7 +25,6 @@ import "package:island/widgets/attachment_uploader.dart";
|
|||||||
import "package:island/widgets/chat/call_overlay.dart";
|
import "package:island/widgets/chat/call_overlay.dart";
|
||||||
import "package:island/widgets/chat/message_item.dart";
|
import "package:island/widgets/chat/message_item.dart";
|
||||||
import "package:island/widgets/content/cloud_files.dart";
|
import "package:island/widgets/content/cloud_files.dart";
|
||||||
import "package:island/widgets/post/compose_shared.dart";
|
|
||||||
import "package:island/widgets/response.dart";
|
import "package:island/widgets/response.dart";
|
||||||
import "package:material_symbols_icons/material_symbols_icons.dart";
|
import "package:material_symbols_icons/material_symbols_icons.dart";
|
||||||
import "package:styled_widget/styled_widget.dart";
|
import "package:styled_widget/styled_widget.dart";
|
||||||
@@ -32,6 +32,7 @@ import "package:super_sliver_list/super_sliver_list.dart";
|
|||||||
import "package:material_symbols_icons/symbols.dart";
|
import "package:material_symbols_icons/symbols.dart";
|
||||||
import "package:island/widgets/chat/call_button.dart";
|
import "package:island/widgets/chat/call_button.dart";
|
||||||
import "package:island/widgets/chat/chat_input.dart";
|
import "package:island/widgets/chat/chat_input.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";
|
||||||
|
|
||||||
class ChatRoomScreen extends HookConsumerWidget {
|
class ChatRoomScreen extends HookConsumerWidget {
|
||||||
@@ -44,6 +45,7 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
final chatIdentity = ref.watch(chatroomIdentityProvider(id));
|
final chatIdentity = ref.watch(chatroomIdentityProvider(id));
|
||||||
final isSyncing = ref.watch(isSyncingProvider);
|
final isSyncing = ref.watch(isSyncingProvider);
|
||||||
final onlineCount = ref.watch(chatOnlineCountNotifierProvider(id));
|
final onlineCount = ref.watch(chatOnlineCountNotifierProvider(id));
|
||||||
|
final settings = ref.watch(appSettingsNotifierProvider);
|
||||||
|
|
||||||
final hasOnlineCount = onlineCount.hasValue;
|
final hasOnlineCount = onlineCount.hasValue;
|
||||||
|
|
||||||
@@ -144,6 +146,7 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
final attachmentProgress = useState<Map<String, Map<int, double>>>({});
|
final attachmentProgress = useState<Map<String, Map<int, double>>>({});
|
||||||
|
|
||||||
var isLoading = false;
|
var isLoading = false;
|
||||||
|
var isScrollingToMessage = false; // Flag to prevent scroll conflicts
|
||||||
|
|
||||||
final listController = useMemoized(() => ListController(), []);
|
final listController = useMemoized(() => ListController(), []);
|
||||||
|
|
||||||
@@ -192,6 +195,59 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> pickAudioMedia() async {
|
||||||
|
final result = await FilePicker.platform.pickFiles(
|
||||||
|
type: FileType.audio,
|
||||||
|
allowMultiple: true,
|
||||||
|
allowCompression: false,
|
||||||
|
);
|
||||||
|
if (result == null || result.count == 0) return;
|
||||||
|
attachments.value = [
|
||||||
|
...attachments.value,
|
||||||
|
...result.files.map(
|
||||||
|
(e) => UniversalFile(data: e.xFile, type: UniversalFileType.audio),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> pickGeneralFile() async {
|
||||||
|
final result = await FilePicker.platform.pickFiles(
|
||||||
|
allowMultiple: true,
|
||||||
|
allowCompression: false,
|
||||||
|
);
|
||||||
|
if (result == null || result.count == 0) return;
|
||||||
|
attachments.value = [
|
||||||
|
...attachments.value,
|
||||||
|
...result.files.map(
|
||||||
|
(e) => UniversalFile(data: e.xFile, type: UniversalFileType.file),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
void linkAttachment() async {
|
||||||
|
final cloudFile = await showModalBottomSheet<SnCloudFile?>(
|
||||||
|
context: context,
|
||||||
|
useRootNavigator: true,
|
||||||
|
isScrollControlled: true,
|
||||||
|
builder: (context) => const ChatLinkAttachment(),
|
||||||
|
);
|
||||||
|
if (cloudFile == null) return;
|
||||||
|
|
||||||
|
attachments.value = [
|
||||||
|
...attachments.value,
|
||||||
|
UniversalFile(
|
||||||
|
data: cloudFile,
|
||||||
|
type: switch (cloudFile.mimeType?.split('/').firstOrNull) {
|
||||||
|
'image' => UniversalFileType.image,
|
||||||
|
'video' => UniversalFileType.video,
|
||||||
|
'audio' => UniversalFileType.audio,
|
||||||
|
_ => UniversalFileType.file,
|
||||||
|
},
|
||||||
|
isLink: true,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
void sendMessage() {
|
void sendMessage() {
|
||||||
if (messageController.text.trim().isNotEmpty ||
|
if (messageController.text.trim().isNotEmpty ||
|
||||||
attachments.value.isNotEmpty) {
|
attachments.value.isNotEmpty) {
|
||||||
@@ -332,6 +388,94 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
|
|
||||||
const messageKeyPrefix = 'message-';
|
const messageKeyPrefix = 'message-';
|
||||||
|
|
||||||
|
// Helper function for scroll animation
|
||||||
|
void performScrollAnimation({
|
||||||
|
required int index,
|
||||||
|
required ListController listController,
|
||||||
|
required ScrollController scrollController,
|
||||||
|
required String messageId,
|
||||||
|
required WidgetRef ref,
|
||||||
|
}) {
|
||||||
|
// Update flashing message first
|
||||||
|
ref
|
||||||
|
.read(flashingMessagesProvider.notifier)
|
||||||
|
.update((set) => set.union({messageId}));
|
||||||
|
|
||||||
|
// Use multiple post-frame callbacks to ensure stability
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
try {
|
||||||
|
listController.animateToItem(
|
||||||
|
index: index,
|
||||||
|
scrollController: scrollController,
|
||||||
|
alignment: 0.5,
|
||||||
|
duration:
|
||||||
|
(estimatedDistance) => Duration(
|
||||||
|
milliseconds:
|
||||||
|
(estimatedDistance * 0.5).clamp(200, 800).toInt(),
|
||||||
|
),
|
||||||
|
curve: (estimatedDistance) => Curves.easeOutCubic,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reset the scroll flag after animation completes
|
||||||
|
Future.delayed(const Duration(milliseconds: 800), () {
|
||||||
|
isScrollingToMessage = false;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// If animation fails, reset the flag
|
||||||
|
isScrollingToMessage = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Robust scroll-to-message function to prevent jumping back
|
||||||
|
void scrollToMessage({
|
||||||
|
required String messageId,
|
||||||
|
required List<LocalChatMessage> messageList,
|
||||||
|
required MessagesNotifier messagesNotifier,
|
||||||
|
required ListController listController,
|
||||||
|
required ScrollController scrollController,
|
||||||
|
required WidgetRef ref,
|
||||||
|
}) {
|
||||||
|
// Prevent concurrent scroll operations
|
||||||
|
if (isScrollingToMessage) return;
|
||||||
|
isScrollingToMessage = true;
|
||||||
|
|
||||||
|
final messageIndex = messageList.indexWhere((m) => m.id == messageId);
|
||||||
|
|
||||||
|
if (messageIndex == -1) {
|
||||||
|
// Message not in current list, need to load it first
|
||||||
|
messagesNotifier.jumpToMessage(messageId).then((index) {
|
||||||
|
if (index != -1) {
|
||||||
|
// Wait for UI to rebuild before animating
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
performScrollAnimation(
|
||||||
|
index: index,
|
||||||
|
listController: listController,
|
||||||
|
scrollController: scrollController,
|
||||||
|
messageId: messageId,
|
||||||
|
ref: ref,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
isScrollingToMessage = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Message is already in list, scroll directly with slight delay
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
performScrollAnimation(
|
||||||
|
index: messageIndex,
|
||||||
|
listController: listController,
|
||||||
|
scrollController: scrollController,
|
||||||
|
messageId: messageId,
|
||||||
|
ref: ref,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> uploadAttachment(int index) async {
|
Future<void> uploadAttachment(int index) async {
|
||||||
final attachment = attachments.value[index];
|
final attachment = attachments.value[index];
|
||||||
if (attachment.isOnCloud) return;
|
if (attachment.isOnCloud) return;
|
||||||
@@ -348,10 +492,6 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
if (config == null) return;
|
if (config == null) return;
|
||||||
|
|
||||||
final baseUrl = ref.watch(serverUrlProvider);
|
|
||||||
final token = await getToken(ref.watch(tokenProvider));
|
|
||||||
if (token == null) throw ArgumentError('Token is null');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Use 'chat-upload' as temporary key for progress
|
// Use 'chat-upload' as temporary key for progress
|
||||||
attachmentProgress.value = {
|
attachmentProgress.value = {
|
||||||
@@ -360,15 +500,10 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
};
|
};
|
||||||
|
|
||||||
final cloudFile =
|
final cloudFile =
|
||||||
await putFileToCloud(
|
await FileUploader.createCloudFile(
|
||||||
|
client: ref.read(apiClientProvider),
|
||||||
fileData: attachment,
|
fileData: attachment,
|
||||||
atk: token,
|
|
||||||
baseUrl: baseUrl,
|
|
||||||
poolId: config.poolId,
|
poolId: config.poolId,
|
||||||
filename: attachment.data.name ?? 'Chat media',
|
|
||||||
mimetype:
|
|
||||||
attachment.data.mimeType ??
|
|
||||||
ComposeLogic.getMimeTypeFromFileType(attachment.type),
|
|
||||||
mode:
|
mode:
|
||||||
attachment.type == UniversalFileType.file
|
attachment.type == UniversalFileType.file
|
||||||
? FileUploadMode.generic
|
? FileUploadMode.generic
|
||||||
@@ -396,22 +531,25 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget chatMessageListWidget(List<LocalChatMessage> messageList) =>
|
Widget chatMessageListWidget(
|
||||||
SuperListView.builder(
|
List<LocalChatMessage> messageList,
|
||||||
|
) => SuperListView.builder(
|
||||||
listController: listController,
|
listController: listController,
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
top: 16,
|
top: 16,
|
||||||
bottom: 80 + MediaQuery.of(context).padding.bottom,
|
bottom: MediaQuery.of(context).padding.bottom + 16,
|
||||||
),
|
),
|
||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
reverse: true, // Show newest messages at the bottom
|
reverse: true, // Show newest messages at the bottom
|
||||||
itemCount: messageList.length,
|
itemCount: messageList.length,
|
||||||
findChildIndexCallback: (key) {
|
findChildIndexCallback: (key) {
|
||||||
final valueKey = key as ValueKey;
|
if (key is! ValueKey<String>) return null;
|
||||||
final messageId = (valueKey.value as String).substring(
|
final messageId = key.value.substring(messageKeyPrefix.length);
|
||||||
messageKeyPrefix.length,
|
final index = messageList.indexWhere(
|
||||||
|
(m) => (m.nonce ?? m.id) == messageId,
|
||||||
);
|
);
|
||||||
return messageList.indexWhere((m) => m.id == messageId);
|
// Return null for invalid indices to let SuperListView handle it properly
|
||||||
|
return index >= 0 ? index : null;
|
||||||
},
|
},
|
||||||
extentEstimation: (_, _) => 40,
|
extentEstimation: (_, _) => 40,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
@@ -427,13 +565,14 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
.abs() >
|
.abs() >
|
||||||
3;
|
3;
|
||||||
|
|
||||||
final key = ValueKey('$messageKeyPrefix${message.id}');
|
// Use a stable animation key that doesn't change during message lifecycle
|
||||||
|
final key = Key('$messageKeyPrefix${message.nonce ?? message.id}');
|
||||||
|
|
||||||
return chatIdentity.when(
|
final messageWidget = chatIdentity.when(
|
||||||
skipError: true,
|
skipError: true,
|
||||||
data:
|
data:
|
||||||
(identity) => MessageItem(
|
(identity) => MessageItem(
|
||||||
key: key,
|
key: settings.disableAnimation ? key : null,
|
||||||
message: message,
|
message: message,
|
||||||
isCurrentUser: identity?.id == message.senderId,
|
isCurrentUser: identity?.id == message.senderId,
|
||||||
onAction: (action) {
|
onAction: (action) {
|
||||||
@@ -452,54 +591,25 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
messageForwardingTo.value = message.toRemoteMessage();
|
messageForwardingTo.value = message.toRemoteMessage();
|
||||||
case MessageItemAction.reply:
|
case MessageItemAction.reply:
|
||||||
messageReplyingTo.value = message.toRemoteMessage();
|
messageReplyingTo.value = message.toRemoteMessage();
|
||||||
|
case MessageItemAction.resend:
|
||||||
|
messagesNotifier.retryMessage(message.id);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onJump: (messageId) {
|
onJump: (messageId) {
|
||||||
final messageIndex = messageList.indexWhere(
|
scrollToMessage(
|
||||||
(m) => m.id == messageId,
|
messageId: messageId,
|
||||||
);
|
messageList: messageList,
|
||||||
if (messageIndex == -1) {
|
messagesNotifier: messagesNotifier,
|
||||||
messagesNotifier.jumpToMessage(messageId).then((index) {
|
listController: listController,
|
||||||
if (index != -1) {
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
listController.animateToItem(
|
|
||||||
index: index,
|
|
||||||
scrollController: scrollController,
|
scrollController: scrollController,
|
||||||
alignment: 0.5,
|
ref: ref,
|
||||||
duration:
|
|
||||||
(estimatedDistance) =>
|
|
||||||
Duration(milliseconds: 250),
|
|
||||||
curve: (estimatedDistance) => Curves.easeInOut,
|
|
||||||
);
|
);
|
||||||
});
|
|
||||||
ref
|
|
||||||
.read(flashingMessagesProvider.notifier)
|
|
||||||
.update((set) => set.union({messageId}));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
listController.animateToItem(
|
|
||||||
index: messageIndex,
|
|
||||||
scrollController: scrollController,
|
|
||||||
alignment: 0.5,
|
|
||||||
duration:
|
|
||||||
(estimatedDistance) =>
|
|
||||||
Duration(milliseconds: 250),
|
|
||||||
curve: (estimatedDistance) => Curves.easeInOut,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
ref
|
|
||||||
.read(flashingMessagesProvider.notifier)
|
|
||||||
.update((set) => set.union({messageId}));
|
|
||||||
},
|
},
|
||||||
progress: attachmentProgress.value[message.id],
|
progress: attachmentProgress.value[message.id],
|
||||||
showAvatar: isLastInGroup,
|
showAvatar: isLastInGroup,
|
||||||
),
|
),
|
||||||
loading:
|
loading:
|
||||||
() => MessageItem(
|
() => MessageItem(
|
||||||
key: key,
|
|
||||||
message: message,
|
message: message,
|
||||||
isCurrentUser: false,
|
isCurrentUser: false,
|
||||||
onAction: null,
|
onAction: null,
|
||||||
@@ -507,7 +617,28 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
showAvatar: false,
|
showAvatar: false,
|
||||||
onJump: (_) {},
|
onJump: (_) {},
|
||||||
),
|
),
|
||||||
error: (_, _) => SizedBox.shrink(key: key),
|
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,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -539,46 +670,40 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
'chatDetail',
|
'chatDetail',
|
||||||
pathParameters: {'id': id},
|
pathParameters: {'id': id},
|
||||||
);
|
);
|
||||||
if (result is String && messages.valueOrNull != null) {
|
if (result is SearchMessagesResult &&
|
||||||
// Jump to the message that was selected in search
|
messages.valueOrNull != null) {
|
||||||
final messageList = messages.valueOrNull!;
|
final messageId = result.messageId;
|
||||||
final messageIndex = messageList.indexWhere(
|
|
||||||
(m) => m.id == result,
|
// Jump to the message and trigger flash effect
|
||||||
);
|
messagesNotifier.jumpToMessage(messageId).then((index) {
|
||||||
if (messageIndex == -1) {
|
if (index != -1 && context.mounted) {
|
||||||
messagesNotifier.jumpToMessage(result).then((index) {
|
// Update flashing message
|
||||||
if (index != -1) {
|
ref
|
||||||
|
.read(flashingMessagesProvider.notifier)
|
||||||
|
.update((set) => set.union({messageId}));
|
||||||
|
|
||||||
|
// Scroll to the message with animation
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
try {
|
||||||
listController.animateToItem(
|
listController.animateToItem(
|
||||||
index: index,
|
index: index,
|
||||||
scrollController: scrollController,
|
scrollController: scrollController,
|
||||||
alignment: 0.5,
|
alignment: 0.5,
|
||||||
duration:
|
duration:
|
||||||
(estimatedDistance) =>
|
(estimatedDistance) => Duration(
|
||||||
Duration(milliseconds: 250),
|
milliseconds:
|
||||||
curve: (estimatedDistance) => Curves.easeInOut,
|
(estimatedDistance * 0.5)
|
||||||
|
.clamp(200, 800)
|
||||||
|
.toInt(),
|
||||||
|
),
|
||||||
|
curve: (estimatedDistance) => Curves.easeOutCubic,
|
||||||
);
|
);
|
||||||
});
|
} catch (e) {
|
||||||
ref
|
// If animation fails, just update flashing state
|
||||||
.read(flashingMessagesProvider.notifier)
|
|
||||||
.update((set) => set.union({result}));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
listController.animateToItem(
|
|
||||||
index: messageIndex,
|
|
||||||
scrollController: scrollController,
|
|
||||||
alignment: 0.5,
|
|
||||||
duration:
|
|
||||||
(estimatedDistance) => Duration(milliseconds: 250),
|
|
||||||
curve: (estimatedDistance) => Curves.easeInOut,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
ref
|
|
||||||
.read(flashingMessagesProvider.notifier)
|
|
||||||
.update((set) => set.union({result}));
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -587,28 +712,51 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
body: Stack(
|
body: Stack(
|
||||||
children: [
|
children: [
|
||||||
// Messages
|
// Messages and Input in Column
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
switchInCurve: Curves.easeOutCubic,
|
||||||
|
switchOutCurve: Curves.easeInCubic,
|
||||||
|
transitionBuilder: (
|
||||||
|
Widget child,
|
||||||
|
Animation<double> animation,
|
||||||
|
) {
|
||||||
|
return SlideTransition(
|
||||||
|
position: Tween<Offset>(
|
||||||
|
begin: const Offset(0, 0.05),
|
||||||
|
end: Offset.zero,
|
||||||
|
).animate(animation),
|
||||||
|
child: FadeTransition(opacity: animation, child: child),
|
||||||
|
);
|
||||||
|
},
|
||||||
child: messages.when(
|
child: messages.when(
|
||||||
data:
|
data:
|
||||||
(messageList) =>
|
(messageList) =>
|
||||||
messageList.isEmpty
|
messageList.isEmpty
|
||||||
? Center(child: Text('No messages yet'.tr()))
|
? Center(
|
||||||
|
key: const ValueKey('empty-messages'),
|
||||||
|
child: Text('No messages yet'.tr()),
|
||||||
|
)
|
||||||
: chatMessageListWidget(messageList),
|
: chatMessageListWidget(messageList),
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
loading:
|
||||||
|
() => const Center(
|
||||||
|
key: ValueKey('loading-messages'),
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
error:
|
error:
|
||||||
(error, _) => ResponseErrorWidget(
|
(error, _) => ResponseErrorWidget(
|
||||||
|
key: const ValueKey('error-messages'),
|
||||||
error: error,
|
error: error,
|
||||||
onRetry: () => messagesNotifier.loadInitial(),
|
onRetry: () => messagesNotifier.loadInitial(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Input
|
),
|
||||||
Positioned(
|
chatRoom.when(
|
||||||
bottom: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
child: chatRoom.when(
|
|
||||||
data:
|
data:
|
||||||
(room) => Column(
|
(room) => Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -636,11 +784,14 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
pickVideoMedia();
|
pickVideoMedia();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onPickAudio: pickAudioMedia,
|
||||||
|
onPickGeneralFile: pickGeneralFile,
|
||||||
|
onLinkAttachment: linkAttachment,
|
||||||
attachments: attachments.value,
|
attachments: attachments.value,
|
||||||
onUploadAttachment: uploadAttachment,
|
onUploadAttachment: uploadAttachment,
|
||||||
onDeleteAttachment: (index) async {
|
onDeleteAttachment: (index) async {
|
||||||
final attachment = attachments.value[index];
|
final attachment = attachments.value[index];
|
||||||
if (attachment.isOnCloud) {
|
if (attachment.isOnCloud && !attachment.isLink) {
|
||||||
final client = ref.watch(apiClientProvider);
|
final client = ref.watch(apiClientProvider);
|
||||||
await client.delete(
|
await client.delete(
|
||||||
'/drive/files/${attachment.data.id}',
|
'/drive/files/${attachment.data.id}',
|
||||||
@@ -670,6 +821,8 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
error: (_, _) => const SizedBox.shrink(),
|
error: (_, _) => const SizedBox.shrink(),
|
||||||
loading: () => const SizedBox.shrink(),
|
loading: () => const SizedBox.shrink(),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
left: 0,
|
left: 0,
|
||||||
|
|||||||
@@ -410,7 +410,7 @@ class ChatDetailScreen extends HookConsumerWidget {
|
|||||||
if (result is SearchMessagesResult) {
|
if (result is SearchMessagesResult) {
|
||||||
// Navigate back to room screen with message to jump to
|
// Navigate back to room screen with message to jump to
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
context.pop(result.messageId);
|
context.pop(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,10 +4,13 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:go_router/go_router.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/pods/chat/messages_notifier.dart';
|
import 'package:island/pods/chat/messages_notifier.dart';
|
||||||
|
import 'package:island/pods/chat/chat_rooms.dart';
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
import 'package:island/widgets/chat/message_list_tile.dart';
|
import 'package:island/widgets/chat/message_list_tile.dart';
|
||||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||||
import 'package:super_sliver_list/super_sliver_list.dart';
|
import 'package:super_sliver_list/super_sliver_list.dart';
|
||||||
|
import 'package:island/services/responsive.dart';
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
// Class to represent the result when popping from search messages
|
// Class to represent the result when popping from search messages
|
||||||
class SearchMessagesResult {
|
class SearchMessagesResult {
|
||||||
@@ -15,6 +18,93 @@ class SearchMessagesResult {
|
|||||||
const SearchMessagesResult(this.messageId);
|
const SearchMessagesResult(this.messageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Search states for better UX
|
||||||
|
enum SearchState { idle, searching, results, noResults, error }
|
||||||
|
|
||||||
|
class _SearchFilters extends StatelessWidget {
|
||||||
|
final ValueNotifier<bool> withLinks;
|
||||||
|
final ValueNotifier<bool> withAttachments;
|
||||||
|
final void Function(String) performSearch;
|
||||||
|
final TextEditingController searchController;
|
||||||
|
final bool isLarge;
|
||||||
|
|
||||||
|
const _SearchFilters({
|
||||||
|
required this.withLinks,
|
||||||
|
required this.withAttachments,
|
||||||
|
required this.performSearch,
|
||||||
|
required this.searchController,
|
||||||
|
required this.isLarge,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (isLarge) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
Symbols.link,
|
||||||
|
color:
|
||||||
|
withLinks.value
|
||||||
|
? Theme.of(context).colorScheme.primary
|
||||||
|
: Theme.of(context).iconTheme.color,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
withLinks.value = !withLinks.value;
|
||||||
|
performSearch(searchController.text);
|
||||||
|
},
|
||||||
|
tooltip: 'searchLinks'.tr(),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
Symbols.file_copy,
|
||||||
|
color:
|
||||||
|
withAttachments.value
|
||||||
|
? Theme.of(context).colorScheme.primary
|
||||||
|
: Theme.of(context).iconTheme.color,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
withAttachments.value = !withAttachments.value;
|
||||||
|
performSearch(searchController.text);
|
||||||
|
},
|
||||||
|
tooltip: 'searchAttachments'.tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
FilterChip(
|
||||||
|
avatar: const Icon(Symbols.link, size: 16),
|
||||||
|
label: const Text('searchLinks').tr(),
|
||||||
|
selected: withLinks.value,
|
||||||
|
onSelected: (bool? value) {
|
||||||
|
withLinks.value = value!;
|
||||||
|
performSearch(searchController.text);
|
||||||
|
},
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
FilterChip(
|
||||||
|
avatar: const Icon(Symbols.file_copy, size: 16),
|
||||||
|
label: const Text('searchAttachments').tr(),
|
||||||
|
selected: withAttachments.value,
|
||||||
|
onSelected: (bool? value) {
|
||||||
|
withAttachments.value = value!;
|
||||||
|
performSearch(searchController.text);
|
||||||
|
},
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class SearchMessagesScreen extends HookConsumerWidget {
|
class SearchMessagesScreen extends HookConsumerWidget {
|
||||||
final String roomId;
|
final String roomId;
|
||||||
|
|
||||||
@@ -25,99 +115,329 @@ class SearchMessagesScreen extends HookConsumerWidget {
|
|||||||
final searchController = useTextEditingController();
|
final searchController = useTextEditingController();
|
||||||
final withLinks = useState(false);
|
final withLinks = useState(false);
|
||||||
final withAttachments = useState(false);
|
final withAttachments = useState(false);
|
||||||
|
final searchState = useState(SearchState.idle);
|
||||||
|
final searchResultCount = useState<int?>(null);
|
||||||
|
final searchResults = useState<AsyncValue<List<dynamic>>>(
|
||||||
|
const AsyncValue.data([]),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Debounce timer for search optimization
|
||||||
|
final debounceTimer = useRef<Timer?>(null);
|
||||||
|
|
||||||
final messagesNotifier = ref.read(
|
final messagesNotifier = ref.read(
|
||||||
messagesNotifierProvider(roomId).notifier,
|
messagesNotifierProvider(roomId).notifier,
|
||||||
);
|
);
|
||||||
final messages = ref.watch(messagesNotifierProvider(roomId));
|
|
||||||
|
// Optimized search function with debouncing
|
||||||
|
void performSearch(String query) async {
|
||||||
|
final trimmedQuery = query.trim();
|
||||||
|
final hasFilters = withLinks.value || withAttachments.value;
|
||||||
|
|
||||||
|
if (trimmedQuery.isEmpty && !hasFilters) {
|
||||||
|
searchState.value = SearchState.idle;
|
||||||
|
searchResultCount.value = null;
|
||||||
|
searchResults.value = const AsyncValue.data([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
searchState.value = SearchState.searching;
|
||||||
|
searchResults.value = const AsyncValue.loading();
|
||||||
|
|
||||||
|
// Cancel previous search if still active
|
||||||
|
debounceTimer.value?.cancel();
|
||||||
|
|
||||||
|
// Debounce search to avoid excessive API calls
|
||||||
|
debounceTimer.value = Timer(const Duration(milliseconds: 300), () async {
|
||||||
|
try {
|
||||||
|
final results = await messagesNotifier.getSearchResults(
|
||||||
|
query.trim(),
|
||||||
|
withLinks: withLinks.value,
|
||||||
|
withAttachments: withAttachments.value,
|
||||||
|
);
|
||||||
|
searchResults.value = AsyncValue.data(results);
|
||||||
|
searchState.value =
|
||||||
|
results.isEmpty ? SearchState.noResults : SearchState.results;
|
||||||
|
searchResultCount.value = results.length;
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
searchResults.value = AsyncValue.error(error, stackTrace);
|
||||||
|
searchState.value = SearchState.error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search state is now managed locally in performSearch
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
// Clear search when screen is disposed
|
// Clear search when screen is disposed
|
||||||
return () {
|
return () {
|
||||||
messagesNotifier.clearSearch();
|
debounceTimer.value?.cancel();
|
||||||
|
// Note: Don't access ref here as widget may be disposed
|
||||||
|
// Flashing messages will be cleared by the next screen or jump operation
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Clear flashing messages when screen initializes (safer than in dispose)
|
||||||
|
useEffect(() {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
// Clear flashing messages when entering search screen
|
||||||
|
ref.read(flashingMessagesProvider.notifier).state = {};
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
final isLarge = isWideScreen(context);
|
||||||
|
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(title: const Text('searchMessages').tr()),
|
appBar: AppBar(
|
||||||
|
title: const Text('searchMessages').tr(),
|
||||||
|
bottom:
|
||||||
|
searchState.value == SearchState.searching
|
||||||
|
? const PreferredSize(
|
||||||
|
preferredSize: Size.fromHeight(2),
|
||||||
|
child: LinearProgressIndicator(),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
Column(
|
// Search input section
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
child: Material(
|
||||||
|
elevation: 2,
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||||
|
borderRadius: BorderRadius.circular(32),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8),
|
||||||
|
child:
|
||||||
|
isLarge
|
||||||
|
? Row(
|
||||||
children: [
|
children: [
|
||||||
TextField(
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
controller: searchController,
|
controller: searchController,
|
||||||
|
autofocus: true,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'searchMessagesHint'.tr(),
|
hintText: 'searchMessagesHint'.tr(),
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
isDense: true,
|
isDense: true,
|
||||||
contentPadding: EdgeInsets.only(
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
left: 16,
|
horizontal: 12,
|
||||||
right: 16,
|
vertical: 12,
|
||||||
top: 12,
|
|
||||||
bottom: 16,
|
|
||||||
),
|
),
|
||||||
suffix: IconButton(
|
suffixIcon: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (searchResultCount.value != null &&
|
||||||
|
searchState.value ==
|
||||||
|
SearchState.results)
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.only(
|
||||||
|
right: 8,
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8,
|
||||||
|
vertical: 2,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.primary
|
||||||
|
.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'${searchResultCount.value}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color:
|
||||||
|
Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (searchController.text.isNotEmpty)
|
||||||
|
IconButton(
|
||||||
iconSize: 18,
|
iconSize: 18,
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
icon: const Icon(Icons.clear),
|
icon: const Icon(Icons.clear),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
searchController.clear();
|
searchController.clear();
|
||||||
messagesNotifier.clearSearch();
|
performSearch('');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
onChanged: (query) {
|
|
||||||
messagesNotifier.searchMessages(
|
|
||||||
query,
|
|
||||||
withLinks: withLinks.value,
|
|
||||||
withAttachments: withAttachments.value,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
Row(
|
onChanged: performSearch,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: _SearchFilters(
|
||||||
|
withLinks: withLinks,
|
||||||
|
withAttachments: withAttachments,
|
||||||
|
performSearch: performSearch,
|
||||||
|
searchController: searchController,
|
||||||
|
isLarge: isLarge,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: Column(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
TextField(
|
||||||
child: CheckboxListTile(
|
controller: searchController,
|
||||||
secondary: const Icon(Symbols.link),
|
autofocus: true,
|
||||||
title: const Text('searchLinks').tr(),
|
decoration: InputDecoration(
|
||||||
value: withLinks.value,
|
hintText: 'searchMessagesHint'.tr(),
|
||||||
onChanged: (bool? value) {
|
border: InputBorder.none,
|
||||||
withLinks.value = value!;
|
isDense: true,
|
||||||
messagesNotifier.searchMessages(
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
searchController.text,
|
horizontal: 12,
|
||||||
withLinks: withLinks.value,
|
vertical: 12,
|
||||||
withAttachments: withAttachments.value,
|
),
|
||||||
);
|
suffixIcon: Row(
|
||||||
},
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (searchResultCount.value != null &&
|
||||||
|
searchState.value ==
|
||||||
|
SearchState.results)
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.only(right: 8),
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8,
|
||||||
|
vertical: 2,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.primary
|
||||||
|
.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
12,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
child: Text(
|
||||||
child: CheckboxListTile(
|
'${searchResultCount.value}',
|
||||||
secondary: const Icon(Symbols.file_copy),
|
style: TextStyle(
|
||||||
title: const Text('searchAttachments').tr(),
|
fontSize: 12,
|
||||||
value: withAttachments.value,
|
fontWeight: FontWeight.bold,
|
||||||
onChanged: (bool? value) {
|
color:
|
||||||
withAttachments.value = value!;
|
Theme.of(
|
||||||
messagesNotifier.searchMessages(
|
context,
|
||||||
searchController.text,
|
).colorScheme.primary,
|
||||||
withLinks: withLinks.value,
|
),
|
||||||
withAttachments: withAttachments.value,
|
),
|
||||||
);
|
),
|
||||||
|
if (searchController.text.isNotEmpty)
|
||||||
|
IconButton(
|
||||||
|
iconSize: 18,
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
icon: const Icon(Icons.clear),
|
||||||
|
onPressed: () {
|
||||||
|
searchController.clear();
|
||||||
|
performSearch('');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onChanged: performSearch,
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
left: 8,
|
||||||
|
right: 8,
|
||||||
|
top: 8,
|
||||||
|
bottom: 8,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
_SearchFilters(
|
||||||
|
withLinks: withLinks,
|
||||||
|
withAttachments: withAttachments,
|
||||||
|
performSearch: performSearch,
|
||||||
|
searchController: searchController,
|
||||||
|
isLarge: false,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Search results section
|
||||||
|
Expanded(
|
||||||
|
child: searchResults.value.when(
|
||||||
|
data: (messageList) {
|
||||||
|
switch (searchState.value) {
|
||||||
|
case SearchState.idle:
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.search,
|
||||||
|
size: 64,
|
||||||
|
color: Theme.of(context).disabledColor,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'searchMessagesHint'.tr(),
|
||||||
|
style: Theme.of(
|
||||||
|
context,
|
||||||
|
).textTheme.bodyLarge?.copyWith(
|
||||||
|
color: Theme.of(context).disabledColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const Divider(height: 1),
|
);
|
||||||
Expanded(
|
|
||||||
child: messages.when(
|
case SearchState.noResults:
|
||||||
data:
|
return Center(
|
||||||
(messageList) =>
|
child: Column(
|
||||||
messageList.isEmpty
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
? Center(child: Text('noMessagesFound'.tr()))
|
children: [
|
||||||
: SuperListView.builder(
|
Icon(
|
||||||
|
Icons.search_off,
|
||||||
|
size: 64,
|
||||||
|
color: Theme.of(context).disabledColor,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'noMessagesFound'.tr(),
|
||||||
|
style: Theme.of(
|
||||||
|
context,
|
||||||
|
).textTheme.bodyLarge?.copyWith(
|
||||||
|
color: Theme.of(context).disabledColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'tryDifferentKeywords'.tr(),
|
||||||
|
style: Theme.of(
|
||||||
|
context,
|
||||||
|
).textTheme.bodySmall?.copyWith(
|
||||||
|
color: Theme.of(context).disabledColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
case SearchState.results:
|
||||||
|
return SuperListView.builder(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
reverse: false, // Show newest messages at the top
|
reverse: false, // Show newest messages at the top
|
||||||
itemCount: messageList.length,
|
itemCount: messageList.length,
|
||||||
@@ -131,12 +451,54 @@ class SearchMessagesScreen extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
loading: () {
|
||||||
|
if (searchState.value == SearchState.searching) {
|
||||||
|
return const Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
CircularProgressIndicator(),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
Text('Searching...'),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
);
|
||||||
error:
|
}
|
||||||
(error, _) => Center(
|
return const Center(child: CircularProgressIndicator());
|
||||||
child: Text('errorGeneric'.tr(args: [error.toString()])),
|
},
|
||||||
|
error: (error, _) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.error_outline,
|
||||||
|
size: 64,
|
||||||
|
color: Theme.of(context).colorScheme.error,
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'searchError'.tr(),
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.error,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: () => performSearch(searchController.text),
|
||||||
|
icon: const Icon(Icons.refresh),
|
||||||
|
label: const Text('retry').tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ import 'package:gap/gap.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/post.dart';
|
import 'package:island/models/post.dart';
|
||||||
import 'package:island/models/publisher.dart';
|
import 'package:island/models/publisher.dart';
|
||||||
|
import 'package:island/models/heatmap.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
import 'package:island/screens/creators/publishers.dart';
|
import 'package:island/screens/creators/publishers_form.dart';
|
||||||
import 'package:island/services/responsive.dart';
|
import 'package:island/services/responsive.dart';
|
||||||
import 'package:island/utils/text.dart';
|
import 'package:island/utils/text.dart';
|
||||||
import 'package:island/widgets/account/account_picker.dart';
|
import 'package:island/widgets/account/account_picker.dart';
|
||||||
@@ -18,6 +19,7 @@ import 'package:island/widgets/app_scaffold.dart';
|
|||||||
import 'package:island/widgets/content/cloud_files.dart';
|
import 'package:island/widgets/content/cloud_files.dart';
|
||||||
import 'package:island/widgets/content/sheet.dart';
|
import 'package:island/widgets/content/sheet.dart';
|
||||||
import 'package:island/widgets/response.dart';
|
import 'package:island/widgets/response.dart';
|
||||||
|
import 'package:island/widgets/activity_heatmap.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
|
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
|
||||||
@@ -33,6 +35,14 @@ Future<SnPublisherStats?> publisherStats(Ref ref, String? uname) async {
|
|||||||
return SnPublisherStats.fromJson(resp.data);
|
return SnPublisherStats.fromJson(resp.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
Future<SnHeatmap?> publisherHeatmap(Ref ref, String? uname) async {
|
||||||
|
if (uname == null) return null;
|
||||||
|
final apiClient = ref.watch(apiClientProvider);
|
||||||
|
final resp = await apiClient.get('/sphere/publishers/$uname/heatmap');
|
||||||
|
return SnHeatmap.fromJson(resp.data);
|
||||||
|
}
|
||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
Future<SnPublisherMember?> publisherIdentity(Ref ref, String uname) async {
|
Future<SnPublisherMember?> publisherIdentity(Ref ref, String uname) async {
|
||||||
try {
|
try {
|
||||||
@@ -102,42 +112,168 @@ class PublisherMemberListNotifier extends _$PublisherMemberListNotifier
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class CreatorHubShellScreen extends StatelessWidget {
|
class PublisherSelector extends StatelessWidget {
|
||||||
final Widget child;
|
final SnPublisher? currentPublisher;
|
||||||
const CreatorHubShellScreen({super.key, required this.child});
|
final List<DropdownMenuItem<SnPublisher>> publishersMenu;
|
||||||
|
final ValueChanged<SnPublisher?>? onChanged;
|
||||||
|
final bool isReadOnly;
|
||||||
|
|
||||||
|
const PublisherSelector({
|
||||||
|
super.key,
|
||||||
|
required this.currentPublisher,
|
||||||
|
required this.publishersMenu,
|
||||||
|
this.onChanged,
|
||||||
|
this.isReadOnly = false,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isWide = isWideScreen(context);
|
if (isReadOnly || currentPublisher == null) {
|
||||||
if (isWide) {
|
return ProfilePictureWidget(
|
||||||
return AppBackground(
|
radius: 16,
|
||||||
isRoot: true,
|
fileId: currentPublisher?.picture?.id,
|
||||||
child: Row(
|
).center().padding(right: 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
return DropdownButtonHideUnderline(
|
||||||
|
child: DropdownButton2<SnPublisher>(
|
||||||
|
value: currentPublisher,
|
||||||
|
hint: CircleAvatar(
|
||||||
|
radius: 16,
|
||||||
|
child: Icon(
|
||||||
|
Symbols.person,
|
||||||
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.onSecondaryContainer.withOpacity(0.9),
|
||||||
|
fill: 1,
|
||||||
|
),
|
||||||
|
).center().padding(right: 8),
|
||||||
|
items: publishersMenu,
|
||||||
|
onChanged: onChanged,
|
||||||
|
selectedItemBuilder: (context) {
|
||||||
|
return publishersMenu
|
||||||
|
.map(
|
||||||
|
(e) => ProfilePictureWidget(
|
||||||
|
radius: 16,
|
||||||
|
fileId: e.value?.picture?.id,
|
||||||
|
).center().padding(right: 8),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
},
|
||||||
|
buttonStyleData: ButtonStyleData(
|
||||||
|
height: 40,
|
||||||
|
padding: const EdgeInsets.only(left: 14, right: 8),
|
||||||
|
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20)),
|
||||||
|
),
|
||||||
|
dropdownStyleData: DropdownStyleData(
|
||||||
|
width: 320,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||||
|
decoration: BoxDecoration(borderRadius: BorderRadius.circular(4)),
|
||||||
|
),
|
||||||
|
menuItemStyleData: const MenuItemStyleData(
|
||||||
|
height: 64,
|
||||||
|
padding: EdgeInsets.only(left: 14, right: 14),
|
||||||
|
),
|
||||||
|
iconStyleData: IconStyleData(
|
||||||
|
icon: Icon(Icons.arrow_drop_down),
|
||||||
|
iconSize: 19,
|
||||||
|
iconEnabledColor: Theme.of(context).appBarTheme.foregroundColor!,
|
||||||
|
iconDisabledColor: Theme.of(context).appBarTheme.foregroundColor!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PublisherUnselectedWidget extends HookConsumerWidget {
|
||||||
|
final ValueChanged<SnPublisher> onPublisherSelected;
|
||||||
|
|
||||||
|
const _PublisherUnselectedWidget({required this.onPublisherSelected});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final publishers = ref.watch(publishersManagedProvider);
|
||||||
|
final publisherInvites = ref.watch(publisherInvitesProvider);
|
||||||
|
|
||||||
|
final hasPublishers = publishers.value?.isNotEmpty ?? false;
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Flexible(flex: 2, child: const CreatorHubScreen(isAside: true)),
|
if (!hasPublishers) ...[
|
||||||
const VerticalDivider(width: 1),
|
const Icon(
|
||||||
Flexible(flex: 3, child: child),
|
Symbols.info,
|
||||||
|
fill: 1,
|
||||||
|
size: 32,
|
||||||
|
).padding(bottom: 6, top: 24),
|
||||||
|
Text(
|
||||||
|
'creatorHubUnselectedHint',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
|
).tr(),
|
||||||
|
const Gap(24),
|
||||||
|
],
|
||||||
|
if (hasPublishers)
|
||||||
|
...(publishers.value?.map(
|
||||||
|
(publisher) => ListTile(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
),
|
||||||
|
leading: ProfilePictureWidget(file: publisher.picture),
|
||||||
|
title: Text(publisher.nick),
|
||||||
|
subtitle: Text('@${publisher.name}'),
|
||||||
|
onTap: () => onPublisherSelected(publisher),
|
||||||
|
),
|
||||||
|
) ??
|
||||||
|
[]),
|
||||||
|
const Divider(height: 1),
|
||||||
|
ListTile(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
),
|
||||||
|
leading: const CircleAvatar(child: Icon(Symbols.mail)),
|
||||||
|
title: Text('publisherCollabInvitation').tr(),
|
||||||
|
subtitle: Text(
|
||||||
|
'publisherCollabInvitationCount',
|
||||||
|
).plural(publisherInvites.value?.length ?? 0),
|
||||||
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
|
onTap: () {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
builder: (_) => const _PublisherInviteSheet(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
),
|
||||||
|
leading: const CircleAvatar(child: Icon(Symbols.add)),
|
||||||
|
title: Text('createPublisher').tr(),
|
||||||
|
subtitle: Text('createPublisherHint').tr(),
|
||||||
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
|
onTap: () {
|
||||||
|
context.pushNamed('creatorNew').then((value) {
|
||||||
|
if (value != null) {
|
||||||
|
ref.invalidate(publishersManagedProvider);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return AppBackground(isRoot: true, child: child);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class CreatorHubScreen extends HookConsumerWidget {
|
class CreatorHubScreen extends HookConsumerWidget {
|
||||||
final bool isAside;
|
const CreatorHubScreen({super.key});
|
||||||
const CreatorHubScreen({super.key, this.isAside = false});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final isWide = isWideScreen(context);
|
|
||||||
if (isWide && !isAside) {
|
|
||||||
return Container(color: Theme.of(context).colorScheme.surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
final publishers = ref.watch(publishersManagedProvider);
|
final publishers = ref.watch(publishersManagedProvider);
|
||||||
final publisherInvites = ref.watch(publisherInvitesProvider);
|
|
||||||
final currentPublisher = useState<SnPublisher?>(
|
final currentPublisher = useState<SnPublisher?>(
|
||||||
publishers.value?.firstOrNull,
|
publishers.value?.firstOrNull,
|
||||||
);
|
);
|
||||||
@@ -203,231 +339,102 @@ class CreatorHubScreen extends HookConsumerWidget {
|
|||||||
publisherStatsProvider(currentPublisher.value?.name),
|
publisherStatsProvider(currentPublisher.value?.name),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final publisherHeatmap = ref.watch(
|
||||||
|
publisherHeatmapProvider(currentPublisher.value?.name),
|
||||||
|
);
|
||||||
|
|
||||||
final publisherFeatures = ref.watch(
|
final publisherFeatures = ref.watch(
|
||||||
publisherFeaturesProvider(currentPublisher.value?.name),
|
publisherFeaturesProvider(currentPublisher.value?.name),
|
||||||
);
|
);
|
||||||
|
|
||||||
return AppScaffold(
|
Widget buildNavigationWidget(bool isWide) {
|
||||||
appBar: AppBar(
|
final leftItems = [
|
||||||
leading: !isWide ? const PageBackButton() : null,
|
|
||||||
title: Text('creatorHub').tr(),
|
|
||||||
actions: [
|
|
||||||
DropdownButtonHideUnderline(
|
|
||||||
child: DropdownButton2<SnPublisher>(
|
|
||||||
alignment: Alignment.centerRight,
|
|
||||||
value: currentPublisher.value,
|
|
||||||
hint: CircleAvatar(
|
|
||||||
radius: 16,
|
|
||||||
child: Icon(
|
|
||||||
Symbols.person,
|
|
||||||
color: Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.onSecondaryContainer.withOpacity(0.9),
|
|
||||||
fill: 1,
|
|
||||||
),
|
|
||||||
).center().padding(right: 8),
|
|
||||||
items: [...publishersMenu],
|
|
||||||
onChanged: (value) {
|
|
||||||
currentPublisher.value = value;
|
|
||||||
},
|
|
||||||
selectedItemBuilder: (context) {
|
|
||||||
return [
|
|
||||||
...publishersMenu.map(
|
|
||||||
(e) => ProfilePictureWidget(
|
|
||||||
radius: 16,
|
|
||||||
fileId: e.value?.picture?.id,
|
|
||||||
).center().padding(right: 8),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
},
|
|
||||||
buttonStyleData: ButtonStyleData(
|
|
||||||
height: 40,
|
|
||||||
padding: const EdgeInsets.only(left: 14, right: 8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
dropdownStyleData: DropdownStyleData(
|
|
||||||
width: 320,
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(4),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
menuItemStyleData: const MenuItemStyleData(
|
|
||||||
height: 64,
|
|
||||||
padding: EdgeInsets.only(left: 14, right: 14),
|
|
||||||
),
|
|
||||||
iconStyleData: IconStyleData(
|
|
||||||
icon: Icon(Icons.arrow_drop_down),
|
|
||||||
iconSize: 19,
|
|
||||||
iconEnabledColor:
|
|
||||||
Theme.of(context).appBarTheme.foregroundColor!,
|
|
||||||
iconDisabledColor:
|
|
||||||
Theme.of(context).appBarTheme.foregroundColor!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Gap(8),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
body: publisherStats.when(
|
|
||||||
data:
|
|
||||||
(stats) => SingleChildScrollView(
|
|
||||||
child:
|
|
||||||
currentPublisher.value == null
|
|
||||||
? Column(
|
|
||||||
children: [
|
|
||||||
const Gap(24),
|
|
||||||
const Icon(Symbols.info, size: 32).padding(bottom: 4),
|
|
||||||
Text(
|
|
||||||
'creatorHubUnselectedHint',
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
).tr(),
|
|
||||||
const Gap(24),
|
|
||||||
const Divider(height: 1),
|
|
||||||
...(publishers.value?.map(
|
|
||||||
(publisher) => ListTile(
|
|
||||||
leading: ProfilePictureWidget(
|
|
||||||
file: publisher.picture,
|
|
||||||
),
|
|
||||||
title: Text(publisher.nick),
|
|
||||||
subtitle: Text('@${publisher.name}'),
|
|
||||||
onTap: () {
|
|
||||||
currentPublisher.value = publisher;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
) ??
|
|
||||||
[]),
|
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const CircleAvatar(
|
shape: RoundedRectangleBorder(
|
||||||
child: Icon(Symbols.mail),
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
),
|
),
|
||||||
title: Text('publisherCollabInvitation').tr(),
|
|
||||||
subtitle: Text(
|
|
||||||
'publisherCollabInvitationCount',
|
|
||||||
).plural(publisherInvites.value?.length ?? 0),
|
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
|
||||||
onTap: () {
|
|
||||||
showModalBottomSheet(
|
|
||||||
context: context,
|
|
||||||
isScrollControlled: true,
|
|
||||||
builder: (_) => const _PublisherInviteSheet(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
leading: const CircleAvatar(
|
|
||||||
child: Icon(Symbols.add),
|
|
||||||
),
|
|
||||||
title: Text('createPublisher').tr(),
|
|
||||||
subtitle: Text('createPublisherHint').tr(),
|
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
|
||||||
onTap: () {
|
|
||||||
context.pushNamed('creatorNew').then((value) {
|
|
||||||
if (value != null) {
|
|
||||||
ref.invalidate(publishersManagedProvider);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
: Column(
|
|
||||||
children: [
|
|
||||||
if (stats != null)
|
|
||||||
_PublisherStatsWidget(
|
|
||||||
stats: stats,
|
|
||||||
).padding(vertical: 12, horizontal: 12),
|
|
||||||
ListTile(
|
|
||||||
minTileHeight: 48,
|
minTileHeight: 48,
|
||||||
title: Text('stickers').tr(),
|
title: Text('stickers').tr(),
|
||||||
trailing: Icon(Symbols.chevron_right),
|
trailing: Icon(Symbols.chevron_right),
|
||||||
leading: const Icon(Symbols.ar_stickers),
|
leading: const Icon(Symbols.ar_stickers),
|
||||||
contentPadding: EdgeInsets.symmetric(
|
|
||||||
horizontal: 24,
|
|
||||||
),
|
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context.pushNamed(
|
context.pushNamed(
|
||||||
'creatorStickers',
|
'creatorStickers',
|
||||||
pathParameters: {
|
pathParameters: {'name': currentPublisher.value!.name},
|
||||||
'name': currentPublisher.value!.name,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
),
|
||||||
minTileHeight: 48,
|
minTileHeight: 48,
|
||||||
title: Text('posts').tr(),
|
title: Text('posts').tr(),
|
||||||
trailing: Icon(Symbols.chevron_right),
|
trailing: Icon(Symbols.chevron_right),
|
||||||
leading: const Icon(Symbols.sticky_note_2),
|
leading: const Icon(Symbols.sticky_note_2),
|
||||||
contentPadding: EdgeInsets.symmetric(
|
|
||||||
horizontal: 24,
|
|
||||||
),
|
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context.pushNamed(
|
context.pushNamed(
|
||||||
'creatorPosts',
|
'creatorPosts',
|
||||||
pathParameters: {
|
pathParameters: {'name': currentPublisher.value!.name},
|
||||||
'name': currentPublisher.value!.name,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
),
|
||||||
minTileHeight: 48,
|
minTileHeight: 48,
|
||||||
title: Text('polls').tr(),
|
title: Text('polls').tr(),
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
leading: const Icon(Symbols.poll),
|
leading: const Icon(Symbols.poll),
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 24,
|
|
||||||
),
|
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context.pushNamed(
|
context.pushNamed(
|
||||||
'creatorPolls',
|
'creatorPolls',
|
||||||
pathParameters: {
|
pathParameters: {'name': currentPublisher.value!.name},
|
||||||
'name': currentPublisher.value!.name,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
),
|
||||||
|
minTileHeight: 48,
|
||||||
|
title: const Text('webFeeds').tr(),
|
||||||
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
|
leading: const Icon(Symbols.rss_feed),
|
||||||
|
onTap: () {
|
||||||
|
context.push('/creators/${currentPublisher.value!.name}/feeds');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
final rightItems = [
|
||||||
|
ListTile(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
),
|
||||||
minTileHeight: 48,
|
minTileHeight: 48,
|
||||||
title: Text('publisherMembers').tr(),
|
title: Text('publisherMembers').tr(),
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
leading: const Icon(Symbols.group),
|
leading: const Icon(Symbols.group),
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 24,
|
|
||||||
),
|
|
||||||
onTap: () {
|
onTap: () {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
context: context,
|
context: context,
|
||||||
builder:
|
builder:
|
||||||
(context) => _PublisherMemberListSheet(
|
(context) => _PublisherMemberListSheet(
|
||||||
publisherUname:
|
publisherUname: currentPublisher.value!.name,
|
||||||
currentPublisher.value!.name,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
|
||||||
minTileHeight: 48,
|
|
||||||
title: const Text('webFeeds').tr(),
|
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
|
||||||
leading: const Icon(Symbols.rss_feed),
|
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 24,
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
context.push(
|
|
||||||
'/creators/${currentPublisher.value!.name}/feeds',
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ExpansionTile(
|
ExpansionTile(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
),
|
||||||
title: Text('publisherFeatures').tr(),
|
title: Text('publisherFeatures').tr(),
|
||||||
leading: const Icon(Symbols.flag),
|
leading: const Icon(Symbols.flag),
|
||||||
tilePadding: EdgeInsets.symmetric(horizontal: 24),
|
tilePadding: const EdgeInsets.only(left: 16, right: 24),
|
||||||
minTileHeight: 48,
|
minTileHeight: 48,
|
||||||
children: [
|
children: [
|
||||||
...publisherFeatures.when(
|
...publisherFeatures.when(
|
||||||
@@ -437,28 +444,19 @@ class CreatorHubScreen extends HookConsumerWidget {
|
|||||||
'publisherFeature${entry.key.capitalizeEachWord()}';
|
'publisherFeature${entry.key.capitalizeEachWord()}';
|
||||||
return ListTile(
|
return ListTile(
|
||||||
minTileHeight: 48,
|
minTileHeight: 48,
|
||||||
contentPadding: EdgeInsets.symmetric(
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
horizontal: 24,
|
|
||||||
),
|
|
||||||
leading: Icon(
|
leading: Icon(
|
||||||
Symbols.circle,
|
Symbols.circle,
|
||||||
color:
|
color: entry.value ? Colors.green : Colors.red,
|
||||||
entry.value
|
|
||||||
? Colors.green
|
|
||||||
: Colors.red,
|
|
||||||
fill: 1,
|
fill: 1,
|
||||||
size: 16,
|
size: 16,
|
||||||
).padding(left: 2, top: 4),
|
).padding(left: 2, top: 4),
|
||||||
title: Text(keyPrefix).tr(),
|
title: Text(keyPrefix).tr(),
|
||||||
subtitle: Column(
|
subtitle: Column(
|
||||||
crossAxisAlignment:
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
CrossAxisAlignment.start,
|
|
||||||
children: [
|
children: [
|
||||||
Text('${keyPrefix}Description').tr(),
|
Text('${keyPrefix}Description').tr(),
|
||||||
if (!entry.value)
|
if (!entry.value) Text('${keyPrefix}Hint').tr().bold(),
|
||||||
Text(
|
|
||||||
'${keyPrefix}Hint',
|
|
||||||
).tr().bold(),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
isThreeLine: true,
|
isThreeLine: true,
|
||||||
@@ -470,44 +468,141 @@ class CreatorHubScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Divider(height: 1).padding(vertical: 8),
|
|
||||||
ListTile(
|
ListTile(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
),
|
||||||
minTileHeight: 48,
|
minTileHeight: 48,
|
||||||
title: Text('editPublisher').tr(),
|
title: Text('editPublisher').tr(),
|
||||||
trailing: Icon(Symbols.chevron_right),
|
trailing: Icon(Symbols.chevron_right),
|
||||||
leading: const Icon(Symbols.edit),
|
leading: const Icon(Symbols.edit),
|
||||||
contentPadding: EdgeInsets.symmetric(
|
onTap: updatePublisher,
|
||||||
horizontal: 24,
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
updatePublisher();
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
),
|
||||||
minTileHeight: 48,
|
minTileHeight: 48,
|
||||||
title: Text('deletePublisher').tr(),
|
title: Text('deletePublisher').tr(),
|
||||||
trailing: Icon(Symbols.chevron_right),
|
trailing: Icon(Symbols.chevron_right),
|
||||||
leading: const Icon(Symbols.delete),
|
leading: const Icon(Symbols.delete),
|
||||||
contentPadding: EdgeInsets.symmetric(
|
onTap: deletePublisher,
|
||||||
horizontal: 24,
|
|
||||||
),
|
),
|
||||||
onTap: () {
|
];
|
||||||
deletePublisher();
|
|
||||||
|
if (isWide) {
|
||||||
|
return Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Card(
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
|
child: Column(children: leftItems),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Card(
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
|
child: Column(children: rightItems),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 12);
|
||||||
|
} else {
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: Column(
|
||||||
|
children: [...leftItems, const Divider(height: 8), ...rightItems],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return AppScaffold(
|
||||||
|
isNoBackground: false,
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: const PageBackButton(),
|
||||||
|
title: Text('creatorHub').tr(),
|
||||||
|
actions: [
|
||||||
|
if (!isWideScreen(context))
|
||||||
|
PublisherSelector(
|
||||||
|
currentPublisher: currentPublisher.value,
|
||||||
|
publishersMenu: publishersMenu,
|
||||||
|
onChanged: (value) {
|
||||||
|
currentPublisher.value = value;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
const Gap(8),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
final isWide = isWideScreen(context);
|
||||||
|
final maxWidth = isWide ? 800.0 : double.infinity;
|
||||||
|
|
||||||
|
return Center(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(maxWidth: maxWidth),
|
||||||
|
child: publisherStats.when(
|
||||||
|
data:
|
||||||
|
(stats) => SingleChildScrollView(
|
||||||
|
child:
|
||||||
|
currentPublisher.value == null
|
||||||
|
? ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(maxWidth: 640),
|
||||||
|
child: _PublisherUnselectedWidget(
|
||||||
|
onPublisherSelected: (publisher) {
|
||||||
|
currentPublisher.value = publisher;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).center()
|
||||||
|
: isWide
|
||||||
|
? Column(
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
PublisherSelector(
|
||||||
|
currentPublisher: currentPublisher.value,
|
||||||
|
publishersMenu: publishersMenu,
|
||||||
|
onChanged: (value) {
|
||||||
|
currentPublisher.value = value;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (stats != null)
|
||||||
|
_PublisherStatsWidget(
|
||||||
|
stats: stats,
|
||||||
|
heatmap: publisherHeatmap.value,
|
||||||
|
).padding(horizontal: 12),
|
||||||
|
buildNavigationWidget(true),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: Column(
|
||||||
|
spacing: 12,
|
||||||
|
children: [
|
||||||
|
if (stats != null)
|
||||||
|
_PublisherStatsWidget(
|
||||||
|
stats: stats,
|
||||||
|
heatmap: publisherHeatmap.value,
|
||||||
|
).padding(horizontal: 16),
|
||||||
|
buildNavigationWidget(false),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
error: (_, _) => const SizedBox.shrink(),
|
error: (_, _) => const SizedBox.shrink(),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PublisherStatsWidget extends StatelessWidget {
|
class _PublisherStatsWidget extends StatelessWidget {
|
||||||
final SnPublisherStats stats;
|
final SnPublisherStats stats;
|
||||||
const _PublisherStatsWidget({required this.stats});
|
final SnHeatmap? heatmap;
|
||||||
|
const _PublisherStatsWidget({required this.stats, this.heatmap});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -560,6 +655,7 @@ class _PublisherStatsWidget extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
if (heatmap != null) ActivityHeatmapWidget(heatmap: heatmap!),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -576,6 +672,18 @@ class _PublisherStatsWidget extends StatelessWidget {
|
|||||||
height: 100,
|
height: 100,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
|
||||||
|
child: Tooltip(
|
||||||
|
richMessage: TextSpan(
|
||||||
|
text: statLabel.tr(),
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
children: [
|
||||||
|
TextSpan(text: ' '),
|
||||||
|
TextSpan(
|
||||||
|
text: statValue,
|
||||||
|
style: TextStyle(fontWeight: FontWeight.normal),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
@@ -583,6 +691,8 @@ class _PublisherStatsWidget extends StatelessWidget {
|
|||||||
Text(
|
Text(
|
||||||
statValue,
|
statValue,
|
||||||
style: Theme.of(context).textTheme.headlineMedium,
|
style: Theme.of(context).textTheme.headlineMedium,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
const Gap(4),
|
const Gap(4),
|
||||||
Text(
|
Text(
|
||||||
@@ -594,6 +704,7 @@ class _PublisherStatsWidget extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -149,6 +149,127 @@ class _PublisherStatsProviderElement
|
|||||||
String? get uname => (origin as PublisherStatsProvider).uname;
|
String? get uname => (origin as PublisherStatsProvider).uname;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _$publisherHeatmapHash() => r'5f70c55e14629ec8628445a317888e02fccd9af2';
|
||||||
|
|
||||||
|
/// See also [publisherHeatmap].
|
||||||
|
@ProviderFor(publisherHeatmap)
|
||||||
|
const publisherHeatmapProvider = PublisherHeatmapFamily();
|
||||||
|
|
||||||
|
/// See also [publisherHeatmap].
|
||||||
|
class PublisherHeatmapFamily extends Family<AsyncValue<SnHeatmap?>> {
|
||||||
|
/// See also [publisherHeatmap].
|
||||||
|
const PublisherHeatmapFamily();
|
||||||
|
|
||||||
|
/// See also [publisherHeatmap].
|
||||||
|
PublisherHeatmapProvider call(String? uname) {
|
||||||
|
return PublisherHeatmapProvider(uname);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
PublisherHeatmapProvider getProviderOverride(
|
||||||
|
covariant PublisherHeatmapProvider provider,
|
||||||
|
) {
|
||||||
|
return call(provider.uname);
|
||||||
|
}
|
||||||
|
|
||||||
|
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'publisherHeatmapProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [publisherHeatmap].
|
||||||
|
class PublisherHeatmapProvider extends AutoDisposeFutureProvider<SnHeatmap?> {
|
||||||
|
/// See also [publisherHeatmap].
|
||||||
|
PublisherHeatmapProvider(String? uname)
|
||||||
|
: this._internal(
|
||||||
|
(ref) => publisherHeatmap(ref as PublisherHeatmapRef, uname),
|
||||||
|
from: publisherHeatmapProvider,
|
||||||
|
name: r'publisherHeatmapProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$publisherHeatmapHash,
|
||||||
|
dependencies: PublisherHeatmapFamily._dependencies,
|
||||||
|
allTransitiveDependencies:
|
||||||
|
PublisherHeatmapFamily._allTransitiveDependencies,
|
||||||
|
uname: uname,
|
||||||
|
);
|
||||||
|
|
||||||
|
PublisherHeatmapProvider._internal(
|
||||||
|
super._createNotifier, {
|
||||||
|
required super.name,
|
||||||
|
required super.dependencies,
|
||||||
|
required super.allTransitiveDependencies,
|
||||||
|
required super.debugGetCreateSourceHash,
|
||||||
|
required super.from,
|
||||||
|
required this.uname,
|
||||||
|
}) : super.internal();
|
||||||
|
|
||||||
|
final String? uname;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Override overrideWith(
|
||||||
|
FutureOr<SnHeatmap?> Function(PublisherHeatmapRef provider) create,
|
||||||
|
) {
|
||||||
|
return ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
override: PublisherHeatmapProvider._internal(
|
||||||
|
(ref) => create(ref as PublisherHeatmapRef),
|
||||||
|
from: from,
|
||||||
|
name: null,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
debugGetCreateSourceHash: null,
|
||||||
|
uname: uname,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AutoDisposeFutureProviderElement<SnHeatmap?> createElement() {
|
||||||
|
return _PublisherHeatmapProviderElement(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is PublisherHeatmapProvider && other.uname == uname;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, uname.hashCode);
|
||||||
|
|
||||||
|
return _SystemHash.finish(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
mixin PublisherHeatmapRef on AutoDisposeFutureProviderRef<SnHeatmap?> {
|
||||||
|
/// The parameter `uname` of this provider.
|
||||||
|
String? get uname;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PublisherHeatmapProviderElement
|
||||||
|
extends AutoDisposeFutureProviderElement<SnHeatmap?>
|
||||||
|
with PublisherHeatmapRef {
|
||||||
|
_PublisherHeatmapProviderElement(super.provider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get uname => (origin as PublisherHeatmapProvider).uname;
|
||||||
|
}
|
||||||
|
|
||||||
String _$publisherIdentityHash() => r'299372f25fa4b2bf8e11a8ba2d645100fc38e76f';
|
String _$publisherIdentityHash() => r'299372f25fa4b2bf8e11a8ba2d645100fc38e76f';
|
||||||
|
|
||||||
/// See also [publisherIdentity].
|
/// See also [publisherIdentity].
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ class CreatorPollListScreen extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
|
isNoBackground: false,
|
||||||
appBar: AppBar(title: const Text('Polls')),
|
appBar: AppBar(title: const Text('Polls')),
|
||||||
floatingActionButton: FloatingActionButton(
|
floatingActionButton: FloatingActionButton(
|
||||||
onPressed: () => _createPoll(context),
|
onPressed: () => _createPoll(context),
|
||||||
@@ -105,10 +106,13 @@ class CreatorPollListScreen extends HookConsumerWidget {
|
|||||||
return endItemView;
|
return endItemView;
|
||||||
}
|
}
|
||||||
final pollWithStats = data.items[index];
|
final pollWithStats = data.items[index];
|
||||||
return _CreatorPollItem(
|
return ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(maxWidth: 640),
|
||||||
|
child: _CreatorPollItem(
|
||||||
pollWithStats: pollWithStats,
|
pollWithStats: pollWithStats,
|
||||||
pubName: pubName,
|
pubName: pubName,
|
||||||
);
|
),
|
||||||
|
).center();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
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:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
import 'package:island/widgets/content/sheet.dart';
|
|
||||||
import 'package:island/widgets/post/post_list.dart';
|
import 'package:island/widgets/post/post_list.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
|
||||||
|
|
||||||
class CreatorPostListScreen extends HookConsumerWidget {
|
class CreatorPostListScreen extends HookConsumerWidget {
|
||||||
final String pubName;
|
final String pubName;
|
||||||
@@ -16,61 +13,19 @@ class CreatorPostListScreen extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final refreshKey = useState(0);
|
final refreshKey = useState(0);
|
||||||
|
|
||||||
void showCreatePostSheet() {
|
|
||||||
showModalBottomSheet(
|
|
||||||
context: context,
|
|
||||||
builder:
|
|
||||||
(context) => SheetScaffold(
|
|
||||||
titleText: 'create'.tr(),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Symbols.edit),
|
|
||||||
title: Text('Post'),
|
|
||||||
subtitle: Text('Create a regular post'),
|
|
||||||
onTap: () async {
|
|
||||||
Navigator.pop(context);
|
|
||||||
final result = await context.pushNamed(
|
|
||||||
'postCompose',
|
|
||||||
queryParameters: {'type': '0'},
|
|
||||||
);
|
|
||||||
if (result == true) {
|
|
||||||
refreshKey.value++;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Symbols.article),
|
|
||||||
title: Text('Article'),
|
|
||||||
subtitle: Text('Create a detailed article'),
|
|
||||||
onTap: () async {
|
|
||||||
Navigator.pop(context);
|
|
||||||
final result = await context.pushNamed(
|
|
||||||
'postCompose',
|
|
||||||
queryParameters: {'type': '1'},
|
|
||||||
);
|
|
||||||
if (result == true) {
|
|
||||||
refreshKey.value++;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
|
isNoBackground: false,
|
||||||
appBar: AppBar(title: Text('posts').tr()),
|
appBar: AppBar(title: Text('posts').tr()),
|
||||||
body: CustomScrollView(
|
body: CustomScrollView(
|
||||||
key: ValueKey(refreshKey.value),
|
key: ValueKey(refreshKey.value),
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverPostList(pubName: pubName, itemType: PostItemType.creator),
|
SliverPostList(
|
||||||
],
|
pubName: pubName,
|
||||||
|
itemType: PostItemType.creator,
|
||||||
|
maxWidth: 640,
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
),
|
),
|
||||||
floatingActionButton: FloatingActionButton(
|
],
|
||||||
onPressed: showCreatePostSheet,
|
|
||||||
child: const Icon(Symbols.add),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,21 +10,19 @@ import 'package:image_picker/image_picker.dart';
|
|||||||
import 'package:island/models/file.dart';
|
import 'package:island/models/file.dart';
|
||||||
import 'package:island/models/publisher.dart';
|
import 'package:island/models/publisher.dart';
|
||||||
import 'package:island/models/realm.dart';
|
import 'package:island/models/realm.dart';
|
||||||
import 'package:island/pods/config.dart';
|
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
import 'package:island/pods/userinfo.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/file.dart';
|
import 'package:island/services/file.dart';
|
||||||
import 'package:island/services/responsive.dart';
|
import 'package:island/services/file_uploader.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/cloud_files.dart';
|
import 'package:island/widgets/content/cloud_files.dart';
|
||||||
import 'package:island/widgets/realm/realm_selection_dropdown.dart';
|
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.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';
|
||||||
|
|
||||||
part 'publishers.g.dart';
|
part 'publishers_form.g.dart';
|
||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
Future<List<SnPublisher>> publishersManaged(Ref ref) async {
|
Future<List<SnPublisher>> publishersManaged(Ref ref) async {
|
||||||
@@ -95,19 +93,13 @@ class EditPublisherScreen extends HookConsumerWidget {
|
|||||||
|
|
||||||
submitting.value = true;
|
submitting.value = true;
|
||||||
try {
|
try {
|
||||||
final baseUrl = ref.watch(serverUrlProvider);
|
|
||||||
final token = await getToken(ref.watch(tokenProvider));
|
|
||||||
if (token == null) throw ArgumentError('Token is null');
|
|
||||||
final cloudFile =
|
final cloudFile =
|
||||||
await putFileToCloud(
|
await FileUploader.createCloudFile(
|
||||||
fileData: UniversalFile(
|
fileData: UniversalFile(
|
||||||
data: result,
|
data: result,
|
||||||
type: UniversalFileType.image,
|
type: UniversalFileType.image,
|
||||||
),
|
),
|
||||||
atk: token,
|
client: ref.read(apiClientProvider),
|
||||||
baseUrl: baseUrl,
|
|
||||||
filename: result.name,
|
|
||||||
mimetype: result.mimeType ?? 'image/jpeg',
|
|
||||||
).future;
|
).future;
|
||||||
if (cloudFile == null) {
|
if (cloudFile == null) {
|
||||||
throw ArgumentError('Failed to upload the file...');
|
throw ArgumentError('Failed to upload the file...');
|
||||||
@@ -186,27 +178,15 @@ class EditPublisherScreen extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
|
isNoBackground: false,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(name == null ? 'createPublisher' : 'editPublisher').tr(),
|
title: Text(name == null ? 'createPublisher' : 'editPublisher').tr(),
|
||||||
leading: const PageBackButton(),
|
leading: const PageBackButton(),
|
||||||
),
|
),
|
||||||
body: SingleChildScrollView(
|
body: SingleChildScrollView(
|
||||||
padding: getTabbedPadding(context, bottom: 16),
|
padding: EdgeInsets.only(bottom: 16),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
RealmSelectionDropdown(
|
|
||||||
value: currentRealm.value,
|
|
||||||
realms: joinedRealms.when(
|
|
||||||
data: (realms) => realms,
|
|
||||||
loading: () => [],
|
|
||||||
error: (_, _) => [],
|
|
||||||
),
|
|
||||||
onChanged: (SnRealm? value) {
|
|
||||||
currentRealm.value = value;
|
|
||||||
},
|
|
||||||
isLoading: joinedRealms.isLoading,
|
|
||||||
error: joinedRealms.error?.toString(),
|
|
||||||
),
|
|
||||||
AspectRatio(
|
AspectRatio(
|
||||||
aspectRatio: 16 / 7,
|
aspectRatio: 16 / 7,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
@@ -280,6 +260,32 @@ class EditPublisherScreen extends HookConsumerWidget {
|
|||||||
onTapOutside:
|
onTapOutside:
|
||||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
),
|
),
|
||||||
|
DropdownButtonFormField<SnRealm>(
|
||||||
|
value: currentRealm.value,
|
||||||
|
decoration: InputDecoration(labelText: 'realm'.tr()),
|
||||||
|
items: [
|
||||||
|
DropdownMenuItem<SnRealm>(
|
||||||
|
value: null,
|
||||||
|
child: Text('individual'.tr()),
|
||||||
|
),
|
||||||
|
...joinedRealms.maybeWhen(
|
||||||
|
data:
|
||||||
|
(realms) => realms.map(
|
||||||
|
(realm) => DropdownMenuItem(
|
||||||
|
value: realm,
|
||||||
|
child: Text(realm.name),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
orElse: () => [],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onChanged:
|
||||||
|
joinedRealms.isLoading
|
||||||
|
? null
|
||||||
|
: (SnRealm? value) {
|
||||||
|
currentRealm.value = value;
|
||||||
|
},
|
||||||
|
),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
part of 'publishers.dart';
|
part of 'publishers_form.dart';
|
||||||
|
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
@@ -8,13 +8,14 @@ import 'package:freezed_annotation/freezed_annotation.dart';
|
|||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:google_fonts/google_fonts.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/sticker.dart';
|
import 'package:island/models/sticker.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
import 'package:island/screens/creators/stickers/stickers.dart';
|
import 'package:island/screens/creators/stickers/stickers.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
|
||||||
import 'package:island/widgets/content/cloud_file_picker.dart';
|
import 'package:island/widgets/content/cloud_file_picker.dart';
|
||||||
import 'package:island/widgets/content/cloud_files.dart';
|
import 'package:island/widgets/content/cloud_files.dart';
|
||||||
|
import 'package:island/widgets/content/sheet.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.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';
|
||||||
@@ -33,13 +34,13 @@ Future<List<SnSticker>> stickerPackContent(Ref ref, String packId) async {
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
class StickerPackDetailScreen extends HookConsumerWidget {
|
class StickerPackDetailContent extends HookConsumerWidget {
|
||||||
final String id;
|
final String id;
|
||||||
final String pubName;
|
final String pubName;
|
||||||
const StickerPackDetailScreen({
|
const StickerPackDetailContent({
|
||||||
super.key,
|
super.key,
|
||||||
required this.pubName,
|
|
||||||
required this.id,
|
required this.id,
|
||||||
|
required this.pubName,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -67,34 +68,7 @@ class StickerPackDetailScreen extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return AppScaffold(
|
return pack.when(
|
||||||
appBar: AppBar(
|
|
||||||
title: Text(pack.value?.name ?? 'loading'.tr()),
|
|
||||||
actions: [
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Symbols.add_circle),
|
|
||||||
onPressed: () {
|
|
||||||
context
|
|
||||||
.pushNamed(
|
|
||||||
'creatorStickerNew',
|
|
||||||
pathParameters: {'name': pubName, 'packId': id},
|
|
||||||
)
|
|
||||||
.then((value) {
|
|
||||||
if (value != null) {
|
|
||||||
ref.invalidate(stickerPackContentProvider(id));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
_StickerPackActionMenu(
|
|
||||||
pubName: pubName,
|
|
||||||
packId: id,
|
|
||||||
iconShadow: Shadow(),
|
|
||||||
),
|
|
||||||
const Gap(8),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
body: pack.when(
|
|
||||||
data:
|
data:
|
||||||
(pack) => Column(
|
(pack) => Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -124,10 +98,13 @@ class StickerPackDetailScreen extends HookConsumerWidget {
|
|||||||
spacing: 4,
|
spacing: 4,
|
||||||
children: [
|
children: [
|
||||||
const Icon(Symbols.tag, size: 16),
|
const Icon(Symbols.tag, size: 16),
|
||||||
SelectableText(
|
Flexible(
|
||||||
|
child: SelectableText(
|
||||||
pack.id,
|
pack.id,
|
||||||
|
maxLines: 1,
|
||||||
style: GoogleFonts.robotoMono(),
|
style: GoogleFonts.robotoMono(),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
).opacity(0.85),
|
).opacity(0.85),
|
||||||
],
|
],
|
||||||
@@ -148,7 +125,7 @@ class StickerPackDetailScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
gridDelegate:
|
gridDelegate:
|
||||||
const SliverGridDelegateWithMaxCrossAxisExtent(
|
const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||||
maxCrossAxisExtent: 48,
|
maxCrossAxisExtent: 80,
|
||||||
mainAxisSpacing: 8,
|
mainAxisSpacing: 8,
|
||||||
crossAxisSpacing: 8,
|
crossAxisSpacing: 8,
|
||||||
),
|
),
|
||||||
@@ -166,7 +143,7 @@ class StickerPackDetailScreen extends HookConsumerWidget {
|
|||||||
Clipboard.setData(
|
Clipboard.setData(
|
||||||
ClipboardData(
|
ClipboardData(
|
||||||
text:
|
text:
|
||||||
':${pack.prefix}${sticker.slug}:',
|
':${pack.prefix}+${sticker.slug}:',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -176,21 +153,21 @@ class StickerPackDetailScreen extends HookConsumerWidget {
|
|||||||
title: 'edit'.tr(),
|
title: 'edit'.tr(),
|
||||||
image: MenuImage.icon(Symbols.edit),
|
image: MenuImage.icon(Symbols.edit),
|
||||||
callback: () {
|
callback: () {
|
||||||
context
|
showModalBottomSheet(
|
||||||
.pushNamed(
|
context: context,
|
||||||
'creatorStickerEdit',
|
isScrollControlled: true,
|
||||||
pathParameters: {
|
builder:
|
||||||
'name': pubName,
|
(context) => SheetScaffold(
|
||||||
'packId': id,
|
titleText: 'editSticker'.tr(),
|
||||||
'id': sticker.id,
|
child: StickerForm(
|
||||||
},
|
packId: id,
|
||||||
)
|
id: sticker.id,
|
||||||
.then((value) {
|
),
|
||||||
|
),
|
||||||
|
).then((value) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
ref.invalidate(
|
ref.invalidate(
|
||||||
stickerPackContentProvider(
|
stickerPackContentProvider(id),
|
||||||
id,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -222,6 +199,7 @@ class StickerPackDetailScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
child: CloudImageWidget(
|
child: CloudImageWidget(
|
||||||
fileId: sticker.imageId,
|
fileId: sticker.imageId,
|
||||||
|
fit: BoxFit.contain,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -243,17 +221,17 @@ class StickerPackDetailScreen extends HookConsumerWidget {
|
|||||||
(err, _) =>
|
(err, _) =>
|
||||||
Text('Error: $err').textAlignment(TextAlign.center).center(),
|
Text('Error: $err').textAlignment(TextAlign.center).center(),
|
||||||
loading: () => const CircularProgressIndicator().center(),
|
loading: () => const CircularProgressIndicator().center(),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _StickerPackActionMenu extends HookConsumerWidget {
|
class StickerPackActionMenu extends HookConsumerWidget {
|
||||||
final String pubName;
|
final String pubName;
|
||||||
final String packId;
|
final String packId;
|
||||||
final Shadow iconShadow;
|
final Shadow iconShadow;
|
||||||
|
|
||||||
const _StickerPackActionMenu({
|
const StickerPackActionMenu({
|
||||||
|
super.key,
|
||||||
required this.pubName,
|
required this.pubName,
|
||||||
required this.packId,
|
required this.packId,
|
||||||
required this.iconShadow,
|
required this.iconShadow,
|
||||||
@@ -267,7 +245,22 @@ class _StickerPackActionMenu extends HookConsumerWidget {
|
|||||||
(context) => [
|
(context) => [
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context.push('/creators/$pubName/stickers/$packId/edit');
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
builder:
|
||||||
|
(context) => SheetScaffold(
|
||||||
|
titleText: 'editStickerPack'.tr(),
|
||||||
|
child: StickerPackForm(
|
||||||
|
pubName: pubName,
|
||||||
|
packId: packId,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).then((value) {
|
||||||
|
if (value != null) {
|
||||||
|
ref.invalidate(stickerPackProvider(packId));
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -310,42 +303,10 @@ class _StickerPackActionMenu extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
class StickerForm extends HookConsumerWidget {
|
||||||
sealed class StickerWithPackQuery with _$StickerWithPackQuery {
|
|
||||||
const factory StickerWithPackQuery({
|
|
||||||
required String packId,
|
|
||||||
required String id,
|
|
||||||
}) = _StickerWithPackQuery;
|
|
||||||
}
|
|
||||||
|
|
||||||
@riverpod
|
|
||||||
Future<SnSticker?> stickerPackSticker(
|
|
||||||
Ref ref,
|
|
||||||
StickerWithPackQuery? query,
|
|
||||||
) async {
|
|
||||||
if (query == null) return null;
|
|
||||||
final apiClient = ref.watch(apiClientProvider);
|
|
||||||
final resp = await apiClient.get(
|
|
||||||
'/sphere/stickers/${query.packId}/content/${query.id}',
|
|
||||||
);
|
|
||||||
if (resp.data == null) return null;
|
|
||||||
return SnSticker.fromJson(resp.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
class NewStickersScreen extends StatelessWidget {
|
|
||||||
final String packId;
|
|
||||||
const NewStickersScreen({super.key, required this.packId});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return EditStickersScreen(packId: packId, id: null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class EditStickersScreen extends HookConsumerWidget {
|
|
||||||
final String packId;
|
final String packId;
|
||||||
final String? id;
|
final String? id;
|
||||||
const EditStickersScreen({super.key, required this.packId, required this.id});
|
const StickerForm({super.key, required this.packId, this.id});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@@ -395,16 +356,16 @@ class EditStickersScreen extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return AppScaffold(
|
return Column(
|
||||||
appBar: AppBar(
|
|
||||||
title: Text(id == null ? 'createSticker' : 'editSticker').tr(),
|
|
||||||
),
|
|
||||||
body: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
spacing: 8,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 96,
|
height: 80,
|
||||||
width: 96,
|
width: 80,
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||||
child: Container(
|
child: Container(
|
||||||
@@ -419,45 +380,39 @@ class EditStickersScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Gap(16),
|
IconButton.filledTonal(
|
||||||
Form(
|
onPressed: () {
|
||||||
key: formKey,
|
|
||||||
child: Column(
|
|
||||||
spacing: 8,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
TextFormField(
|
|
||||||
controller: imageController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: 'stickerImage'.tr(),
|
|
||||||
border: const UnderlineInputBorder(),
|
|
||||||
suffix: InkWell(
|
|
||||||
onTap: () {
|
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => CloudFilePicker(),
|
builder:
|
||||||
|
(context) => CloudFilePicker(
|
||||||
|
allowedTypes: {UniversalFileType.image},
|
||||||
|
),
|
||||||
).then((value) {
|
).then((value) {
|
||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
image.value = value[0].id;
|
image.value = value[0].id;
|
||||||
imageController.text = image.value!;
|
imageController.text = image.value!;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
icon: const Icon(Symbols.cloud_upload),
|
||||||
child: const Icon(
|
|
||||||
Symbols.cloud_upload,
|
|
||||||
).padding(horizontal: 4),
|
|
||||||
),
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
readOnly: true,
|
const Gap(16),
|
||||||
onTapOutside:
|
Form(
|
||||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
key: formKey,
|
||||||
),
|
child: Column(
|
||||||
|
spacing: 12,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: slugController,
|
controller: slugController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'stickerSlug'.tr(),
|
labelText: 'stickerSlug'.tr(),
|
||||||
helperText: 'stickerSlugHint'.tr(),
|
helperText: 'stickerSlugHint'.tr(),
|
||||||
border: const UnderlineInputBorder(),
|
border: const OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onTapOutside:
|
onTapOutside:
|
||||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
@@ -475,7 +430,28 @@ class EditStickersScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
).padding(horizontal: 24, vertical: 24),
|
).padding(horizontal: 24, vertical: 16);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
sealed class StickerWithPackQuery with _$StickerWithPackQuery {
|
||||||
|
const factory StickerWithPackQuery({
|
||||||
|
required String packId,
|
||||||
|
required String id,
|
||||||
|
}) = _StickerWithPackQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
Future<SnSticker?> stickerPackSticker(
|
||||||
|
Ref ref,
|
||||||
|
StickerWithPackQuery? query,
|
||||||
|
) async {
|
||||||
|
if (query == null) return null;
|
||||||
|
final apiClient = ref.watch(apiClientProvider);
|
||||||
|
final resp = await apiClient.get(
|
||||||
|
'/sphere/stickers/${query.packId}/content/${query.id}',
|
||||||
|
);
|
||||||
|
if (resp.data == null) return null;
|
||||||
|
return SnSticker.fromJson(resp.data);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
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';
|
||||||
import 'package:go_router/go_router.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/sticker.dart';
|
import 'package:island/models/sticker.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
|
import 'package:island/services/responsive.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/sheet.dart';
|
||||||
|
import 'package:island/screens/creators/stickers/pack_detail.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.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';
|
||||||
@@ -22,29 +24,50 @@ class StickersScreen extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final content = SliverStickerPacksList(pubName: pubName);
|
||||||
|
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
|
isNoBackground: false,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('stickers').tr(),
|
title: const Text('stickers').tr(),
|
||||||
actions: [
|
actions: [const Gap(8)],
|
||||||
IconButton(
|
),
|
||||||
|
floatingActionButton: FloatingActionButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context
|
showModalBottomSheet(
|
||||||
.pushNamed(
|
context: context,
|
||||||
'creatorStickerPackNew',
|
isScrollControlled: true,
|
||||||
pathParameters: {'name': pubName},
|
builder:
|
||||||
)
|
(context) => SheetScaffold(
|
||||||
.then((value) {
|
titleText: 'createStickerPack'.tr(),
|
||||||
|
child: StickerPackForm(pubName: pubName),
|
||||||
|
),
|
||||||
|
).then((value) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
ref.invalidate(stickerPacksNotifierProvider(pubName));
|
ref.invalidate(stickerPacksNotifierProvider(pubName));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
icon: const Icon(Symbols.add_circle),
|
child: const Icon(Symbols.add),
|
||||||
),
|
),
|
||||||
const Gap(8),
|
body:
|
||||||
],
|
isWideScreen(context)
|
||||||
|
? Center(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(maxWidth: 640),
|
||||||
|
child: Card(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: const BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(8),
|
||||||
|
topRight: Radius.circular(8),
|
||||||
),
|
),
|
||||||
body: SliverStickerPacksList(pubName: pubName),
|
),
|
||||||
|
margin: const EdgeInsets.only(top: 16),
|
||||||
|
child: content,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: content,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,13 +93,52 @@ class SliverStickerPacksList extends HookConsumerWidget {
|
|||||||
|
|
||||||
final sticker = data.items[index];
|
final sticker = data.items[index];
|
||||||
return ListTile(
|
return ListTile(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
),
|
||||||
title: Text(sticker.name),
|
title: Text(sticker.name),
|
||||||
subtitle: Text(sticker.description),
|
subtitle: Text(sticker.description),
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context.pushNamed(
|
showModalBottomSheet(
|
||||||
'creatorStickerPackDetail',
|
context: context,
|
||||||
pathParameters: {'name': pubName, 'packId': sticker.id},
|
isScrollControlled: true,
|
||||||
|
builder:
|
||||||
|
(context) => SheetScaffold(
|
||||||
|
titleText: sticker.name,
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Symbols.add_circle),
|
||||||
|
onPressed: () {
|
||||||
|
final id = sticker.id;
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
builder:
|
||||||
|
(context) => SheetScaffold(
|
||||||
|
titleText: 'createSticker'.tr(),
|
||||||
|
child: StickerForm(packId: id),
|
||||||
|
),
|
||||||
|
).then((value) {
|
||||||
|
if (value != null) {
|
||||||
|
ref.invalidate(
|
||||||
|
stickerPackContentProvider(id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
StickerPackActionMenu(
|
||||||
|
pubName: pubName,
|
||||||
|
packId: sticker.id,
|
||||||
|
iconShadow: Shadow(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: StickerPackDetailContent(
|
||||||
|
id: sticker.id,
|
||||||
|
pubName: pubName,
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -135,20 +197,10 @@ Future<SnStickerPack?> stickerPack(Ref ref, String? packId) async {
|
|||||||
return SnStickerPack.fromJson(resp.data);
|
return SnStickerPack.fromJson(resp.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
class NewStickerPacksScreen extends HookConsumerWidget {
|
class StickerPackForm extends HookConsumerWidget {
|
||||||
final String pubName;
|
|
||||||
const NewStickerPacksScreen({super.key, required this.pubName});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
return EditStickerPacksScreen(pubName: pubName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class EditStickerPacksScreen extends HookConsumerWidget {
|
|
||||||
final String pubName;
|
final String pubName;
|
||||||
final String? packId;
|
final String? packId;
|
||||||
const EditStickerPacksScreen({super.key, required this.pubName, this.packId});
|
const StickerPackForm({super.key, required this.pubName, this.packId});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@@ -187,7 +239,7 @@ class EditStickerPacksScreen extends HookConsumerWidget {
|
|||||||
options: Options(method: packId == null ? 'POST' : 'PATCH'),
|
options: Options(method: packId == null ? 'POST' : 'PATCH'),
|
||||||
);
|
);
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
context.pop(SnStickerPack.fromJson(resp.data));
|
Navigator.of(context).pop(SnStickerPack.fromJson(resp.data));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showErrorAlert(err);
|
showErrorAlert(err);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -195,24 +247,21 @@ class EditStickerPacksScreen extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return AppScaffold(
|
return Column(
|
||||||
appBar: AppBar(
|
|
||||||
title:
|
|
||||||
Text(packId == null ? 'createStickerPack' : 'editStickerPack').tr(),
|
|
||||||
),
|
|
||||||
body: Column(
|
|
||||||
children: [
|
children: [
|
||||||
Form(
|
Form(
|
||||||
key: formKey,
|
key: formKey,
|
||||||
child: Column(
|
child: Column(
|
||||||
spacing: 8,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
spacing: 16,
|
||||||
children: [
|
children: [
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: nameController,
|
controller: nameController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'name'.tr(),
|
labelText: 'name'.tr(),
|
||||||
border: const UnderlineInputBorder(),
|
border: const OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value == null || value.isEmpty) {
|
if (value == null || value.isEmpty) {
|
||||||
@@ -227,7 +276,9 @@ class EditStickerPacksScreen extends HookConsumerWidget {
|
|||||||
controller: descriptionController,
|
controller: descriptionController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'description'.tr(),
|
labelText: 'description'.tr(),
|
||||||
border: const UnderlineInputBorder(),
|
border: const OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
|
),
|
||||||
alignLabelWithHint: true,
|
alignLabelWithHint: true,
|
||||||
),
|
),
|
||||||
minLines: 3,
|
minLines: 3,
|
||||||
@@ -239,8 +290,10 @@ class EditStickerPacksScreen extends HookConsumerWidget {
|
|||||||
controller: prefixController,
|
controller: prefixController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'stickerPackPrefix'.tr(),
|
labelText: 'stickerPackPrefix'.tr(),
|
||||||
border: const UnderlineInputBorder(),
|
border: const OutlineInputBorder(
|
||||||
helperText: 'deleteStickerHint'.tr(),
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
|
),
|
||||||
|
helperText: 'stickerPackPrefixHint'.tr(),
|
||||||
),
|
),
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value == null || value.isEmpty) {
|
if (value == null || value.isEmpty) {
|
||||||
@@ -264,7 +317,6 @@ class EditStickerPacksScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
).padding(horizontal: 24, vertical: 16),
|
).padding(horizontal: 24, vertical: 16);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,25 +6,15 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:island/models/webfeed.dart';
|
import 'package:island/models/webfeed.dart';
|
||||||
import 'package:island/pods/webfeed.dart';
|
import 'package:island/pods/webfeed.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
import 'package:island/widgets/response.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 WebFeedNewScreen extends StatelessWidget {
|
class WebfeedForm extends HookConsumerWidget {
|
||||||
final String pubName;
|
|
||||||
const WebFeedNewScreen({super.key, required this.pubName});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return WebFeedEditScreen(pubName: pubName, feedId: null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class WebFeedEditScreen extends HookConsumerWidget {
|
|
||||||
final String pubName;
|
final String pubName;
|
||||||
final String? feedId;
|
final String? feedId;
|
||||||
|
|
||||||
const WebFeedEditScreen({super.key, required this.pubName, this.feedId});
|
const WebfeedForm({super.key, required this.pubName, this.feedId});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@@ -113,14 +103,14 @@ class WebFeedEditScreen extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return feedAsync.when(
|
return feedAsync.when(
|
||||||
loading:
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
() => const AppScaffold(
|
|
||||||
body: Center(child: CircularProgressIndicator()),
|
|
||||||
),
|
|
||||||
error:
|
error:
|
||||||
(error, stack) => AppScaffold(
|
(error, _) => ResponseErrorWidget(
|
||||||
appBar: AppBar(title: const Text('Error')),
|
error: error,
|
||||||
body: Center(child: Text('Error: $error')),
|
onRetry:
|
||||||
|
() => ref.invalidate(
|
||||||
|
webFeedNotifierProvider((pubName: pubName, feedId: feedId)),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
data: (feed) {
|
data: (feed) {
|
||||||
// Initialize form fields if they're empty and we have a feed
|
// Initialize form fields if they're empty and we have a feed
|
||||||
@@ -131,40 +121,8 @@ class WebFeedEditScreen extends HookConsumerWidget {
|
|||||||
isScrapEnabled.value = feed.config.scrapPage;
|
isScrapEnabled.value = feed.config.scrapPage;
|
||||||
}
|
}
|
||||||
|
|
||||||
return _buildForm(
|
|
||||||
context,
|
|
||||||
formKey: formKey,
|
|
||||||
titleController: titleController,
|
|
||||||
urlController: urlController,
|
|
||||||
descriptionController: descriptionController,
|
|
||||||
isScrapEnabled: isScrapEnabled.value,
|
|
||||||
onScrapEnabledChanged: (value) => isScrapEnabled.value = value,
|
|
||||||
onSave: saveFeed,
|
|
||||||
onDelete: deleteFeed,
|
|
||||||
isLoading: isLoading.value,
|
|
||||||
ref: ref,
|
|
||||||
hasFeedId: feedId != null,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildForm(
|
|
||||||
BuildContext context, {
|
|
||||||
required WidgetRef ref,
|
|
||||||
required GlobalKey<FormState> formKey,
|
|
||||||
required TextEditingController titleController,
|
|
||||||
required TextEditingController urlController,
|
|
||||||
required TextEditingController descriptionController,
|
|
||||||
required bool isScrapEnabled,
|
|
||||||
required ValueChanged<bool> onScrapEnabledChanged,
|
|
||||||
required VoidCallback onSave,
|
|
||||||
required VoidCallback onDelete,
|
|
||||||
required bool isLoading,
|
|
||||||
required bool hasFeedId,
|
|
||||||
}) {
|
|
||||||
final scrapNow = useCallback(() async {
|
final scrapNow = useCallback(() async {
|
||||||
showLoadingModal(context);
|
isLoading.value = true;
|
||||||
try {
|
try {
|
||||||
await ref
|
await ref
|
||||||
.read(
|
.read(
|
||||||
@@ -181,30 +139,20 @@ class WebFeedEditScreen extends HookConsumerWidget {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
showErrorAlert(e);
|
showErrorAlert(e);
|
||||||
} finally {
|
} finally {
|
||||||
if (context.mounted) hideLoadingModal(context);
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
}, [pubName, feedId, ref, context]);
|
}, [pubName, feedId, ref, context, isLoading]);
|
||||||
|
|
||||||
return AppScaffold(
|
final formFields = Column(
|
||||||
appBar: AppBar(
|
|
||||||
title: Text(hasFeedId ? 'Edit Web Feed' : 'New Web Feed'),
|
|
||||||
actions: [
|
|
||||||
if (hasFeedId)
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Symbols.delete_forever),
|
|
||||||
onPressed: isLoading ? null : onDelete,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
body: Form(
|
|
||||||
key: formKey,
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
children: [
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: titleController,
|
controller: titleController,
|
||||||
decoration: const InputDecoration(labelText: 'Title'),
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Title',
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
|
),
|
||||||
|
),
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value == null || value.isEmpty) {
|
if (value == null || value.isEmpty) {
|
||||||
return 'Please enter a title';
|
return 'Please enter a title';
|
||||||
@@ -220,6 +168,9 @@ class WebFeedEditScreen extends HookConsumerWidget {
|
|||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: 'URL',
|
labelText: 'URL',
|
||||||
hintText: 'https://example.com/feed',
|
hintText: 'https://example.com/feed',
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
keyboardType: TextInputType.url,
|
keyboardType: TextInputType.url,
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
@@ -241,6 +192,9 @@ class WebFeedEditScreen extends HookConsumerWidget {
|
|||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: 'Description',
|
labelText: 'Description',
|
||||||
alignLabelWithHint: true,
|
alignLabelWithHint: true,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onTapOutside:
|
onTapOutside:
|
||||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
@@ -260,30 +214,49 @@ class WebFeedEditScreen extends HookConsumerWidget {
|
|||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
value: isScrapEnabled,
|
value: isScrapEnabled.value,
|
||||||
onChanged: onScrapEnabledChanged,
|
onChanged: (value) => isScrapEnabled.value = value,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
if (hasFeedId) ...[
|
if (feedId != null) ...[
|
||||||
FilledButton.tonalIcon(
|
TextButton.icon(
|
||||||
onPressed: isLoading ? null : scrapNow,
|
onPressed: isLoading.value ? null : scrapNow,
|
||||||
icon: const Icon(Symbols.refresh),
|
icon: const Icon(Symbols.refresh),
|
||||||
label: const Text('Scrape Now'),
|
label: const Text('Scrape Now'),
|
||||||
).alignment(Alignment.centerRight),
|
).alignment(Alignment.centerRight),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
],
|
],
|
||||||
FilledButton.icon(
|
],
|
||||||
onPressed: isLoading ? null : onSave,
|
).padding(all: 20);
|
||||||
|
|
||||||
|
final formWidget = Form(
|
||||||
|
key: formKey,
|
||||||
|
child: SingleChildScrollView(child: formFields),
|
||||||
|
);
|
||||||
|
|
||||||
|
final buttonsRow = Row(
|
||||||
|
children: [
|
||||||
|
if (feedId != null)
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: isLoading.value ? null : deleteFeed,
|
||||||
|
icon: const Icon(Symbols.delete_forever),
|
||||||
|
label: const Text('Delete Web Feed'),
|
||||||
|
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: isLoading.value ? null : saveFeed,
|
||||||
icon: const Icon(Symbols.save),
|
icon: const Icon(Symbols.save),
|
||||||
label: Text('saveChanges').tr(),
|
label: Text('saveChanges').tr(),
|
||||||
).alignment(Alignment.centerRight),
|
),
|
||||||
],
|
],
|
||||||
).padding(all: 20),
|
).padding(horizontal: 20, vertical: 12);
|
||||||
),
|
|
||||||
),
|
return Column(children: [Expanded(child: formWidget), buttonsRow]);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
import 'package:island/pods/webfeed.dart';
|
import 'package:island/pods/webfeed.dart';
|
||||||
|
import 'package:island/screens/creators/webfeed/webfeed_edit.dart';
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
|
import 'package:island/widgets/content/sheet.dart';
|
||||||
import 'package:island/widgets/empty_state.dart';
|
import 'package:island/widgets/empty_state.dart';
|
||||||
import 'package:island/widgets/extended_refresh_indicator.dart';
|
import 'package:island/widgets/extended_refresh_indicator.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
|
||||||
class WebFeedListScreen extends ConsumerWidget {
|
class WebFeedListScreen extends ConsumerWidget {
|
||||||
final String pubName;
|
final String pubName;
|
||||||
@@ -17,13 +19,19 @@ class WebFeedListScreen extends ConsumerWidget {
|
|||||||
final feedsAsync = ref.watch(webFeedListProvider(pubName));
|
final feedsAsync = ref.watch(webFeedListProvider(pubName));
|
||||||
|
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
|
isNoBackground: false,
|
||||||
appBar: AppBar(title: const Text('Web Feeds')),
|
appBar: AppBar(title: const Text('Web Feeds')),
|
||||||
floatingActionButton: FloatingActionButton(
|
floatingActionButton: FloatingActionButton(
|
||||||
child: const Icon(Symbols.add),
|
child: const Icon(Symbols.add),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.pushNamed(
|
showModalBottomSheet(
|
||||||
'creatorFeedNew',
|
context: context,
|
||||||
pathParameters: {'name': pubName},
|
isScrollControlled: true,
|
||||||
|
builder:
|
||||||
|
(context) => SheetScaffold(
|
||||||
|
titleText: 'New Web Feed',
|
||||||
|
child: WebfeedForm(pubName: pubName, feedId: null),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -43,7 +51,9 @@ class WebFeedListScreen extends ConsumerWidget {
|
|||||||
itemCount: feeds.length,
|
itemCount: feeds.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final feed = feeds[index];
|
final feed = feeds[index];
|
||||||
return Card(
|
return ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(maxWidth: 640),
|
||||||
|
child: Card(
|
||||||
margin: const EdgeInsets.symmetric(
|
margin: const EdgeInsets.symmetric(
|
||||||
horizontal: 12,
|
horizontal: 12,
|
||||||
vertical: 4,
|
vertical: 4,
|
||||||
@@ -66,13 +76,22 @@ class WebFeedListScreen extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context.pushNamed(
|
showModalBottomSheet(
|
||||||
'creatorFeedEdit',
|
context: context,
|
||||||
pathParameters: {'name': pubName, 'feedId': feed.id},
|
isScrollControlled: true,
|
||||||
|
builder:
|
||||||
|
(context) => SheetScaffold(
|
||||||
|
titleText: 'Edit Web Feed',
|
||||||
|
child: WebfeedForm(
|
||||||
|
pubName: pubName,
|
||||||
|
feedId: feed.id,
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
|
).center();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
import 'package:go_router/go_router.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/custom_app.dart';
|
import 'package:island/models/custom_app.dart';
|
||||||
@@ -32,6 +33,7 @@ class AppDetailScreen extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
|
isNoBackground: false,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(appData.value?.name ?? 'appDetails'.tr()),
|
title: Text(appData.value?.name ?? 'appDetails'.tr()),
|
||||||
actions: [
|
actions: [
|
||||||
@@ -51,6 +53,7 @@ class AppDetailScreen extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
const Gap(8),
|
||||||
],
|
],
|
||||||
bottom: TabBar(
|
bottom: TabBar(
|
||||||
controller: tabController,
|
controller: tabController,
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class BotDetailScreen extends HookConsumerWidget {
|
|||||||
final botData = ref.watch(botProvider(publisherName, projectId, botId));
|
final botData = ref.watch(botProvider(publisherName, projectId, botId));
|
||||||
|
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
|
isNoBackground: false,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(botData.value?.account.nick ?? 'botDetails'.tr()),
|
title: Text(botData.value?.account.nick ?? 'botDetails'.tr()),
|
||||||
actions: [
|
actions: [
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:island/models/custom_app.dart';
|
import 'package:island/models/custom_app.dart';
|
||||||
import 'package:island/models/file.dart';
|
import 'package:island/models/file.dart';
|
||||||
import 'package:island/pods/config.dart';
|
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
import 'package:island/screens/developers/apps.dart';
|
import 'package:island/screens/developers/apps.dart';
|
||||||
import 'package:island/services/file.dart';
|
import 'package:island/services/file.dart';
|
||||||
|
import 'package:island/services/file_uploader.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/cloud_files.dart';
|
import 'package:island/widgets/content/cloud_files.dart';
|
||||||
@@ -137,19 +137,13 @@ class EditAppScreen extends HookConsumerWidget {
|
|||||||
|
|
||||||
submitting.value = true;
|
submitting.value = true;
|
||||||
try {
|
try {
|
||||||
final baseUrl = ref.watch(serverUrlProvider);
|
|
||||||
final token = await getToken(ref.watch(tokenProvider));
|
|
||||||
if (token == null) throw ArgumentError('Token is null');
|
|
||||||
final cloudFile =
|
final cloudFile =
|
||||||
await putFileToCloud(
|
await FileUploader.createCloudFile(
|
||||||
|
client: ref.read(apiClientProvider),
|
||||||
fileData: UniversalFile(
|
fileData: UniversalFile(
|
||||||
data: result,
|
data: result,
|
||||||
type: UniversalFileType.image,
|
type: UniversalFileType.image,
|
||||||
),
|
),
|
||||||
atk: token,
|
|
||||||
baseUrl: baseUrl,
|
|
||||||
filename: result.name,
|
|
||||||
mimetype: result.mimeType ?? 'image/jpeg',
|
|
||||||
).future;
|
).future;
|
||||||
if (cloudFile == null) {
|
if (cloudFile == null) {
|
||||||
throw ArgumentError('Failed to upload the file...');
|
throw ArgumentError('Failed to upload the file...');
|
||||||
@@ -323,6 +317,7 @@ class EditAppScreen extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
|
isNoBackground: false,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(isNew ? 'createCustomApp'.tr() : 'editCustomApp'.tr()),
|
title: Text(isNew ? 'createCustomApp'.tr() : 'editCustomApp'.tr()),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:island/models/bot.dart';
|
import 'package:island/models/bot.dart';
|
||||||
import 'package:island/models/file.dart';
|
import 'package:island/models/file.dart';
|
||||||
import 'package:island/pods/config.dart';
|
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
import 'package:island/services/file.dart';
|
import 'package:island/services/file.dart';
|
||||||
|
import 'package:island/services/file_uploader.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/cloud_files.dart';
|
import 'package:island/widgets/content/cloud_files.dart';
|
||||||
@@ -123,19 +123,13 @@ class EditBotScreen extends HookConsumerWidget {
|
|||||||
|
|
||||||
submitting.value = true;
|
submitting.value = true;
|
||||||
try {
|
try {
|
||||||
final baseUrl = ref.watch(serverUrlProvider);
|
|
||||||
final token = await getToken(ref.watch(tokenProvider));
|
|
||||||
if (token == null) throw ArgumentError('Token is null');
|
|
||||||
final cloudFile =
|
final cloudFile =
|
||||||
await putFileToCloud(
|
await FileUploader.createCloudFile(
|
||||||
fileData: UniversalFile(
|
fileData: UniversalFile(
|
||||||
data: result,
|
data: result,
|
||||||
type: UniversalFileType.image,
|
type: UniversalFileType.image,
|
||||||
),
|
),
|
||||||
atk: token,
|
client: ref.read(apiClientProvider),
|
||||||
baseUrl: baseUrl,
|
|
||||||
filename: result.name,
|
|
||||||
mimetype: result.mimeType ?? 'image/jpeg',
|
|
||||||
).future;
|
).future;
|
||||||
if (cloudFile == null) {
|
if (cloudFile == null) {
|
||||||
throw ArgumentError('Failed to upload the file...');
|
throw ArgumentError('Failed to upload the file...');
|
||||||
@@ -198,6 +192,7 @@ class EditBotScreen extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
|
isNoBackground: false,
|
||||||
appBar: AppBar(title: Text(isNew ? 'createBot'.tr() : 'editBot'.tr())),
|
appBar: AppBar(title: Text(isNew ? 'createBot'.tr() : 'editBot'.tr())),
|
||||||
body:
|
body:
|
||||||
botData == null && !isNew
|
botData == null && !isNew
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ 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/dev_project.dart';
|
import 'package:island/models/dev_project.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
import 'package:island/screens/developers/projects.dart';
|
import 'package:island/screens/developers/hub.dart';
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
import 'package:island/widgets/response.dart';
|
import 'package:island/widgets/response.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -168,5 +168,125 @@ final developersProvider =
|
|||||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
// ignore: unused_element
|
// ignore: unused_element
|
||||||
typedef DevelopersRef = AutoDisposeFutureProviderRef<List<SnDeveloper>>;
|
typedef DevelopersRef = AutoDisposeFutureProviderRef<List<SnDeveloper>>;
|
||||||
|
String _$devProjectsHash() => r'715b395bebda785d38691ffee3b88e50b498c91a';
|
||||||
|
|
||||||
|
/// See also [devProjects].
|
||||||
|
@ProviderFor(devProjects)
|
||||||
|
const devProjectsProvider = DevProjectsFamily();
|
||||||
|
|
||||||
|
/// See also [devProjects].
|
||||||
|
class DevProjectsFamily extends Family<AsyncValue<List<DevProject>>> {
|
||||||
|
/// See also [devProjects].
|
||||||
|
const DevProjectsFamily();
|
||||||
|
|
||||||
|
/// See also [devProjects].
|
||||||
|
DevProjectsProvider call(String pubName) {
|
||||||
|
return DevProjectsProvider(pubName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
DevProjectsProvider getProviderOverride(
|
||||||
|
covariant DevProjectsProvider 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'devProjectsProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [devProjects].
|
||||||
|
class DevProjectsProvider extends AutoDisposeFutureProvider<List<DevProject>> {
|
||||||
|
/// See also [devProjects].
|
||||||
|
DevProjectsProvider(String pubName)
|
||||||
|
: this._internal(
|
||||||
|
(ref) => devProjects(ref as DevProjectsRef, pubName),
|
||||||
|
from: devProjectsProvider,
|
||||||
|
name: r'devProjectsProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$devProjectsHash,
|
||||||
|
dependencies: DevProjectsFamily._dependencies,
|
||||||
|
allTransitiveDependencies: DevProjectsFamily._allTransitiveDependencies,
|
||||||
|
pubName: pubName,
|
||||||
|
);
|
||||||
|
|
||||||
|
DevProjectsProvider._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
|
||||||
|
Override overrideWith(
|
||||||
|
FutureOr<List<DevProject>> Function(DevProjectsRef provider) create,
|
||||||
|
) {
|
||||||
|
return ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
override: DevProjectsProvider._internal(
|
||||||
|
(ref) => create(ref as DevProjectsRef),
|
||||||
|
from: from,
|
||||||
|
name: null,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
debugGetCreateSourceHash: null,
|
||||||
|
pubName: pubName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AutoDisposeFutureProviderElement<List<DevProject>> createElement() {
|
||||||
|
return _DevProjectsProviderElement(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is DevProjectsProvider && 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 DevProjectsRef on AutoDisposeFutureProviderRef<List<DevProject>> {
|
||||||
|
/// The parameter `pubName` of this provider.
|
||||||
|
String get pubName;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DevProjectsProviderElement
|
||||||
|
extends AutoDisposeFutureProviderElement<List<DevProject>>
|
||||||
|
with DevProjectsRef {
|
||||||
|
_DevProjectsProviderElement(super.provider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pubName => (origin as DevProjectsProvider).pubName;
|
||||||
|
}
|
||||||
|
|
||||||
// ignore_for_file: type=lint
|
// 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
|
// 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
|
||||||
|
|||||||
@@ -1,92 +0,0 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:gap/gap.dart';
|
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:island/screens/developers/apps.dart';
|
|
||||||
import 'package:island/screens/developers/bots.dart';
|
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
|
||||||
|
|
||||||
class ProjectDetailScreen extends HookConsumerWidget {
|
|
||||||
final String publisherName;
|
|
||||||
final String projectId;
|
|
||||||
|
|
||||||
const ProjectDetailScreen({
|
|
||||||
super.key,
|
|
||||||
required this.publisherName,
|
|
||||||
required this.projectId,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final tabController = useTabController(initialLength: 2);
|
|
||||||
|
|
||||||
return AppScaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title: Text('projectDetails').tr(),
|
|
||||||
actions: [
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Symbols.add),
|
|
||||||
onPressed: () {
|
|
||||||
// Get current tab index
|
|
||||||
final index = tabController.index;
|
|
||||||
switch (index) {
|
|
||||||
case 0:
|
|
||||||
context.pushNamed(
|
|
||||||
'developerAppNew',
|
|
||||||
pathParameters: {
|
|
||||||
'name': publisherName,
|
|
||||||
'projectId': projectId,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
context.pushNamed(
|
|
||||||
'developerBotNew',
|
|
||||||
pathParameters: {
|
|
||||||
'name': publisherName,
|
|
||||||
'projectId': projectId,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const Gap(8),
|
|
||||||
],
|
|
||||||
bottom: TabBar(
|
|
||||||
controller: tabController,
|
|
||||||
tabs: [
|
|
||||||
Tab(
|
|
||||||
child: Text(
|
|
||||||
'customApps'.tr(),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: TextStyle(
|
|
||||||
color: Theme.of(context).appBarTheme.foregroundColor!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Tab(
|
|
||||||
child: Text(
|
|
||||||
'bots'.tr(),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: TextStyle(
|
|
||||||
color: Theme.of(context).appBarTheme.foregroundColor!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
body: TabBarView(
|
|
||||||
controller: tabController,
|
|
||||||
children: [
|
|
||||||
CustomAppsScreen(publisherName: publisherName, projectId: projectId),
|
|
||||||
BotsScreen(publisherName: publisherName, projectId: projectId),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
106
lib/screens/developers/project_detail_view.dart
Normal file
106
lib/screens/developers/project_detail_view.dart
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
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/models/dev_project.dart';
|
||||||
|
import 'package:island/screens/developers/apps.dart';
|
||||||
|
import 'package:island/screens/developers/bots.dart';
|
||||||
|
import 'package:island/services/responsive.dart';
|
||||||
|
|
||||||
|
class ProjectDetailView extends HookConsumerWidget {
|
||||||
|
final String publisherName;
|
||||||
|
final DevProject project;
|
||||||
|
final VoidCallback onBackToHub;
|
||||||
|
|
||||||
|
const ProjectDetailView({
|
||||||
|
super.key,
|
||||||
|
required this.publisherName,
|
||||||
|
required this.project,
|
||||||
|
required this.onBackToHub,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final tabController = useTabController(initialLength: 2);
|
||||||
|
|
||||||
|
final isWide = isWideScreen(context);
|
||||||
|
|
||||||
|
if (isWide) {
|
||||||
|
return Row(
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
Card(
|
||||||
|
margin: const EdgeInsets.only(left: 16, bottom: 16, top: 12),
|
||||||
|
child: Transform.translate(
|
||||||
|
offset: const Offset(0, -56),
|
||||||
|
child: NavigationRail(
|
||||||
|
extended: isWiderScreen(context),
|
||||||
|
scrollable: true,
|
||||||
|
labelType:
|
||||||
|
isWiderScreen(context)
|
||||||
|
? null
|
||||||
|
: NavigationRailLabelType.selected,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
selectedIndex: tabController.index,
|
||||||
|
onDestinationSelected:
|
||||||
|
(index) => tabController.animateTo(index),
|
||||||
|
destinations: [
|
||||||
|
NavigationRailDestination(
|
||||||
|
icon: Icon(Icons.apps),
|
||||||
|
label: Text('customApps'.tr()),
|
||||||
|
),
|
||||||
|
NavigationRailDestination(
|
||||||
|
icon: Icon(Icons.smart_toy),
|
||||||
|
label: Text('bots'.tr()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: TabBarView(
|
||||||
|
controller: tabController,
|
||||||
|
children: [
|
||||||
|
CustomAppsScreen(
|
||||||
|
publisherName: publisherName,
|
||||||
|
projectId: project.id,
|
||||||
|
),
|
||||||
|
BotsScreen(publisherName: publisherName, projectId: project.id),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
color: Theme.of(context).colorScheme.surface,
|
||||||
|
child: TabBar(
|
||||||
|
dividerColor: Colors.transparent,
|
||||||
|
controller: tabController,
|
||||||
|
tabs: [
|
||||||
|
Tab(
|
||||||
|
child: Text('customApps'.tr(), textAlign: TextAlign.center),
|
||||||
|
),
|
||||||
|
Tab(child: Text('bots'.tr(), textAlign: TextAlign.center)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: TabBarView(
|
||||||
|
controller: tabController,
|
||||||
|
children: [
|
||||||
|
CustomAppsScreen(
|
||||||
|
publisherName: publisherName,
|
||||||
|
projectId: project.id,
|
||||||
|
),
|
||||||
|
BotsScreen(publisherName: publisherName, projectId: project.id),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:gap/gap.dart';
|
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:island/models/dev_project.dart';
|
|
||||||
import 'package:island/pods/network.dart';
|
|
||||||
import 'package:island/widgets/alert.dart';
|
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
|
||||||
import 'package:island/widgets/response.dart';
|
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|
||||||
|
|
||||||
part 'projects.g.dart';
|
|
||||||
|
|
||||||
@riverpod
|
|
||||||
Future<List<DevProject>> devProjects(Ref ref, String pubName) async {
|
|
||||||
final client = ref.watch(apiClientProvider);
|
|
||||||
final resp = await client.get('/develop/developers/$pubName/projects');
|
|
||||||
return (resp.data as List)
|
|
||||||
.map((e) => DevProject.fromJson(e))
|
|
||||||
.cast<DevProject>()
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
class DevProjectsScreen extends HookConsumerWidget {
|
|
||||||
final String publisherName;
|
|
||||||
const DevProjectsScreen({super.key, required this.publisherName});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final projects = ref.watch(devProjectsProvider(publisherName));
|
|
||||||
|
|
||||||
return AppScaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title: Text('projects').tr(),
|
|
||||||
actions: [
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Symbols.add),
|
|
||||||
onPressed: () {
|
|
||||||
context.pushNamed(
|
|
||||||
'developerProjectNew',
|
|
||||||
pathParameters: {'name': publisherName},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const Gap(8),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
body: projects.when(
|
|
||||||
data: (data) {
|
|
||||||
if (data.isEmpty) {
|
|
||||||
return Center(child: Text('noProjects').tr());
|
|
||||||
}
|
|
||||||
return RefreshIndicator(
|
|
||||||
onRefresh:
|
|
||||||
() => ref.refresh(devProjectsProvider(publisherName).future),
|
|
||||||
child: ListView.builder(
|
|
||||||
padding: EdgeInsets.only(top: 4),
|
|
||||||
itemCount: data.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final project = data[index];
|
|
||||||
return Card(
|
|
||||||
margin: const EdgeInsets.all(8.0),
|
|
||||||
child: ListTile(
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(8.0),
|
|
||||||
),
|
|
||||||
contentPadding: EdgeInsets.only(left: 20, right: 12),
|
|
||||||
title: Text(project.name),
|
|
||||||
subtitle: Text(project.description ?? ''),
|
|
||||||
trailing: PopupMenuButton(
|
|
||||||
itemBuilder:
|
|
||||||
(context) => [
|
|
||||||
PopupMenuItem(
|
|
||||||
value: 'edit',
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Symbols.edit),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Text('edit').tr(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
PopupMenuItem(
|
|
||||||
value: 'delete',
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Symbols.delete, color: Colors.red),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Text(
|
|
||||||
'delete',
|
|
||||||
style: TextStyle(color: Colors.red),
|
|
||||||
).tr(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
onSelected: (value) {
|
|
||||||
if (value == 'edit') {
|
|
||||||
context.pushNamed(
|
|
||||||
'developerProjectEdit',
|
|
||||||
pathParameters: {
|
|
||||||
'name': publisherName,
|
|
||||||
'id': project.id,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else if (value == 'delete') {
|
|
||||||
showConfirmAlert(
|
|
||||||
'deleteProjectHint'.tr(),
|
|
||||||
'deleteProject'.tr(),
|
|
||||||
).then((confirm) {
|
|
||||||
if (confirm) {
|
|
||||||
final client = ref.read(apiClientProvider);
|
|
||||||
client.delete(
|
|
||||||
'/develop/developers/$publisherName/projects/${project.id}',
|
|
||||||
);
|
|
||||||
ref.invalidate(
|
|
||||||
devProjectsProvider(publisherName),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
context.pushNamed(
|
|
||||||
'developerProjectDetail',
|
|
||||||
pathParameters: {
|
|
||||||
'name': publisherName,
|
|
||||||
'projectId': project.id,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
|
||||||
error:
|
|
||||||
(err, stack) => ResponseErrorWidget(
|
|
||||||
error: err,
|
|
||||||
onRetry: () => ref.invalidate(devProjectsProvider(publisherName)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'projects.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// RiverpodGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
String _$devProjectsHash() => r'87fdcab47cd7d79ab019a5625617abeb1ffa1f39';
|
|
||||||
|
|
||||||
/// 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 [devProjects].
|
|
||||||
@ProviderFor(devProjects)
|
|
||||||
const devProjectsProvider = DevProjectsFamily();
|
|
||||||
|
|
||||||
/// See also [devProjects].
|
|
||||||
class DevProjectsFamily extends Family<AsyncValue<List<DevProject>>> {
|
|
||||||
/// See also [devProjects].
|
|
||||||
const DevProjectsFamily();
|
|
||||||
|
|
||||||
/// See also [devProjects].
|
|
||||||
DevProjectsProvider call(String pubName) {
|
|
||||||
return DevProjectsProvider(pubName);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
DevProjectsProvider getProviderOverride(
|
|
||||||
covariant DevProjectsProvider 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'devProjectsProvider';
|
|
||||||
}
|
|
||||||
|
|
||||||
/// See also [devProjects].
|
|
||||||
class DevProjectsProvider extends AutoDisposeFutureProvider<List<DevProject>> {
|
|
||||||
/// See also [devProjects].
|
|
||||||
DevProjectsProvider(String pubName)
|
|
||||||
: this._internal(
|
|
||||||
(ref) => devProjects(ref as DevProjectsRef, pubName),
|
|
||||||
from: devProjectsProvider,
|
|
||||||
name: r'devProjectsProvider',
|
|
||||||
debugGetCreateSourceHash:
|
|
||||||
const bool.fromEnvironment('dart.vm.product')
|
|
||||||
? null
|
|
||||||
: _$devProjectsHash,
|
|
||||||
dependencies: DevProjectsFamily._dependencies,
|
|
||||||
allTransitiveDependencies: DevProjectsFamily._allTransitiveDependencies,
|
|
||||||
pubName: pubName,
|
|
||||||
);
|
|
||||||
|
|
||||||
DevProjectsProvider._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
|
|
||||||
Override overrideWith(
|
|
||||||
FutureOr<List<DevProject>> Function(DevProjectsRef provider) create,
|
|
||||||
) {
|
|
||||||
return ProviderOverride(
|
|
||||||
origin: this,
|
|
||||||
override: DevProjectsProvider._internal(
|
|
||||||
(ref) => create(ref as DevProjectsRef),
|
|
||||||
from: from,
|
|
||||||
name: null,
|
|
||||||
dependencies: null,
|
|
||||||
allTransitiveDependencies: null,
|
|
||||||
debugGetCreateSourceHash: null,
|
|
||||||
pubName: pubName,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
AutoDisposeFutureProviderElement<List<DevProject>> createElement() {
|
|
||||||
return _DevProjectsProviderElement(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
return other is DevProjectsProvider && 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 DevProjectsRef on AutoDisposeFutureProviderRef<List<DevProject>> {
|
|
||||||
/// The parameter `pubName` of this provider.
|
|
||||||
String get pubName;
|
|
||||||
}
|
|
||||||
|
|
||||||
class _DevProjectsProviderElement
|
|
||||||
extends AutoDisposeFutureProviderElement<List<DevProject>>
|
|
||||||
with DevProjectsRef {
|
|
||||||
_DevProjectsProviderElement(super.provider);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get pubName => (origin as DevProjectsProvider).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
|
|
||||||
@@ -20,6 +20,7 @@ class ArticleDetailScreen extends ConsumerWidget {
|
|||||||
final articleAsync = ref.watch(articleDetailProvider(articleId));
|
final articleAsync = ref.watch(articleDetailProvider(articleId));
|
||||||
|
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
|
isNoBackground: false,
|
||||||
body: articleAsync.when(
|
body: articleAsync.when(
|
||||||
data:
|
data:
|
||||||
(article) => AppScaffold(
|
(article) => AppScaffold(
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import 'package:island/pods/event_calendar.dart';
|
|||||||
import 'package:island/pods/userinfo.dart';
|
import 'package:island/pods/userinfo.dart';
|
||||||
import 'package:island/screens/notification.dart';
|
import 'package:island/screens/notification.dart';
|
||||||
import 'package:island/services/responsive.dart';
|
import 'package:island/services/responsive.dart';
|
||||||
|
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
import 'package:island/models/post.dart';
|
import 'package:island/models/post.dart';
|
||||||
import 'package:island/widgets/check_in.dart';
|
import 'package:island/widgets/check_in.dart';
|
||||||
@@ -21,7 +20,6 @@ import 'package:island/widgets/post/post_featured.dart';
|
|||||||
import 'package:island/widgets/post/post_item.dart';
|
import 'package:island/widgets/post/post_item.dart';
|
||||||
import 'package:island/widgets/post/compose_card.dart';
|
import 'package:island/widgets/post/compose_card.dart';
|
||||||
import 'package:island/widgets/post/compose_dialog.dart';
|
import 'package:island/widgets/post/compose_dialog.dart';
|
||||||
import 'package:island/screens/tabs.dart';
|
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
|
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
|
||||||
@@ -56,7 +54,12 @@ Widget notificationIndicatorWidget(
|
|||||||
trailing: const Icon(Symbols.chevron_right),
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
contentPadding: EdgeInsets.only(left: 16, right: 15),
|
contentPadding: EdgeInsets.only(left: 16, right: 15),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
GoRouter.of(context).pushNamed('notifications');
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
useRootNavigator: true,
|
||||||
|
builder: (context) => const NotificationSheet(),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -231,7 +234,6 @@ class ExploreScreen extends HookConsumerWidget {
|
|||||||
child: const Icon(Symbols.edit),
|
child: const Icon(Symbols.edit),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
floatingActionButtonLocation: isWide ? null : TabbedFabLocation(context),
|
|
||||||
body:
|
body:
|
||||||
isWide
|
isWide
|
||||||
? _buildWideBody(
|
? _buildWideBody(
|
||||||
@@ -333,7 +335,7 @@ class ExploreScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
PostFeaturedList(),
|
PostFeaturedList(),
|
||||||
PostComposeCard(
|
PostComposeCard(
|
||||||
onSubmit: (post) {
|
onSubmit: () {
|
||||||
activitiesNotifier.forceRefresh();
|
activitiesNotifier.forceRefresh();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -415,7 +417,6 @@ class ExploreScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
bodyView,
|
bodyView,
|
||||||
SliverGap(getTabbedPadding(context).bottom),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
).padding(horizontal: 8),
|
).padding(horizontal: 8),
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import 'dart:math' as math;
|
|||||||
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:gap/gap.dart';
|
|
||||||
import 'package:go_router/go_router.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/account.dart';
|
import 'package:island/models/account.dart';
|
||||||
@@ -11,8 +10,8 @@ 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/app_scaffold.dart';
|
|
||||||
import 'package:island/widgets/content/markdown.dart';
|
import 'package:island/widgets/content/markdown.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';
|
||||||
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';
|
||||||
@@ -113,8 +112,8 @@ class NotificationListNotifier extends _$NotificationListNotifier
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class NotificationScreen extends HookConsumerWidget {
|
class NotificationSheet extends HookConsumerWidget {
|
||||||
const NotificationScreen({super.key});
|
const NotificationSheet({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@@ -128,19 +127,15 @@ class NotificationScreen extends HookConsumerWidget {
|
|||||||
ref.watch(notificationUnreadCountNotifierProvider.notifier).clear();
|
ref.watch(notificationUnreadCountNotifierProvider.notifier).clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
return AppScaffold(
|
return SheetScaffold(
|
||||||
appBar: AppBar(
|
titleText: 'notifications'.tr(),
|
||||||
leading: const PageBackButton(),
|
|
||||||
title: const Text('notifications').tr(),
|
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: markAllRead,
|
onPressed: markAllRead,
|
||||||
icon: const Icon(Symbols.mark_as_unread),
|
icon: const Icon(Symbols.mark_as_unread),
|
||||||
),
|
),
|
||||||
const Gap(8),
|
|
||||||
],
|
],
|
||||||
),
|
child: PagingHelperView(
|
||||||
body: PagingHelperView(
|
|
||||||
provider: notificationListNotifierProvider,
|
provider: notificationListNotifierProvider,
|
||||||
futureRefreshable: notificationListNotifierProvider.future,
|
futureRefreshable: notificationListNotifierProvider.future,
|
||||||
notifierRefreshable: notificationListNotifierProvider.notifier,
|
notifierRefreshable: notificationListNotifierProvider.notifier,
|
||||||
|
|||||||
@@ -416,6 +416,7 @@ class PollEditorScreen extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
|
isNoBackground: false,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(model.id == null ? 'pollCreate'.tr() : 'pollEdit'.tr()),
|
title: Text(model.id == null ? 'pollCreate'.tr() : 'pollEdit'.tr()),
|
||||||
actions: [
|
actions: [
|
||||||
@@ -433,7 +434,10 @@ class PollEditorScreen extends ConsumerWidget {
|
|||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Form(
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(maxWidth: 640),
|
||||||
|
child:
|
||||||
|
Form(
|
||||||
key: ValueKey(model.id),
|
key: ValueKey(model.id),
|
||||||
child: ListView(
|
child: ListView(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
@@ -441,16 +445,19 @@ class PollEditorScreen extends ConsumerWidget {
|
|||||||
TextFormField(
|
TextFormField(
|
||||||
initialValue: model.title ?? '',
|
initialValue: model.title ?? '',
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'title'.tr(),
|
labelText: 'postTitle'.tr(),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(16)),
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(16),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
maxLength: 256,
|
maxLength: 256,
|
||||||
onChanged: notifier.setTitle,
|
onChanged: notifier.setTitle,
|
||||||
onTapOutside:
|
onTapOutside:
|
||||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
(_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
validator: (v) {
|
validator: (v) {
|
||||||
if (v == null || v.trim().isEmpty) {
|
if (v == null || v.trim().isEmpty) {
|
||||||
return 'pollTitleRequired'.tr();
|
return 'pollTitleRequired'.tr();
|
||||||
@@ -465,14 +472,17 @@ class PollEditorScreen extends ConsumerWidget {
|
|||||||
labelText: 'description'.tr(),
|
labelText: 'description'.tr(),
|
||||||
alignLabelWithHint: true,
|
alignLabelWithHint: true,
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(16)),
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(16),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
maxLines: 3,
|
maxLines: 3,
|
||||||
maxLength: 4096,
|
maxLength: 4096,
|
||||||
onChanged: notifier.setDescription,
|
onChanged: notifier.setDescription,
|
||||||
onTapOutside:
|
onTapOutside:
|
||||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
(_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
),
|
),
|
||||||
const Gap(12),
|
const Gap(12),
|
||||||
_EndDatePicker(
|
_EndDatePicker(
|
||||||
@@ -504,7 +514,8 @@ class PollEditorScreen extends ConsumerWidget {
|
|||||||
.map(
|
.map(
|
||||||
(t) => MenuItemButton(
|
(t) => MenuItemButton(
|
||||||
leadingIcon: Icon(_iconForType(t)),
|
leadingIcon: Icon(_iconForType(t)),
|
||||||
onPressed: () => notifier.addQuestion(t),
|
onPressed:
|
||||||
|
() => notifier.addQuestion(t),
|
||||||
child: Text(_labelForType(t)),
|
child: Text(_labelForType(t)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -552,13 +563,17 @@ class PollEditorScreen extends ConsumerWidget {
|
|||||||
question: q,
|
question: q,
|
||||||
onMoveUp:
|
onMoveUp:
|
||||||
index > 0
|
index > 0
|
||||||
? () => notifier.moveQuestionUp(index)
|
? () =>
|
||||||
|
notifier.moveQuestionUp(index)
|
||||||
: null,
|
: null,
|
||||||
onMoveDown:
|
onMoveDown:
|
||||||
index < model.questions.length - 1
|
index < model.questions.length - 1
|
||||||
? () => notifier.moveQuestionDown(index)
|
? () => notifier.moveQuestionDown(
|
||||||
|
index,
|
||||||
|
)
|
||||||
: null,
|
: null,
|
||||||
onDelete: () => notifier.removeQuestion(index),
|
onDelete:
|
||||||
|
() => notifier.removeQuestion(index),
|
||||||
),
|
),
|
||||||
const Divider(height: 1),
|
const Divider(height: 1),
|
||||||
Padding(
|
Padding(
|
||||||
@@ -576,11 +591,15 @@ class PollEditorScreen extends ConsumerWidget {
|
|||||||
const Gap(96),
|
const Gap(96),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
).center(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Material(
|
Material(
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
child:
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(maxWidth: 640),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
OutlinedButton.icon(
|
OutlinedButton.icon(
|
||||||
@@ -596,7 +615,9 @@ class PollEditorScreen extends ConsumerWidget {
|
|||||||
_submitPoll(context, ref);
|
_submitPoll(context, ref);
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.cloud_upload_outlined),
|
icon: const Icon(Icons.cloud_upload_outlined),
|
||||||
label: Text(model.id == null ? 'create'.tr() : 'update'.tr()),
|
label: Text(
|
||||||
|
model.id == null ? 'create'.tr() : 'update'.tr(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
).padding(
|
).padding(
|
||||||
@@ -604,6 +625,7 @@ class PollEditorScreen extends ConsumerWidget {
|
|||||||
top: 16,
|
top: 16,
|
||||||
bottom: MediaQuery.of(context).padding.bottom + 16,
|
bottom: MediaQuery.of(context).padding.bottom + 16,
|
||||||
),
|
),
|
||||||
|
).center(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -6,21 +6,20 @@ 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/post.dart';
|
import 'package:island/models/post.dart';
|
||||||
import 'package:island/screens/creators/publishers.dart';
|
import 'package:island/screens/creators/publishers_form.dart';
|
||||||
import 'package:island/screens/posts/compose_article.dart';
|
import 'package:island/screens/posts/compose_article.dart';
|
||||||
import 'package:island/services/responsive.dart';
|
import 'package:island/screens/posts/post_detail.dart';
|
||||||
|
import 'package:island/services/compose_storage_db.dart';
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
import 'package:island/widgets/attachment_uploader.dart';
|
|
||||||
import 'package:island/widgets/content/attachment_preview.dart';
|
|
||||||
import 'package:island/widgets/content/cloud_files.dart';
|
import 'package:island/widgets/content/cloud_files.dart';
|
||||||
|
import 'package:island/widgets/post/compose_attachments.dart';
|
||||||
|
import 'package:island/widgets/post/compose_form_fields.dart';
|
||||||
|
import 'package:island/widgets/post/compose_info_banner.dart';
|
||||||
|
import 'package:island/widgets/post/compose_settings_sheet.dart';
|
||||||
import 'package:island/widgets/post/compose_shared.dart';
|
import 'package:island/widgets/post/compose_shared.dart';
|
||||||
|
import 'package:island/widgets/post/compose_toolbar.dart';
|
||||||
import 'package:island/widgets/post/post_item.dart';
|
import 'package:island/widgets/post/post_item.dart';
|
||||||
import 'package:island/widgets/post/publishers_modal.dart';
|
import 'package:island/widgets/post/publishers_modal.dart';
|
||||||
import 'package:island/screens/posts/post_detail.dart';
|
|
||||||
import 'package:island/widgets/post/compose_settings_sheet.dart';
|
|
||||||
import 'package:island/services/compose_storage_db.dart';
|
|
||||||
// DraftManagerSheet is now imported through compose_toolbar.dart
|
|
||||||
import 'package:island/widgets/post/compose_toolbar.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';
|
||||||
|
|
||||||
@@ -92,9 +91,6 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
return ArticleComposeScreen(originalPost: originalPost);
|
return ArticleComposeScreen(originalPost: originalPost);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, continue with regular post compose
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
|
|
||||||
// When editing, preserve the original replied/forwarded post references
|
// When editing, preserve the original replied/forwarded post references
|
||||||
final effectiveRepliedPost = repliedPost ?? originalPost?.repliedPost;
|
final effectiveRepliedPost = repliedPost ?? originalPost?.repliedPost;
|
||||||
final effectiveForwardedPost = forwardedPost ?? originalPost?.forwardedPost;
|
final effectiveForwardedPost = forwardedPost ?? originalPost?.forwardedPost;
|
||||||
@@ -210,109 +206,6 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildWideAttachmentGrid() {
|
|
||||||
return GridView.builder(
|
|
||||||
shrinkWrap: true,
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
|
||||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
|
||||||
crossAxisCount: 2,
|
|
||||||
crossAxisSpacing: 8,
|
|
||||||
mainAxisSpacing: 8,
|
|
||||||
),
|
|
||||||
itemCount: state.attachments.value.length,
|
|
||||||
itemBuilder: (context, idx) {
|
|
||||||
final progressMap = state.attachmentProgress.value;
|
|
||||||
return AttachmentPreview(
|
|
||||||
item: state.attachments.value[idx],
|
|
||||||
progress: progressMap[idx],
|
|
||||||
onRequestUpload: () async {
|
|
||||||
final config = await showModalBottomSheet<AttachmentUploadConfig>(
|
|
||||||
context: context,
|
|
||||||
isScrollControlled: true,
|
|
||||||
builder:
|
|
||||||
(context) => AttachmentUploaderSheet(
|
|
||||||
ref: ref,
|
|
||||||
state: state,
|
|
||||||
index: idx,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (config != null) {
|
|
||||||
await ComposeLogic.uploadAttachment(
|
|
||||||
ref,
|
|
||||||
state,
|
|
||||||
idx,
|
|
||||||
poolId: config.poolId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDelete: () => ComposeLogic.deleteAttachment(ref, state, idx),
|
|
||||||
onUpdate:
|
|
||||||
(value) => ComposeLogic.updateAttachment(state, value, idx),
|
|
||||||
onMove: (delta) {
|
|
||||||
state.attachments.value = ComposeLogic.moveAttachment(
|
|
||||||
state.attachments.value,
|
|
||||||
idx,
|
|
||||||
delta,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget buildNarrowAttachmentList() {
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
for (var idx = 0; idx < state.attachments.value.length; idx++)
|
|
||||||
Container(
|
|
||||||
margin: const EdgeInsets.only(bottom: 8),
|
|
||||||
child: () {
|
|
||||||
final progressMap = state.attachmentProgress.value;
|
|
||||||
return AttachmentPreview(
|
|
||||||
item: state.attachments.value[idx],
|
|
||||||
progress: progressMap[idx],
|
|
||||||
onRequestUpload: () async {
|
|
||||||
final config =
|
|
||||||
await showModalBottomSheet<AttachmentUploadConfig>(
|
|
||||||
context: context,
|
|
||||||
isScrollControlled: true,
|
|
||||||
builder:
|
|
||||||
(context) => AttachmentUploaderSheet(
|
|
||||||
ref: ref,
|
|
||||||
state: state,
|
|
||||||
index: idx,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (config != null) {
|
|
||||||
await ComposeLogic.uploadAttachment(
|
|
||||||
ref,
|
|
||||||
state,
|
|
||||||
idx,
|
|
||||||
poolId: config.poolId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDelete:
|
|
||||||
() => ComposeLogic.deleteAttachment(ref, state, idx),
|
|
||||||
onUpdate:
|
|
||||||
(value) =>
|
|
||||||
ComposeLogic.updateAttachment(state, value, idx),
|
|
||||||
onMove: (delta) {
|
|
||||||
state.attachments.value = ComposeLogic.moveAttachment(
|
|
||||||
state.attachments.value,
|
|
||||||
idx,
|
|
||||||
delta,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build UI
|
|
||||||
return PopScope(
|
return PopScope(
|
||||||
onPopInvoked: (_) {
|
onPopInvoked: (_) {
|
||||||
if (originalPost == null) {
|
if (originalPost == null) {
|
||||||
@@ -362,7 +255,57 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Reply/Forward info section
|
// Reply/Forward info section
|
||||||
_buildInfoBanner(context),
|
ComposeInfoBanner(
|
||||||
|
originalPost: originalPost,
|
||||||
|
replyingTo: repliedPost,
|
||||||
|
forwardingTo: forwardedPost,
|
||||||
|
onReferencePostTap: (context, post) {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
builder:
|
||||||
|
(context) => DraggableScrollableSheet(
|
||||||
|
initialChildSize: 0.7,
|
||||||
|
maxChildSize: 0.9,
|
||||||
|
minChildSize: 0.5,
|
||||||
|
builder:
|
||||||
|
(context, scrollController) => Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color:
|
||||||
|
Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
borderRadius: const BorderRadius.vertical(
|
||||||
|
top: Radius.circular(16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 40,
|
||||||
|
height: 4,
|
||||||
|
margin: const EdgeInsets.symmetric(
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color:
|
||||||
|
Theme.of(context).colorScheme.outline,
|
||||||
|
borderRadius: BorderRadius.circular(2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
controller: scrollController,
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: PostItem(item: post),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
// Main content area
|
// Main content area
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -414,78 +357,27 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
TextField(
|
ComposeFormFields(
|
||||||
controller: state.titleController,
|
state: state,
|
||||||
decoration: InputDecoration(
|
showPublisherAvatar: false,
|
||||||
hintText: 'postTitle'.tr(),
|
onPublisherTap: () {
|
||||||
border: InputBorder.none,
|
showModalBottomSheet(
|
||||||
isCollapsed: true,
|
isScrollControlled: true,
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
context: context,
|
||||||
vertical: 8,
|
builder:
|
||||||
horizontal: 8,
|
(context) => const PublisherModal(),
|
||||||
),
|
).then((value) {
|
||||||
),
|
if (value != null) {
|
||||||
style: theme.textTheme.titleMedium,
|
state.currentPublisher.value = value;
|
||||||
onTapOutside:
|
}
|
||||||
(_) =>
|
});
|
||||||
FocusManager.instance.primaryFocus
|
|
||||||
?.unfocus(),
|
|
||||||
),
|
|
||||||
TextField(
|
|
||||||
controller: state.descriptionController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: 'postDescription'.tr(),
|
|
||||||
border: InputBorder.none,
|
|
||||||
isCollapsed: true,
|
|
||||||
contentPadding: const EdgeInsets.fromLTRB(
|
|
||||||
8,
|
|
||||||
4,
|
|
||||||
8,
|
|
||||||
12,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
style: theme.textTheme.bodyMedium,
|
|
||||||
minLines: 1,
|
|
||||||
maxLines: 3,
|
|
||||||
onTapOutside:
|
|
||||||
(_) =>
|
|
||||||
FocusManager.instance.primaryFocus
|
|
||||||
?.unfocus(),
|
|
||||||
),
|
|
||||||
// Content field with borderless design
|
|
||||||
TextField(
|
|
||||||
controller: state.contentController,
|
|
||||||
style: theme.textTheme.bodyMedium,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
border: InputBorder.none,
|
|
||||||
hintText: 'postContent'.tr(),
|
|
||||||
isCollapsed: true,
|
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
|
||||||
vertical: 8,
|
|
||||||
horizontal: 8,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
maxLines: null,
|
|
||||||
onTapOutside:
|
|
||||||
(_) =>
|
|
||||||
FocusManager.instance.primaryFocus
|
|
||||||
?.unfocus(),
|
|
||||||
),
|
|
||||||
|
|
||||||
const Gap(8),
|
|
||||||
|
|
||||||
// Attachments preview
|
|
||||||
if (state.attachments.value.isNotEmpty)
|
|
||||||
LayoutBuilder(
|
|
||||||
builder: (context, constraints) {
|
|
||||||
final isWide = isWideScreen(context);
|
|
||||||
return isWide
|
|
||||||
? buildWideAttachmentGrid()
|
|
||||||
: buildNarrowAttachmentList();
|
|
||||||
},
|
},
|
||||||
)
|
),
|
||||||
else
|
const Gap(8),
|
||||||
const SizedBox.shrink(),
|
ComposeAttachments(
|
||||||
|
state: state,
|
||||||
|
isCompact: false,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -503,262 +395,4 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildInfoBanner(BuildContext context) {
|
|
||||||
// When editing, preserve the original replied/forwarded post references
|
|
||||||
final effectiveRepliedPost =
|
|
||||||
initialState?.replyingTo ?? originalPost?.repliedPost;
|
|
||||||
final effectiveForwardedPost =
|
|
||||||
initialState?.forwardingTo ?? originalPost?.forwardedPost;
|
|
||||||
|
|
||||||
// Show editing banner when editing a post
|
|
||||||
if (originalPost != null) {
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
width: double.infinity,
|
|
||||||
color: Theme.of(context).colorScheme.primaryContainer,
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Symbols.edit,
|
|
||||||
size: 16,
|
|
||||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
|
||||||
),
|
|
||||||
const Gap(8),
|
|
||||||
Text(
|
|
||||||
'postEditing'.tr(),
|
|
||||||
style: Theme.of(context).textTheme.labelMedium?.copyWith(
|
|
||||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).padding(horizontal: 16, vertical: 8),
|
|
||||||
),
|
|
||||||
// Show reply/forward banners below editing banner if they exist
|
|
||||||
if (effectiveRepliedPost != null)
|
|
||||||
Container(
|
|
||||||
width: double.infinity,
|
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Icon(Symbols.reply, size: 16),
|
|
||||||
const Gap(4),
|
|
||||||
Text(
|
|
||||||
'postReplyingTo'.tr(),
|
|
||||||
style: Theme.of(context).textTheme.labelMedium,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const Gap(8),
|
|
||||||
_buildCompactReferencePost(context, effectiveRepliedPost),
|
|
||||||
],
|
|
||||||
).padding(all: 16),
|
|
||||||
),
|
|
||||||
if (effectiveForwardedPost != null)
|
|
||||||
Container(
|
|
||||||
width: double.infinity,
|
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Icon(Symbols.forward, size: 16),
|
|
||||||
const Gap(4),
|
|
||||||
Text(
|
|
||||||
'postForwardingTo'.tr(),
|
|
||||||
style: Theme.of(context).textTheme.labelMedium,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const Gap(8),
|
|
||||||
_buildCompactReferencePost(context, effectiveForwardedPost),
|
|
||||||
],
|
|
||||||
).padding(all: 16),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show banner for replies (including when editing a reply)
|
|
||||||
if (effectiveRepliedPost != null) {
|
|
||||||
return Container(
|
|
||||||
width: double.infinity,
|
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Icon(Symbols.reply, size: 16),
|
|
||||||
const Gap(4),
|
|
||||||
Text(
|
|
||||||
'postReplyingTo'.tr(),
|
|
||||||
style: Theme.of(context).textTheme.labelMedium,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const Gap(8),
|
|
||||||
_buildCompactReferencePost(context, effectiveRepliedPost),
|
|
||||||
],
|
|
||||||
).padding(all: 16),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show banner for forwards (including when editing a forward)
|
|
||||||
if (effectiveForwardedPost != null) {
|
|
||||||
return Container(
|
|
||||||
width: double.infinity,
|
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Icon(Symbols.forward, size: 16),
|
|
||||||
const Gap(4),
|
|
||||||
Text(
|
|
||||||
'postForwardingTo'.tr(),
|
|
||||||
style: Theme.of(context).textTheme.labelMedium,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const Gap(8),
|
|
||||||
_buildCompactReferencePost(context, effectiveForwardedPost),
|
|
||||||
],
|
|
||||||
).padding(all: 16),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildCompactReferencePost(BuildContext context, SnPost post) {
|
|
||||||
return GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
showModalBottomSheet(
|
|
||||||
context: context,
|
|
||||||
isScrollControlled: true,
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
builder:
|
|
||||||
(context) => DraggableScrollableSheet(
|
|
||||||
initialChildSize: 0.7,
|
|
||||||
maxChildSize: 0.9,
|
|
||||||
minChildSize: 0.5,
|
|
||||||
builder:
|
|
||||||
(context, scrollController) => Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).scaffoldBackgroundColor,
|
|
||||||
borderRadius: const BorderRadius.vertical(
|
|
||||||
top: Radius.circular(16),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
width: 40,
|
|
||||||
height: 4,
|
|
||||||
margin: const EdgeInsets.symmetric(vertical: 8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).colorScheme.outline,
|
|
||||||
borderRadius: BorderRadius.circular(2),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
controller: scrollController,
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: PostItem(item: post),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
border: Border.all(
|
|
||||||
color: Theme.of(context).colorScheme.outline.withOpacity(0.3),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
ProfilePictureWidget(
|
|
||||||
fileId: post.publisher.picture?.id,
|
|
||||||
radius: 16,
|
|
||||||
),
|
|
||||||
const Gap(8),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
post.publisher.nick,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 14,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (post.title?.isNotEmpty ?? false)
|
|
||||||
Text(
|
|
||||||
post.title!,
|
|
||||||
style: TextStyle(
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
fontSize: 13,
|
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
|
||||||
),
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
if (post.content?.isNotEmpty ?? false)
|
|
||||||
Text(
|
|
||||||
post.content!,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
maxLines: 2,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
if (post.attachments.isNotEmpty)
|
|
||||||
Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Symbols.attach_file,
|
|
||||||
size: 12,
|
|
||||||
color: Theme.of(context).colorScheme.secondary,
|
|
||||||
),
|
|
||||||
const Gap(4),
|
|
||||||
Text(
|
|
||||||
'postHasAttachments'.plural(post.attachments.length),
|
|
||||||
style: TextStyle(
|
|
||||||
color: Theme.of(context).colorScheme.secondary,
|
|
||||||
fontSize: 11,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Icon(
|
|
||||||
Symbols.open_in_full,
|
|
||||||
size: 16,
|
|
||||||
color: Theme.of(context).colorScheme.outline,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,21 +7,20 @@ 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/post.dart';
|
import 'package:island/models/post.dart';
|
||||||
|
import 'package:island/screens/creators/publishers_form.dart';
|
||||||
import 'package:island/screens/creators/publishers.dart';
|
import 'package:island/screens/posts/post_detail.dart';
|
||||||
|
import 'package:island/services/compose_storage_db.dart';
|
||||||
import 'package:island/services/responsive.dart';
|
import 'package:island/services/responsive.dart';
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
import 'package:island/widgets/attachment_uploader.dart';
|
import 'package:island/widgets/attachment_uploader.dart';
|
||||||
import 'package:island/screens/posts/post_detail.dart';
|
|
||||||
import 'package:island/widgets/content/attachment_preview.dart';
|
import 'package:island/widgets/content/attachment_preview.dart';
|
||||||
import 'package:island/widgets/content/cloud_files.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/post/compose_form_fields.dart';
|
||||||
import 'package:island/widgets/post/compose_shared.dart';
|
import 'package:island/widgets/post/compose_shared.dart';
|
||||||
import 'package:island/widgets/post/compose_settings_sheet.dart';
|
import 'package:island/widgets/post/compose_settings_sheet.dart';
|
||||||
import 'package:island/services/compose_storage_db.dart';
|
|
||||||
import 'package:island/widgets/post/compose_toolbar.dart';
|
import 'package:island/widgets/post/compose_toolbar.dart';
|
||||||
import 'package:island/widgets/post/publishers_modal.dart';
|
import 'package:island/widgets/post/publishers_modal.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';
|
||||||
|
|
||||||
@@ -233,64 +232,20 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
TextField(
|
ComposeFormFields(
|
||||||
controller: state.titleController,
|
state: state,
|
||||||
decoration: InputDecoration(
|
showPublisherAvatar: false,
|
||||||
hintText: 'postTitle'.tr(),
|
onPublisherTap: () {
|
||||||
border: InputBorder.none,
|
showModalBottomSheet(
|
||||||
isCollapsed: true,
|
isScrollControlled: true,
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
context: context,
|
||||||
vertical: 8,
|
builder: (context) => const PublisherModal(),
|
||||||
horizontal: 8,
|
).then((value) {
|
||||||
),
|
if (value != null) {
|
||||||
),
|
state.currentPublisher.value = value;
|
||||||
style: theme.textTheme.titleMedium,
|
}
|
||||||
onTapOutside:
|
});
|
||||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
},
|
||||||
),
|
|
||||||
TextField(
|
|
||||||
controller: state.descriptionController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: 'postDescription'.tr(),
|
|
||||||
border: InputBorder.none,
|
|
||||||
isCollapsed: true,
|
|
||||||
contentPadding: const EdgeInsets.fromLTRB(8, 4, 8, 12),
|
|
||||||
),
|
|
||||||
style: theme.textTheme.bodyMedium,
|
|
||||||
minLines: 1,
|
|
||||||
maxLines: 3,
|
|
||||||
onTapOutside:
|
|
||||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: KeyboardListener(
|
|
||||||
focusNode: FocusNode(),
|
|
||||||
onKeyEvent:
|
|
||||||
(event) => ComposeLogic.handleKeyPress(
|
|
||||||
event,
|
|
||||||
state,
|
|
||||||
ref,
|
|
||||||
context,
|
|
||||||
originalPost: originalPost,
|
|
||||||
),
|
|
||||||
child: TextField(
|
|
||||||
controller: state.contentController,
|
|
||||||
style: theme.textTheme.bodyMedium,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
border: InputBorder.none,
|
|
||||||
hintText: 'postContent'.tr(),
|
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
|
||||||
vertical: 16,
|
|
||||||
horizontal: 8,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
maxLines: null,
|
|
||||||
expands: true,
|
|
||||||
textAlignVertical: TextAlignVertical.top,
|
|
||||||
onTapOutside:
|
|
||||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
|
||||||
// Attachments preview
|
// Attachments preview
|
||||||
|
|||||||
@@ -490,14 +490,13 @@ class PostDetailScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
if (user.value != null)
|
if (user.value != null)
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 0,
|
bottom: 16 + MediaQuery.of(context).padding.bottom,
|
||||||
left: 0,
|
left: 16,
|
||||||
right: 0,
|
right: 16,
|
||||||
child: Material(
|
child:
|
||||||
elevation: 2,
|
ConstrainedBox(
|
||||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
constraints: BoxConstraints(maxWidth: 660),
|
||||||
child: postState
|
child: postState.when(
|
||||||
.when(
|
|
||||||
data:
|
data:
|
||||||
(post) => PostQuickReply(
|
(post) => PostQuickReply(
|
||||||
parent: post!,
|
parent: post!,
|
||||||
@@ -509,13 +508,8 @@ class PostDetailScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
loading: () => const SizedBox.shrink(),
|
loading: () => const SizedBox.shrink(),
|
||||||
error: (_, _) => const SizedBox.shrink(),
|
error: (_, _) => const SizedBox.shrink(),
|
||||||
)
|
|
||||||
.padding(
|
|
||||||
bottom: MediaQuery.of(context).padding.bottom + 8,
|
|
||||||
top: 8,
|
|
||||||
horizontal: 16,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
).center(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:island/models/post.dart';
|
import 'package:island/models/post.dart';
|
||||||
import 'package:island/models/publisher.dart';
|
import 'package:island/models/publisher.dart';
|
||||||
import 'package:island/models/account.dart';
|
import 'package:island/models/account.dart';
|
||||||
|
import 'package:island/models/heatmap.dart';
|
||||||
import 'package:island/pods/config.dart';
|
import 'package:island/pods/config.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
import 'package:island/services/color.dart';
|
import 'package:island/services/color.dart';
|
||||||
@@ -20,6 +21,7 @@ import 'package:island/widgets/app_scaffold.dart';
|
|||||||
import 'package:island/widgets/content/cloud_files.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/post/post_list.dart';
|
import 'package:island/widgets/post/post_list.dart';
|
||||||
|
import 'package:island/widgets/activity_heatmap.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:island/services/color_extraction.dart';
|
import 'package:island/services/color_extraction.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
@@ -222,6 +224,32 @@ class _PublisherBioWidget extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _PublisherHeatmapWidget extends StatelessWidget {
|
||||||
|
final AsyncValue<SnHeatmap?> heatmap;
|
||||||
|
final bool forceDense;
|
||||||
|
|
||||||
|
const _PublisherHeatmapWidget({
|
||||||
|
required this.heatmap,
|
||||||
|
this.forceDense = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return heatmap.when(
|
||||||
|
data:
|
||||||
|
(data) =>
|
||||||
|
data != null
|
||||||
|
? ActivityHeatmapWidget(
|
||||||
|
heatmap: data,
|
||||||
|
forceDense: forceDense,
|
||||||
|
).padding(horizontal: 8)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
loading: () => const SizedBox.shrink(),
|
||||||
|
error: (_, _) => const SizedBox.shrink(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _PublisherCategoryTabWidget extends StatelessWidget {
|
class _PublisherCategoryTabWidget extends StatelessWidget {
|
||||||
final TabController categoryTabController;
|
final TabController categoryTabController;
|
||||||
|
|
||||||
@@ -292,6 +320,13 @@ Future<Color?> publisherAppbarForcegroundColor(Ref ref, String pubName) async {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
Future<SnHeatmap?> publisherHeatmap(Ref ref, String uname) async {
|
||||||
|
final apiClient = ref.watch(apiClientProvider);
|
||||||
|
final resp = await apiClient.get('/sphere/publishers/$uname/heatmap');
|
||||||
|
return SnHeatmap.fromJson(resp.data);
|
||||||
|
}
|
||||||
|
|
||||||
class PublisherProfileScreen extends HookConsumerWidget {
|
class PublisherProfileScreen extends HookConsumerWidget {
|
||||||
final String name;
|
final String name;
|
||||||
const PublisherProfileScreen({super.key, required this.name});
|
const PublisherProfileScreen({super.key, required this.name});
|
||||||
@@ -301,6 +336,7 @@ class PublisherProfileScreen extends HookConsumerWidget {
|
|||||||
final publisher = ref.watch(publisherProvider(name));
|
final publisher = ref.watch(publisherProvider(name));
|
||||||
final badges = ref.watch(publisherBadgesProvider(name));
|
final badges = ref.watch(publisherBadgesProvider(name));
|
||||||
final subStatus = ref.watch(publisherSubscriptionStatusProvider(name));
|
final subStatus = ref.watch(publisherSubscriptionStatusProvider(name));
|
||||||
|
final heatmap = ref.watch(publisherHeatmapProvider(name));
|
||||||
final appbarColor = ref.watch(
|
final appbarColor = ref.watch(
|
||||||
publisherAppbarForcegroundColorProvider(name),
|
publisherAppbarForcegroundColorProvider(name),
|
||||||
);
|
);
|
||||||
@@ -446,6 +482,10 @@ class PublisherProfileScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
_PublisherVerificationWidget(data: data),
|
_PublisherVerificationWidget(data: data),
|
||||||
_PublisherBioWidget(data: data),
|
_PublisherBioWidget(data: data),
|
||||||
|
_PublisherHeatmapWidget(
|
||||||
|
heatmap: heatmap,
|
||||||
|
forceDense: true,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -517,6 +557,9 @@ class PublisherProfileScreen extends HookConsumerWidget {
|
|||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: _PublisherBioWidget(data: data),
|
child: _PublisherBioWidget(data: data),
|
||||||
),
|
),
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: _PublisherHeatmapWidget(heatmap: heatmap),
|
||||||
|
),
|
||||||
SliverPostList(pubName: name, pinned: true),
|
SliverPostList(pubName: name, pinned: true),
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: _PublisherCategoryTabWidget(
|
child: _PublisherCategoryTabWidget(
|
||||||
|
|||||||
@@ -530,5 +530,126 @@ class _PublisherAppbarForcegroundColorProviderElement
|
|||||||
(origin as PublisherAppbarForcegroundColorProvider).pubName;
|
(origin as PublisherAppbarForcegroundColorProvider).pubName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _$publisherHeatmapHash() => r'86db275ce3861a2855b5ec35fbfef85fc47b23a6';
|
||||||
|
|
||||||
|
/// See also [publisherHeatmap].
|
||||||
|
@ProviderFor(publisherHeatmap)
|
||||||
|
const publisherHeatmapProvider = PublisherHeatmapFamily();
|
||||||
|
|
||||||
|
/// See also [publisherHeatmap].
|
||||||
|
class PublisherHeatmapFamily extends Family<AsyncValue<SnHeatmap?>> {
|
||||||
|
/// See also [publisherHeatmap].
|
||||||
|
const PublisherHeatmapFamily();
|
||||||
|
|
||||||
|
/// See also [publisherHeatmap].
|
||||||
|
PublisherHeatmapProvider call(String uname) {
|
||||||
|
return PublisherHeatmapProvider(uname);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
PublisherHeatmapProvider getProviderOverride(
|
||||||
|
covariant PublisherHeatmapProvider provider,
|
||||||
|
) {
|
||||||
|
return call(provider.uname);
|
||||||
|
}
|
||||||
|
|
||||||
|
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'publisherHeatmapProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [publisherHeatmap].
|
||||||
|
class PublisherHeatmapProvider extends AutoDisposeFutureProvider<SnHeatmap?> {
|
||||||
|
/// See also [publisherHeatmap].
|
||||||
|
PublisherHeatmapProvider(String uname)
|
||||||
|
: this._internal(
|
||||||
|
(ref) => publisherHeatmap(ref as PublisherHeatmapRef, uname),
|
||||||
|
from: publisherHeatmapProvider,
|
||||||
|
name: r'publisherHeatmapProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$publisherHeatmapHash,
|
||||||
|
dependencies: PublisherHeatmapFamily._dependencies,
|
||||||
|
allTransitiveDependencies:
|
||||||
|
PublisherHeatmapFamily._allTransitiveDependencies,
|
||||||
|
uname: uname,
|
||||||
|
);
|
||||||
|
|
||||||
|
PublisherHeatmapProvider._internal(
|
||||||
|
super._createNotifier, {
|
||||||
|
required super.name,
|
||||||
|
required super.dependencies,
|
||||||
|
required super.allTransitiveDependencies,
|
||||||
|
required super.debugGetCreateSourceHash,
|
||||||
|
required super.from,
|
||||||
|
required this.uname,
|
||||||
|
}) : super.internal();
|
||||||
|
|
||||||
|
final String uname;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Override overrideWith(
|
||||||
|
FutureOr<SnHeatmap?> Function(PublisherHeatmapRef provider) create,
|
||||||
|
) {
|
||||||
|
return ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
override: PublisherHeatmapProvider._internal(
|
||||||
|
(ref) => create(ref as PublisherHeatmapRef),
|
||||||
|
from: from,
|
||||||
|
name: null,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
debugGetCreateSourceHash: null,
|
||||||
|
uname: uname,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AutoDisposeFutureProviderElement<SnHeatmap?> createElement() {
|
||||||
|
return _PublisherHeatmapProviderElement(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is PublisherHeatmapProvider && other.uname == uname;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, uname.hashCode);
|
||||||
|
|
||||||
|
return _SystemHash.finish(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
mixin PublisherHeatmapRef on AutoDisposeFutureProviderRef<SnHeatmap?> {
|
||||||
|
/// The parameter `uname` of this provider.
|
||||||
|
String get uname;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PublisherHeatmapProviderElement
|
||||||
|
extends AutoDisposeFutureProviderElement<SnHeatmap?>
|
||||||
|
with PublisherHeatmapRef {
|
||||||
|
_PublisherHeatmapProviderElement(super.provider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get uname => (origin as PublisherHeatmapProvider).uname;
|
||||||
|
}
|
||||||
|
|
||||||
// ignore_for_file: type=lint
|
// 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
|
// 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
|
||||||
|
|||||||
276
lib/screens/realm/realm_form.dart
Normal file
276
lib/screens/realm/realm_form.dart
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
import 'package:croppy/croppy.dart' show CropAspectRatio;
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
import 'package:island/models/file.dart';
|
||||||
|
import 'package:island/models/realm.dart';
|
||||||
|
import 'package:island/pods/network.dart';
|
||||||
|
import 'package:island/screens/realm/realms.dart';
|
||||||
|
import 'package:island/services/file.dart';
|
||||||
|
import 'package:island/services/file_uploader.dart';
|
||||||
|
import 'package:island/widgets/alert.dart';
|
||||||
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
|
import 'package:island/widgets/content/cloud_files.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
|
||||||
|
class NewRealmScreen extends StatelessWidget {
|
||||||
|
const NewRealmScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const EditRealmScreen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EditRealmScreen extends HookConsumerWidget {
|
||||||
|
final String? slug;
|
||||||
|
const EditRealmScreen({super.key, this.slug});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final submitting = useState(false);
|
||||||
|
|
||||||
|
final picture = useState<SnCloudFile?>(null);
|
||||||
|
final background = useState<SnCloudFile?>(null);
|
||||||
|
final isPublic = useState(true);
|
||||||
|
final isCommunity = useState(false);
|
||||||
|
|
||||||
|
final slugController = useTextEditingController();
|
||||||
|
final nameController = useTextEditingController();
|
||||||
|
final descriptionController = useTextEditingController();
|
||||||
|
|
||||||
|
final formKey = useMemoized(GlobalKey<FormState>.new, const []);
|
||||||
|
|
||||||
|
final realm = ref.watch(realmProvider(slug));
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
if (realm.value != null) {
|
||||||
|
picture.value = realm.value!.picture;
|
||||||
|
background.value = realm.value!.background;
|
||||||
|
slugController.text = realm.value!.slug;
|
||||||
|
nameController.text = realm.value!.name;
|
||||||
|
descriptionController.text = realm.value!.description;
|
||||||
|
isPublic.value = realm.value!.isPublic;
|
||||||
|
isCommunity.value = realm.value!.isCommunity;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [realm]);
|
||||||
|
|
||||||
|
void setPicture(String position) async {
|
||||||
|
showLoadingModal(context);
|
||||||
|
var result = await ref
|
||||||
|
.read(imagePickerProvider)
|
||||||
|
.pickImage(source: ImageSource.gallery);
|
||||||
|
if (result == null) {
|
||||||
|
if (context.mounted) hideLoadingModal(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!context.mounted) return;
|
||||||
|
hideLoadingModal(context);
|
||||||
|
result = await cropImage(
|
||||||
|
context,
|
||||||
|
image: result,
|
||||||
|
allowedAspectRatios: [
|
||||||
|
if (position == 'background')
|
||||||
|
const CropAspectRatio(height: 7, width: 16)
|
||||||
|
else
|
||||||
|
const CropAspectRatio(height: 1, width: 1),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
if (result == null) {
|
||||||
|
if (context.mounted) hideLoadingModal(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!context.mounted) return;
|
||||||
|
showLoadingModal(context);
|
||||||
|
submitting.value = true;
|
||||||
|
try {
|
||||||
|
final cloudFile =
|
||||||
|
await FileUploader.createCloudFile(
|
||||||
|
client: ref.read(apiClientProvider),
|
||||||
|
fileData: UniversalFile(
|
||||||
|
data: result,
|
||||||
|
type: UniversalFileType.image,
|
||||||
|
),
|
||||||
|
).future;
|
||||||
|
if (cloudFile == null) {
|
||||||
|
throw ArgumentError('Failed to upload the file...');
|
||||||
|
}
|
||||||
|
switch (position) {
|
||||||
|
case 'picture':
|
||||||
|
picture.value = cloudFile;
|
||||||
|
case 'background':
|
||||||
|
background.value = cloudFile;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
showErrorAlert(err);
|
||||||
|
} finally {
|
||||||
|
if (context.mounted) hideLoadingModal(context);
|
||||||
|
submitting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> performAction() async {
|
||||||
|
if (!formKey.currentState!.validate()) return;
|
||||||
|
|
||||||
|
submitting.value = true;
|
||||||
|
try {
|
||||||
|
final client = ref.watch(apiClientProvider);
|
||||||
|
final resp = await client.request(
|
||||||
|
'/sphere${slug == null ? '/realms' : '/realms/$slug'}',
|
||||||
|
data: {
|
||||||
|
'slug': slugController.text,
|
||||||
|
'name': nameController.text,
|
||||||
|
'description': descriptionController.text,
|
||||||
|
'background_id': background.value?.id,
|
||||||
|
'picture_id': picture.value?.id,
|
||||||
|
'is_public': isPublic.value,
|
||||||
|
'is_community': isCommunity.value,
|
||||||
|
},
|
||||||
|
options: Options(method: slug == null ? 'POST' : 'PATCH'),
|
||||||
|
);
|
||||||
|
if (context.mounted) {
|
||||||
|
context.pop(SnRealm.fromJson(resp.data));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
showErrorAlert(err);
|
||||||
|
} finally {
|
||||||
|
submitting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return AppScaffold(
|
||||||
|
isNoBackground: false,
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(slug == null ? 'createRealm'.tr() : 'editRealm'.tr()),
|
||||||
|
leading: const PageBackButton(),
|
||||||
|
),
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
AspectRatio(
|
||||||
|
aspectRatio: 16 / 7,
|
||||||
|
child: Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
GestureDetector(
|
||||||
|
child: Container(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
|
child:
|
||||||
|
background.value != null
|
||||||
|
? CloudFileWidget(
|
||||||
|
item: background.value!,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
setPicture('background');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
left: 20,
|
||||||
|
bottom: -32,
|
||||||
|
child: GestureDetector(
|
||||||
|
child: ProfilePictureWidget(
|
||||||
|
fileId: picture.value?.id,
|
||||||
|
radius: 40,
|
||||||
|
fallbackIcon: Symbols.group,
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
setPicture('picture');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
).padding(bottom: 32),
|
||||||
|
Form(
|
||||||
|
key: formKey,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
TextFormField(
|
||||||
|
controller: slugController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'slug'.tr(),
|
||||||
|
helperText: 'slugHint'.tr(),
|
||||||
|
),
|
||||||
|
onTapOutside:
|
||||||
|
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
controller: nameController,
|
||||||
|
decoration: InputDecoration(labelText: 'name'.tr()),
|
||||||
|
onTapOutside:
|
||||||
|
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
controller: descriptionController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'description'.tr(),
|
||||||
|
alignLabelWithHint: true,
|
||||||
|
),
|
||||||
|
minLines: 3,
|
||||||
|
maxLines: null,
|
||||||
|
onTapOutside:
|
||||||
|
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Card(
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
CheckboxListTile(
|
||||||
|
secondary: const Icon(Symbols.public),
|
||||||
|
title: Text('publicRealm').tr(),
|
||||||
|
subtitle: Text('publicRealmDescription').tr(),
|
||||||
|
value: isPublic.value,
|
||||||
|
onChanged: (value) {
|
||||||
|
isPublic.value = value ?? true;
|
||||||
|
},
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
CheckboxListTile(
|
||||||
|
secondary: const Icon(Symbols.travel_explore),
|
||||||
|
title: Text('communityRealm').tr(),
|
||||||
|
subtitle: Text('communityRealmDescription').tr(),
|
||||||
|
value: isCommunity.value,
|
||||||
|
onChanged: (value) {
|
||||||
|
isCommunity.value = value ?? false;
|
||||||
|
},
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: TextButton.icon(
|
||||||
|
onPressed: submitting.value ? null : performAction,
|
||||||
|
label: Text('saveChanges'.tr()),
|
||||||
|
icon: const Icon(Symbols.save),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(all: 24),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,24 +1,15 @@
|
|||||||
import 'package:croppy/croppy.dart' show CropAspectRatio;
|
|
||||||
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:go_router/go_router.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:image_picker/image_picker.dart';
|
|
||||||
import 'package:island/models/file.dart';
|
|
||||||
import 'package:island/models/realm.dart';
|
import 'package:island/models/realm.dart';
|
||||||
import 'package:island/pods/config.dart';
|
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
import 'package:island/services/file.dart';
|
|
||||||
import 'package:island/services/responsive.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/cloud_files.dart';
|
import 'package:island/widgets/content/cloud_files.dart';
|
||||||
import 'package:island/widgets/content/sheet.dart';
|
import 'package:island/widgets/content/sheet.dart';
|
||||||
import 'package:island/widgets/response.dart';
|
import 'package:island/widgets/response.dart';
|
||||||
import 'package:island/screens/tabs.dart';
|
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.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';
|
||||||
@@ -34,6 +25,14 @@ Future<List<SnRealm>> realmsJoined(Ref ref) async {
|
|||||||
return resp.data.map((e) => SnRealm.fromJson(e)).cast<SnRealm>().toList();
|
return resp.data.map((e) => SnRealm.fromJson(e)).cast<SnRealm>().toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
Future<SnRealm?> realm(Ref ref, String? identifier) async {
|
||||||
|
if (identifier == null) return null;
|
||||||
|
final client = ref.watch(apiClientProvider);
|
||||||
|
final resp = await client.get('/sphere/realms/$identifier');
|
||||||
|
return SnRealm.fromJson(resp.data);
|
||||||
|
}
|
||||||
|
|
||||||
class RealmListScreen extends HookConsumerWidget {
|
class RealmListScreen extends HookConsumerWidget {
|
||||||
const RealmListScreen({super.key});
|
const RealmListScreen({super.key});
|
||||||
|
|
||||||
@@ -90,7 +89,6 @@ class RealmListScreen extends HookConsumerWidget {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
floatingActionButtonLocation: TabbedFabLocation(context),
|
|
||||||
body: ExtendedRefreshIndicator(
|
body: ExtendedRefreshIndicator(
|
||||||
child: realms.when(
|
child: realms.when(
|
||||||
data:
|
data:
|
||||||
@@ -100,7 +98,7 @@ class RealmListScreen extends HookConsumerWidget {
|
|||||||
child: ListView.separated(
|
child: ListView.separated(
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
top: 8,
|
top: 8,
|
||||||
bottom: getTabbedPadding(context).bottom + 8,
|
bottom: MediaQuery.of(context).padding.bottom + 8,
|
||||||
),
|
),
|
||||||
itemCount: value.length,
|
itemCount: value.length,
|
||||||
itemBuilder: (context, item) {
|
itemBuilder: (context, item) {
|
||||||
@@ -127,277 +125,6 @@ class RealmListScreen extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@riverpod
|
|
||||||
Future<SnRealm?> realm(Ref ref, String? identifier) async {
|
|
||||||
if (identifier == null) return null;
|
|
||||||
final client = ref.watch(apiClientProvider);
|
|
||||||
final resp = await client.get('/sphere/realms/$identifier');
|
|
||||||
return SnRealm.fromJson(resp.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
class NewRealmScreen extends StatelessWidget {
|
|
||||||
const NewRealmScreen({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return const EditRealmScreen();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class EditRealmScreen extends HookConsumerWidget {
|
|
||||||
final String? slug;
|
|
||||||
const EditRealmScreen({super.key, this.slug});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final submitting = useState(false);
|
|
||||||
|
|
||||||
final picture = useState<SnCloudFile?>(null);
|
|
||||||
final background = useState<SnCloudFile?>(null);
|
|
||||||
final isPublic = useState(true);
|
|
||||||
final isCommunity = useState(false);
|
|
||||||
|
|
||||||
final slugController = useTextEditingController();
|
|
||||||
final nameController = useTextEditingController();
|
|
||||||
final descriptionController = useTextEditingController();
|
|
||||||
|
|
||||||
final formKey = useMemoized(GlobalKey<FormState>.new, const []);
|
|
||||||
|
|
||||||
final realm = ref.watch(realmProvider(slug));
|
|
||||||
|
|
||||||
useEffect(() {
|
|
||||||
if (realm.value != null) {
|
|
||||||
picture.value = realm.value!.picture;
|
|
||||||
background.value = realm.value!.background;
|
|
||||||
slugController.text = realm.value!.slug;
|
|
||||||
nameController.text = realm.value!.name;
|
|
||||||
descriptionController.text = realm.value!.description;
|
|
||||||
isPublic.value = realm.value!.isPublic;
|
|
||||||
isCommunity.value = realm.value!.isCommunity;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}, [realm]);
|
|
||||||
|
|
||||||
void setPicture(String position) async {
|
|
||||||
showLoadingModal(context);
|
|
||||||
var result = await ref
|
|
||||||
.read(imagePickerProvider)
|
|
||||||
.pickImage(source: ImageSource.gallery);
|
|
||||||
if (result == null) {
|
|
||||||
if (context.mounted) hideLoadingModal(context);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!context.mounted) return;
|
|
||||||
hideLoadingModal(context);
|
|
||||||
result = await cropImage(
|
|
||||||
context,
|
|
||||||
image: result,
|
|
||||||
allowedAspectRatios: [
|
|
||||||
if (position == 'background')
|
|
||||||
const CropAspectRatio(height: 7, width: 16)
|
|
||||||
else
|
|
||||||
const CropAspectRatio(height: 1, width: 1),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
if (result == null) {
|
|
||||||
if (context.mounted) hideLoadingModal(context);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!context.mounted) return;
|
|
||||||
showLoadingModal(context);
|
|
||||||
submitting.value = true;
|
|
||||||
try {
|
|
||||||
final baseUrl = ref.watch(serverUrlProvider);
|
|
||||||
final token = await getToken(ref.watch(tokenProvider));
|
|
||||||
if (token == null) throw ArgumentError('Access token is null');
|
|
||||||
final cloudFile =
|
|
||||||
await putFileToCloud(
|
|
||||||
fileData: UniversalFile(
|
|
||||||
data: result,
|
|
||||||
type: UniversalFileType.image,
|
|
||||||
),
|
|
||||||
atk: token,
|
|
||||||
baseUrl: baseUrl,
|
|
||||||
filename: result.name,
|
|
||||||
mimetype: result.mimeType ?? 'image/jpeg',
|
|
||||||
).future;
|
|
||||||
if (cloudFile == null) {
|
|
||||||
throw ArgumentError('Failed to upload the file...');
|
|
||||||
}
|
|
||||||
switch (position) {
|
|
||||||
case 'picture':
|
|
||||||
picture.value = cloudFile;
|
|
||||||
case 'background':
|
|
||||||
background.value = cloudFile;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
showErrorAlert(err);
|
|
||||||
} finally {
|
|
||||||
if (context.mounted) hideLoadingModal(context);
|
|
||||||
submitting.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> performAction() async {
|
|
||||||
if (!formKey.currentState!.validate()) return;
|
|
||||||
|
|
||||||
submitting.value = true;
|
|
||||||
try {
|
|
||||||
final client = ref.watch(apiClientProvider);
|
|
||||||
final resp = await client.request(
|
|
||||||
'/sphere${slug == null ? '/realms' : '/realms/$slug'}',
|
|
||||||
data: {
|
|
||||||
'slug': slugController.text,
|
|
||||||
'name': nameController.text,
|
|
||||||
'description': descriptionController.text,
|
|
||||||
'background_id': background.value?.id,
|
|
||||||
'picture_id': picture.value?.id,
|
|
||||||
'is_public': isPublic.value,
|
|
||||||
'is_community': isCommunity.value,
|
|
||||||
},
|
|
||||||
options: Options(method: slug == null ? 'POST' : 'PATCH'),
|
|
||||||
);
|
|
||||||
if (context.mounted) {
|
|
||||||
context.pop(SnRealm.fromJson(resp.data));
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
showErrorAlert(err);
|
|
||||||
} finally {
|
|
||||||
submitting.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return AppScaffold(
|
|
||||||
isNoBackground: false,
|
|
||||||
appBar: AppBar(
|
|
||||||
title: Text(slug == null ? 'createRealm'.tr() : 'editRealm'.tr()),
|
|
||||||
leading: const PageBackButton(),
|
|
||||||
),
|
|
||||||
body: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
AspectRatio(
|
|
||||||
aspectRatio: 16 / 7,
|
|
||||||
child: Stack(
|
|
||||||
clipBehavior: Clip.none,
|
|
||||||
fit: StackFit.expand,
|
|
||||||
children: [
|
|
||||||
GestureDetector(
|
|
||||||
child: Container(
|
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
|
||||||
child:
|
|
||||||
background.value != null
|
|
||||||
? CloudFileWidget(
|
|
||||||
item: background.value!,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
)
|
|
||||||
: const SizedBox.shrink(),
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
setPicture('background');
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
left: 20,
|
|
||||||
bottom: -32,
|
|
||||||
child: GestureDetector(
|
|
||||||
child: ProfilePictureWidget(
|
|
||||||
fileId: picture.value?.id,
|
|
||||||
radius: 40,
|
|
||||||
fallbackIcon: Symbols.group,
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
setPicture('picture');
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
).padding(bottom: 32),
|
|
||||||
Form(
|
|
||||||
key: formKey,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
TextFormField(
|
|
||||||
controller: slugController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: 'slug'.tr(),
|
|
||||||
helperText: 'slugHint'.tr(),
|
|
||||||
),
|
|
||||||
onTapOutside:
|
|
||||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
TextFormField(
|
|
||||||
controller: nameController,
|
|
||||||
decoration: InputDecoration(labelText: 'name'.tr()),
|
|
||||||
onTapOutside:
|
|
||||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
TextFormField(
|
|
||||||
controller: descriptionController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: 'description'.tr(),
|
|
||||||
alignLabelWithHint: true,
|
|
||||||
),
|
|
||||||
minLines: 3,
|
|
||||||
maxLines: null,
|
|
||||||
onTapOutside:
|
|
||||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Card(
|
|
||||||
margin: EdgeInsets.zero,
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
CheckboxListTile(
|
|
||||||
secondary: const Icon(Symbols.public),
|
|
||||||
title: Text('publicRealm').tr(),
|
|
||||||
subtitle: Text('publicRealmDescription').tr(),
|
|
||||||
value: isPublic.value,
|
|
||||||
onChanged: (value) {
|
|
||||||
isPublic.value = value ?? true;
|
|
||||||
},
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
CheckboxListTile(
|
|
||||||
secondary: const Icon(Symbols.travel_explore),
|
|
||||||
title: Text('communityRealm').tr(),
|
|
||||||
subtitle: Text('communityRealmDescription').tr(),
|
|
||||||
value: isCommunity.value,
|
|
||||||
onChanged: (value) {
|
|
||||||
isCommunity.value = value ?? false;
|
|
||||||
},
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.centerRight,
|
|
||||||
child: TextButton.icon(
|
|
||||||
onPressed: submitting.value ? null : performAction,
|
|
||||||
label: Text('saveChanges'.tr()),
|
|
||||||
icon: const Icon(Symbols.save),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).padding(all: 24),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
Future<List<SnRealmMember>> realmInvites(Ref ref) async {
|
Future<List<SnRealmMember>> realmInvites(Ref ref) async {
|
||||||
final client = ref.watch(apiClientProvider);
|
final client = ref.watch(apiClientProvider);
|
||||||
|
|||||||
@@ -234,11 +234,20 @@ class SettingsScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
|
|
||||||
// Color scheme settings
|
// Color scheme settings
|
||||||
ListTile(
|
Theme(
|
||||||
minLeadingWidth: 48,
|
data: Theme.of(
|
||||||
|
context,
|
||||||
|
).copyWith(listTileTheme: ListTileThemeData(minLeadingWidth: 48)),
|
||||||
|
child: ExpansionTile(
|
||||||
title: Text('settingsColorScheme').tr(),
|
title: Text('settingsColorScheme').tr(),
|
||||||
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
tilePadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
leading: const Icon(Symbols.palette),
|
leading: const Icon(Symbols.palette),
|
||||||
|
expandedCrossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Seed color picker
|
||||||
|
ListTile(
|
||||||
|
title: Text('Seed Color').tr(),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
trailing: GestureDetector(
|
trailing: GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
showDialog(
|
showDialog(
|
||||||
@@ -250,11 +259,13 @@ class SettingsScreen extends HookConsumerWidget {
|
|||||||
: Colors.indigo;
|
: Colors.indigo;
|
||||||
|
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: Text('settingsColorScheme').tr(),
|
title: Text('Seed Color').tr(),
|
||||||
content: SingleChildScrollView(
|
content: SingleChildScrollView(
|
||||||
child: ColorPicker(
|
child: ColorPicker(
|
||||||
paletteType: PaletteType.rgbWithBlue,
|
paletteType: PaletteType.hsv,
|
||||||
enableAlpha: false,
|
enableAlpha: true,
|
||||||
|
showLabel: true,
|
||||||
|
hexInputBar: true,
|
||||||
pickerColor: selectedColor,
|
pickerColor: selectedColor,
|
||||||
onColorChanged: (color) {
|
onColorChanged: (color) {
|
||||||
selectedColor = color;
|
selectedColor = color;
|
||||||
@@ -291,13 +302,149 @@ class SettingsScreen extends HookConsumerWidget {
|
|||||||
: Colors.indigo,
|
: Colors.indigo,
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: Theme.of(context).colorScheme.outline.withOpacity(0.5),
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.outline.withOpacity(0.5),
|
||||||
width: 2,
|
width: 2,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// Custom colors section
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
||||||
|
child:
|
||||||
|
Text(
|
||||||
|
'Custom Colors',
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
).bold(),
|
||||||
|
),
|
||||||
|
// Primary color
|
||||||
|
_ColorPickerTile(
|
||||||
|
title: 'Primary',
|
||||||
|
color:
|
||||||
|
settings.customColors?.primary != null
|
||||||
|
? Color(settings.customColors!.primary!)
|
||||||
|
: null,
|
||||||
|
onColorChanged: (color) {
|
||||||
|
final current = settings.customColors ?? ThemeColors();
|
||||||
|
ref
|
||||||
|
.read(appSettingsNotifierProvider.notifier)
|
||||||
|
.setCustomColors(current.copyWith(primary: color?.value));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
// Secondary
|
||||||
|
_ColorPickerTile(
|
||||||
|
title: 'Secondary',
|
||||||
|
color:
|
||||||
|
settings.customColors?.secondary != null
|
||||||
|
? Color(settings.customColors!.secondary!)
|
||||||
|
: null,
|
||||||
|
onColorChanged: (color) {
|
||||||
|
final current = settings.customColors ?? ThemeColors();
|
||||||
|
ref
|
||||||
|
.read(appSettingsNotifierProvider.notifier)
|
||||||
|
.setCustomColors(current.copyWith(secondary: color?.value));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
// Tertiary
|
||||||
|
_ColorPickerTile(
|
||||||
|
title: 'Tertiary',
|
||||||
|
color:
|
||||||
|
settings.customColors?.tertiary != null
|
||||||
|
? Color(settings.customColors!.tertiary!)
|
||||||
|
: null,
|
||||||
|
onColorChanged: (color) {
|
||||||
|
final current = settings.customColors ?? ThemeColors();
|
||||||
|
ref
|
||||||
|
.read(appSettingsNotifierProvider.notifier)
|
||||||
|
.setCustomColors(current.copyWith(tertiary: color?.value));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
// Surface
|
||||||
|
_ColorPickerTile(
|
||||||
|
title: 'Surface',
|
||||||
|
color:
|
||||||
|
settings.customColors?.surface != null
|
||||||
|
? Color(settings.customColors!.surface!)
|
||||||
|
: null,
|
||||||
|
onColorChanged: (color) {
|
||||||
|
final current = settings.customColors ?? ThemeColors();
|
||||||
|
ref
|
||||||
|
.read(appSettingsNotifierProvider.notifier)
|
||||||
|
.setCustomColors(current.copyWith(surface: color?.value));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
// Background
|
||||||
|
_ColorPickerTile(
|
||||||
|
title: 'Background',
|
||||||
|
color:
|
||||||
|
settings.customColors?.background != null
|
||||||
|
? Color(settings.customColors!.background!)
|
||||||
|
: null,
|
||||||
|
onColorChanged: (color) {
|
||||||
|
final current = settings.customColors ?? ThemeColors();
|
||||||
|
ref
|
||||||
|
.read(appSettingsNotifierProvider.notifier)
|
||||||
|
.setCustomColors(
|
||||||
|
current.copyWith(background: color?.value),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
// Error
|
||||||
|
_ColorPickerTile(
|
||||||
|
title: 'Error',
|
||||||
|
color:
|
||||||
|
settings.customColors?.error != null
|
||||||
|
? Color(settings.customColors!.error!)
|
||||||
|
: null,
|
||||||
|
onColorChanged: (color) {
|
||||||
|
final current = settings.customColors ?? ThemeColors();
|
||||||
|
ref
|
||||||
|
.read(appSettingsNotifierProvider.notifier)
|
||||||
|
.setCustomColors(current.copyWith(error: color?.value));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
// Reset custom colors
|
||||||
|
ListTile(
|
||||||
|
title: Text('Reset Custom Colors').tr(),
|
||||||
|
trailing: const Icon(Symbols.restart_alt).padding(right: 2),
|
||||||
|
contentPadding: EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
onTap: () {
|
||||||
|
ref
|
||||||
|
.read(appSettingsNotifierProvider.notifier)
|
||||||
|
.setCustomColors(null);
|
||||||
|
showSnackBar('settingsApplied'.tr());
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Card background opacity settings
|
||||||
|
ListTile(
|
||||||
|
minLeadingWidth: 48,
|
||||||
|
title: Text('settingsCardBackgroundOpacity').tr(),
|
||||||
|
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
||||||
|
leading: const Icon(Symbols.opacity),
|
||||||
|
subtitle: Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8),
|
||||||
|
child: Slider(
|
||||||
|
value: settings.cardTransparency,
|
||||||
|
min: 0.0,
|
||||||
|
max: 1.0,
|
||||||
|
year2023: true,
|
||||||
|
padding: EdgeInsets.only(right: 24),
|
||||||
|
label: '${(settings.cardTransparency * 100).round()}%',
|
||||||
|
onChanged: (value) {
|
||||||
|
ref
|
||||||
|
.read(appSettingsNotifierProvider.notifier)
|
||||||
|
.setAppTransparentBackground(value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
// Background image settings (only for non-web platforms)
|
// Background image settings (only for non-web platforms)
|
||||||
if (!kIsWeb && docBasepath.value != null)
|
if (!kIsWeb && docBasepath.value != null)
|
||||||
@@ -637,6 +784,22 @@ class SettingsScreen extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// Disable animation settings
|
||||||
|
ListTile(
|
||||||
|
minLeadingWidth: 48,
|
||||||
|
title: Text('settingsDisableAnimation').tr(),
|
||||||
|
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
||||||
|
leading: const Icon(Symbols.animation),
|
||||||
|
trailing: Switch(
|
||||||
|
value: settings.disableAnimation,
|
||||||
|
onChanged: (value) {
|
||||||
|
ref
|
||||||
|
.read(appSettingsNotifierProvider.notifier)
|
||||||
|
.setDisableAnimation(value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
// Desktop-specific settings
|
// Desktop-specific settings
|
||||||
@@ -787,3 +950,83 @@ class _SettingsSection extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper widget for color picker tiles
|
||||||
|
class _ColorPickerTile extends StatelessWidget {
|
||||||
|
final String title;
|
||||||
|
final Color? color;
|
||||||
|
final ValueChanged<Color?> onColorChanged;
|
||||||
|
|
||||||
|
const _ColorPickerTile({
|
||||||
|
required this.title,
|
||||||
|
required this.color,
|
||||||
|
required this.onColorChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListTile(
|
||||||
|
title: Text(title),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
trailing: GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
Color selectedColor = color ?? Colors.transparent;
|
||||||
|
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text(title),
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: ColorPicker(
|
||||||
|
paletteType: PaletteType.hsv,
|
||||||
|
enableAlpha: true,
|
||||||
|
showLabel: true,
|
||||||
|
hexInputBar: true,
|
||||||
|
pickerColor: selectedColor,
|
||||||
|
onColorChanged: (newColor) {
|
||||||
|
selectedColor = newColor;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: Text('cancel').tr(),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
onColorChanged(selectedColor);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: Text('confirm').tr(),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
onColorChanged(null);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: Text('Reset').tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
margin: EdgeInsets.symmetric(horizontal: 2, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color ?? Colors.transparent,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context).colorScheme.outline.withOpacity(0.5),
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -11,6 +12,16 @@ import 'package:material_symbols_icons/symbols.dart';
|
|||||||
|
|
||||||
final currentRouteProvider = StateProvider<String?>((ref) => null);
|
final currentRouteProvider = StateProvider<String?>((ref) => null);
|
||||||
|
|
||||||
|
const kWideScreenRouteStart = 4;
|
||||||
|
const kTabRoutes = [
|
||||||
|
'/',
|
||||||
|
'/chat',
|
||||||
|
'/realms',
|
||||||
|
'/account',
|
||||||
|
'/creators',
|
||||||
|
'/developers',
|
||||||
|
];
|
||||||
|
|
||||||
class TabsScreen extends HookConsumerWidget {
|
class TabsScreen extends HookConsumerWidget {
|
||||||
final Widget? child;
|
final Widget? child;
|
||||||
const TabsScreen({super.key, this.child});
|
const TabsScreen({super.key, this.child});
|
||||||
@@ -32,6 +43,8 @@ class TabsScreen extends HookConsumerWidget {
|
|||||||
notificationUnreadCountNotifierProvider,
|
notificationUnreadCountNotifierProvider,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final wideScreen = isWideScreen(context);
|
||||||
|
|
||||||
final destinations = [
|
final destinations = [
|
||||||
NavigationDestination(
|
NavigationDestination(
|
||||||
label: 'explore'.tr(),
|
label: 'explore'.tr(),
|
||||||
@@ -50,19 +63,30 @@ class TabsScreen extends HookConsumerWidget {
|
|||||||
child: const Icon(Symbols.account_circle),
|
child: const Icon(Symbols.account_circle),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (wideScreen)
|
||||||
|
NavigationDestination(
|
||||||
|
label: 'creatorHub'.tr(),
|
||||||
|
icon: const Icon(Symbols.ink_pen),
|
||||||
|
),
|
||||||
|
if (wideScreen)
|
||||||
|
NavigationDestination(
|
||||||
|
label: 'developerHub'.tr(),
|
||||||
|
icon: const Icon(Symbols.data_object),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
final routes = ['/', '/chat', '/realms', '/account'];
|
|
||||||
|
|
||||||
int getCurrentIndex() {
|
int getCurrentIndex() {
|
||||||
if (currentLocation.startsWith('/chat')) return 1;
|
if (currentLocation == '/') return 0;
|
||||||
if (currentLocation.startsWith('/realms')) return 2;
|
final idx = kTabRoutes.indexWhere(
|
||||||
if (currentLocation.startsWith('/account')) return 3;
|
(p) => currentLocation.startsWith(p),
|
||||||
return 0; // Default to explore
|
1,
|
||||||
|
);
|
||||||
|
final value = math.max(idx, 0);
|
||||||
|
return math.min(value, destinations.length - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
void onDestinationSelected(int index) {
|
void onDestinationSelected(int index) {
|
||||||
context.go(routes[index]);
|
context.go(kTabRoutes[index]);
|
||||||
}
|
}
|
||||||
|
|
||||||
final currentIndex = getCurrentIndex();
|
final currentIndex = getCurrentIndex();
|
||||||
@@ -99,32 +123,24 @@ class TabsScreen extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Container(
|
return Scaffold(
|
||||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
backgroundColor: Colors.transparent,
|
||||||
child: Stack(
|
extendBody: true,
|
||||||
children: [
|
resizeToAvoidBottomInset: false,
|
||||||
Positioned.fill(
|
body: ClipRRect(
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: const BorderRadius.only(
|
borderRadius: const BorderRadius.only(
|
||||||
topLeft: Radius.circular(16),
|
topLeft: Radius.circular(16),
|
||||||
topRight: Radius.circular(16),
|
topRight: Radius.circular(16),
|
||||||
),
|
),
|
||||||
child: child ?? const SizedBox.shrink(),
|
child: child ?? const SizedBox.shrink(),
|
||||||
),
|
),
|
||||||
),
|
bottomNavigationBar: ConditionalBottomNav(
|
||||||
Positioned(
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: 0,
|
|
||||||
child: ConditionalBottomNav(
|
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
child: BackdropFilter(
|
child: BackdropFilter(
|
||||||
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
|
filter: ImageFilter.blur(sigmaX: 1, sigmaY: 1),
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(
|
color: Theme.of(context).colorScheme.surface.withOpacity(0.8),
|
||||||
context,
|
|
||||||
).colorScheme.surface.withOpacity(0.8),
|
|
||||||
),
|
),
|
||||||
child: MediaQuery.removePadding(
|
child: MediaQuery.removePadding(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -137,8 +153,7 @@ class TabsScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
surfaceTintColor: Colors.transparent,
|
surfaceTintColor: Colors.transparent,
|
||||||
height: 56,
|
height: 56,
|
||||||
labelBehavior:
|
labelBehavior: NavigationDestinationLabelBehavior.alwaysHide,
|
||||||
NavigationDestinationLabelBehavior.alwaysHide,
|
|
||||||
selectedIndex: currentIndex,
|
selectedIndex: currentIndex,
|
||||||
onDestinationSelected: onDestinationSelected,
|
onDestinationSelected: onDestinationSelected,
|
||||||
destinations: destinations,
|
destinations: destinations,
|
||||||
@@ -148,9 +163,6 @@ class TabsScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -178,7 +190,6 @@ class TabbedFabLocation extends FloatingActionButtonLocation {
|
|||||||
scaffoldGeometry.floatingActionButtonSize.height -
|
scaffoldGeometry.floatingActionButtonSize.height -
|
||||||
scaffoldGeometry.bottomSheetSize.height -
|
scaffoldGeometry.bottomSheetSize.height -
|
||||||
safeAreaPadding.bottom -
|
safeAreaPadding.bottom -
|
||||||
(isWideScreen(context) ? 32 : 80) +
|
|
||||||
16;
|
16;
|
||||||
|
|
||||||
return Offset(fabX, fabY);
|
return Offset(fabX, fabY);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
1012
lib/screens/wallet.dart.backup
Normal file
1012
lib/screens/wallet.dart.backup
Normal file
File diff suppressed because it is too large
Load Diff
@@ -24,6 +24,441 @@ final walletCurrentProvider = AutoDisposeFutureProvider<SnWallet?>.internal(
|
|||||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
// ignore: unused_element
|
// ignore: unused_element
|
||||||
typedef WalletCurrentRef = AutoDisposeFutureProviderRef<SnWallet?>;
|
typedef WalletCurrentRef = AutoDisposeFutureProviderRef<SnWallet?>;
|
||||||
|
String _$walletStatsHash() => r'23d692a922c2388135be6a46afa73c018762eb57';
|
||||||
|
|
||||||
|
/// See also [walletStats].
|
||||||
|
@ProviderFor(walletStats)
|
||||||
|
final walletStatsProvider = AutoDisposeFutureProvider<SnWalletStats>.internal(
|
||||||
|
walletStats,
|
||||||
|
name: r'walletStatsProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product') ? null : _$walletStatsHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
typedef WalletStatsRef = AutoDisposeFutureProviderRef<SnWalletStats>;
|
||||||
|
String _$walletFundsHash() => r'7ceb415f64fcadab2b10461e27b95bf92352c707';
|
||||||
|
|
||||||
|
/// 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 [walletFunds].
|
||||||
|
@ProviderFor(walletFunds)
|
||||||
|
const walletFundsProvider = WalletFundsFamily();
|
||||||
|
|
||||||
|
/// See also [walletFunds].
|
||||||
|
class WalletFundsFamily extends Family<AsyncValue<List<SnWalletFund>>> {
|
||||||
|
/// See also [walletFunds].
|
||||||
|
const WalletFundsFamily();
|
||||||
|
|
||||||
|
/// See also [walletFunds].
|
||||||
|
WalletFundsProvider call({int offset = 0, int take = 20}) {
|
||||||
|
return WalletFundsProvider(offset: offset, take: take);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
WalletFundsProvider getProviderOverride(
|
||||||
|
covariant WalletFundsProvider provider,
|
||||||
|
) {
|
||||||
|
return call(offset: provider.offset, take: provider.take);
|
||||||
|
}
|
||||||
|
|
||||||
|
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'walletFundsProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [walletFunds].
|
||||||
|
class WalletFundsProvider
|
||||||
|
extends AutoDisposeFutureProvider<List<SnWalletFund>> {
|
||||||
|
/// See also [walletFunds].
|
||||||
|
WalletFundsProvider({int offset = 0, int take = 20})
|
||||||
|
: this._internal(
|
||||||
|
(ref) => walletFunds(ref as WalletFundsRef, offset: offset, take: take),
|
||||||
|
from: walletFundsProvider,
|
||||||
|
name: r'walletFundsProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$walletFundsHash,
|
||||||
|
dependencies: WalletFundsFamily._dependencies,
|
||||||
|
allTransitiveDependencies: WalletFundsFamily._allTransitiveDependencies,
|
||||||
|
offset: offset,
|
||||||
|
take: take,
|
||||||
|
);
|
||||||
|
|
||||||
|
WalletFundsProvider._internal(
|
||||||
|
super._createNotifier, {
|
||||||
|
required super.name,
|
||||||
|
required super.dependencies,
|
||||||
|
required super.allTransitiveDependencies,
|
||||||
|
required super.debugGetCreateSourceHash,
|
||||||
|
required super.from,
|
||||||
|
required this.offset,
|
||||||
|
required this.take,
|
||||||
|
}) : super.internal();
|
||||||
|
|
||||||
|
final int offset;
|
||||||
|
final int take;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Override overrideWith(
|
||||||
|
FutureOr<List<SnWalletFund>> Function(WalletFundsRef provider) create,
|
||||||
|
) {
|
||||||
|
return ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
override: WalletFundsProvider._internal(
|
||||||
|
(ref) => create(ref as WalletFundsRef),
|
||||||
|
from: from,
|
||||||
|
name: null,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
debugGetCreateSourceHash: null,
|
||||||
|
offset: offset,
|
||||||
|
take: take,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AutoDisposeFutureProviderElement<List<SnWalletFund>> createElement() {
|
||||||
|
return _WalletFundsProviderElement(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is WalletFundsProvider &&
|
||||||
|
other.offset == offset &&
|
||||||
|
other.take == take;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, offset.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, take.hashCode);
|
||||||
|
|
||||||
|
return _SystemHash.finish(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
mixin WalletFundsRef on AutoDisposeFutureProviderRef<List<SnWalletFund>> {
|
||||||
|
/// The parameter `offset` of this provider.
|
||||||
|
int get offset;
|
||||||
|
|
||||||
|
/// The parameter `take` of this provider.
|
||||||
|
int get take;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _WalletFundsProviderElement
|
||||||
|
extends AutoDisposeFutureProviderElement<List<SnWalletFund>>
|
||||||
|
with WalletFundsRef {
|
||||||
|
_WalletFundsProviderElement(super.provider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get offset => (origin as WalletFundsProvider).offset;
|
||||||
|
@override
|
||||||
|
int get take => (origin as WalletFundsProvider).take;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$walletFundRecipientsHash() =>
|
||||||
|
r'18eb815eb709449dd5c545d81fc0ee43ca667578';
|
||||||
|
|
||||||
|
/// See also [walletFundRecipients].
|
||||||
|
@ProviderFor(walletFundRecipients)
|
||||||
|
const walletFundRecipientsProvider = WalletFundRecipientsFamily();
|
||||||
|
|
||||||
|
/// See also [walletFundRecipients].
|
||||||
|
class WalletFundRecipientsFamily
|
||||||
|
extends Family<AsyncValue<List<SnWalletFundRecipient>>> {
|
||||||
|
/// See also [walletFundRecipients].
|
||||||
|
const WalletFundRecipientsFamily();
|
||||||
|
|
||||||
|
/// See also [walletFundRecipients].
|
||||||
|
WalletFundRecipientsProvider call({int offset = 0, int take = 20}) {
|
||||||
|
return WalletFundRecipientsProvider(offset: offset, take: take);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
WalletFundRecipientsProvider getProviderOverride(
|
||||||
|
covariant WalletFundRecipientsProvider provider,
|
||||||
|
) {
|
||||||
|
return call(offset: provider.offset, take: provider.take);
|
||||||
|
}
|
||||||
|
|
||||||
|
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'walletFundRecipientsProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [walletFundRecipients].
|
||||||
|
class WalletFundRecipientsProvider
|
||||||
|
extends AutoDisposeFutureProvider<List<SnWalletFundRecipient>> {
|
||||||
|
/// See also [walletFundRecipients].
|
||||||
|
WalletFundRecipientsProvider({int offset = 0, int take = 20})
|
||||||
|
: this._internal(
|
||||||
|
(ref) => walletFundRecipients(
|
||||||
|
ref as WalletFundRecipientsRef,
|
||||||
|
offset: offset,
|
||||||
|
take: take,
|
||||||
|
),
|
||||||
|
from: walletFundRecipientsProvider,
|
||||||
|
name: r'walletFundRecipientsProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$walletFundRecipientsHash,
|
||||||
|
dependencies: WalletFundRecipientsFamily._dependencies,
|
||||||
|
allTransitiveDependencies:
|
||||||
|
WalletFundRecipientsFamily._allTransitiveDependencies,
|
||||||
|
offset: offset,
|
||||||
|
take: take,
|
||||||
|
);
|
||||||
|
|
||||||
|
WalletFundRecipientsProvider._internal(
|
||||||
|
super._createNotifier, {
|
||||||
|
required super.name,
|
||||||
|
required super.dependencies,
|
||||||
|
required super.allTransitiveDependencies,
|
||||||
|
required super.debugGetCreateSourceHash,
|
||||||
|
required super.from,
|
||||||
|
required this.offset,
|
||||||
|
required this.take,
|
||||||
|
}) : super.internal();
|
||||||
|
|
||||||
|
final int offset;
|
||||||
|
final int take;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Override overrideWith(
|
||||||
|
FutureOr<List<SnWalletFundRecipient>> Function(
|
||||||
|
WalletFundRecipientsRef provider,
|
||||||
|
)
|
||||||
|
create,
|
||||||
|
) {
|
||||||
|
return ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
override: WalletFundRecipientsProvider._internal(
|
||||||
|
(ref) => create(ref as WalletFundRecipientsRef),
|
||||||
|
from: from,
|
||||||
|
name: null,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
debugGetCreateSourceHash: null,
|
||||||
|
offset: offset,
|
||||||
|
take: take,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AutoDisposeFutureProviderElement<List<SnWalletFundRecipient>>
|
||||||
|
createElement() {
|
||||||
|
return _WalletFundRecipientsProviderElement(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is WalletFundRecipientsProvider &&
|
||||||
|
other.offset == offset &&
|
||||||
|
other.take == take;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, offset.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, take.hashCode);
|
||||||
|
|
||||||
|
return _SystemHash.finish(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
mixin WalletFundRecipientsRef
|
||||||
|
on AutoDisposeFutureProviderRef<List<SnWalletFundRecipient>> {
|
||||||
|
/// The parameter `offset` of this provider.
|
||||||
|
int get offset;
|
||||||
|
|
||||||
|
/// The parameter `take` of this provider.
|
||||||
|
int get take;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _WalletFundRecipientsProviderElement
|
||||||
|
extends AutoDisposeFutureProviderElement<List<SnWalletFundRecipient>>
|
||||||
|
with WalletFundRecipientsRef {
|
||||||
|
_WalletFundRecipientsProviderElement(super.provider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get offset => (origin as WalletFundRecipientsProvider).offset;
|
||||||
|
@override
|
||||||
|
int get take => (origin as WalletFundRecipientsProvider).take;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$walletFundHash() => r'a690b0def8f4293b4a8f244e44f8bb735687e5dd';
|
||||||
|
|
||||||
|
/// See also [walletFund].
|
||||||
|
@ProviderFor(walletFund)
|
||||||
|
const walletFundProvider = WalletFundFamily();
|
||||||
|
|
||||||
|
/// See also [walletFund].
|
||||||
|
class WalletFundFamily extends Family<AsyncValue<SnWalletFund>> {
|
||||||
|
/// See also [walletFund].
|
||||||
|
const WalletFundFamily();
|
||||||
|
|
||||||
|
/// See also [walletFund].
|
||||||
|
WalletFundProvider call(String fundId) {
|
||||||
|
return WalletFundProvider(fundId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
WalletFundProvider getProviderOverride(
|
||||||
|
covariant WalletFundProvider provider,
|
||||||
|
) {
|
||||||
|
return call(provider.fundId);
|
||||||
|
}
|
||||||
|
|
||||||
|
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'walletFundProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [walletFund].
|
||||||
|
class WalletFundProvider extends AutoDisposeFutureProvider<SnWalletFund> {
|
||||||
|
/// See also [walletFund].
|
||||||
|
WalletFundProvider(String fundId)
|
||||||
|
: this._internal(
|
||||||
|
(ref) => walletFund(ref as WalletFundRef, fundId),
|
||||||
|
from: walletFundProvider,
|
||||||
|
name: r'walletFundProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$walletFundHash,
|
||||||
|
dependencies: WalletFundFamily._dependencies,
|
||||||
|
allTransitiveDependencies: WalletFundFamily._allTransitiveDependencies,
|
||||||
|
fundId: fundId,
|
||||||
|
);
|
||||||
|
|
||||||
|
WalletFundProvider._internal(
|
||||||
|
super._createNotifier, {
|
||||||
|
required super.name,
|
||||||
|
required super.dependencies,
|
||||||
|
required super.allTransitiveDependencies,
|
||||||
|
required super.debugGetCreateSourceHash,
|
||||||
|
required super.from,
|
||||||
|
required this.fundId,
|
||||||
|
}) : super.internal();
|
||||||
|
|
||||||
|
final String fundId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Override overrideWith(
|
||||||
|
FutureOr<SnWalletFund> Function(WalletFundRef provider) create,
|
||||||
|
) {
|
||||||
|
return ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
override: WalletFundProvider._internal(
|
||||||
|
(ref) => create(ref as WalletFundRef),
|
||||||
|
from: from,
|
||||||
|
name: null,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
debugGetCreateSourceHash: null,
|
||||||
|
fundId: fundId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AutoDisposeFutureProviderElement<SnWalletFund> createElement() {
|
||||||
|
return _WalletFundProviderElement(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is WalletFundProvider && other.fundId == fundId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, fundId.hashCode);
|
||||||
|
|
||||||
|
return _SystemHash.finish(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
mixin WalletFundRef on AutoDisposeFutureProviderRef<SnWalletFund> {
|
||||||
|
/// The parameter `fundId` of this provider.
|
||||||
|
String get fundId;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _WalletFundProviderElement
|
||||||
|
extends AutoDisposeFutureProviderElement<SnWalletFund>
|
||||||
|
with WalletFundRef {
|
||||||
|
_WalletFundProviderElement(super.provider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get fundId => (origin as WalletFundProvider).fundId;
|
||||||
|
}
|
||||||
|
|
||||||
String _$transactionListNotifierHash() =>
|
String _$transactionListNotifierHash() =>
|
||||||
r'7b777cd44f3351f68f7bd1dd76bfe8b388381bdb';
|
r'7b777cd44f3351f68f7bd1dd76bfe8b388381bdb';
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user