Compare commits
152 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
5663df6ef1
|
|||
|
e996a0c95f
|
|||
|
a090e93f57
|
|||
|
c69034c071
|
|||
|
369ea6cf5b
|
|||
|
2e371b5296
|
|||
|
2e9d61bcfa
|
|||
|
9c2b5b0dfa
|
|||
|
3b40f515b3
|
|||
|
5ee61dbef2
|
|||
|
b151ef6686
|
|||
|
ff934d0f08
|
|||
|
abe5ded896
|
|||
|
f1d72a5215
|
|||
|
864cbe73b7
|
|||
|
108a6da074
|
|||
|
f9a09599c9
|
|||
|
9067dadd3e
|
|||
|
09f8df1e78
|
|||
|
2c5f246c55
|
|||
|
a66c6ea654
|
|||
|
3ad4bb4518
|
|||
|
53f0dcb825
|
|||
|
557f5a2389
|
|||
|
78f14f890f
|
|||
|
77b2effb34
|
|||
|
f02b4abf65
|
|||
|
3f37c4f761
|
|||
|
5deb910fa4
|
|||
|
f50a19f573
|
|||
|
98c8a356e8
|
|||
|
d0c16ea08f
|
|||
|
f2c1b2a531
|
|||
|
3061f0c5a9
|
|||
|
98f7f33c65
|
|||
|
d9af5d32fd
|
|||
|
f2031697ec
|
|||
|
9b85b7573c
|
|||
|
4fb739b33b
|
|||
|
c03ba3bc3a
|
|||
|
fc65440420
|
|||
|
7b85533184
|
|||
|
77d9eb60c6
|
|||
|
4d8953cd22
|
|||
|
fafa460fe8
|
|||
|
faf3a677d4
|
|||
|
0f644a0234
|
|||
|
18d16fdd57
|
|||
|
18e890d63c
|
|||
|
9c5e50c16a
|
|||
|
96a2c8182e
|
|||
|
56b27c3e82
|
|||
|
ad4bf94195
|
|||
|
b77a832d8a
|
|||
|
5e61805db7
|
|||
|
35b96b0bd2
|
|||
|
c8ad791ff3
|
|||
|
1e908502dc
|
|||
|
715ce1a368
|
|||
|
548c9963ee
|
|||
|
db5199438a
|
|||
|
4409a6fb1e
|
|||
|
26a24b0e41
|
|||
|
9b948d259b
|
|||
|
1f713b5b2b
|
|||
|
f92cfafda4
|
|||
|
fa208b44d7
|
|||
|
94adecafbb
|
|||
|
0303ef4a93
|
|||
|
c2b18ce10b
|
|||
|
0767bb53ce
|
|||
|
b233f9a410
|
|||
|
256024fb46
|
|||
|
4a80aaf24d
|
|||
|
aafd160c44
|
|||
|
4a800725e3
|
|||
|
24791b3293
|
|||
|
3ac263d483
|
|||
|
2445d8adf8
|
|||
|
d4f95bbbf4
|
|||
|
943e4b7b5c
|
|||
|
7edc02a1d3
|
|||
|
3f9881e943
|
|||
|
50c25e919c
|
|||
|
99fb08dd55
|
|||
|
e43bc6b8a8
|
|||
|
c247cdf81c
|
|||
|
3ffa730505
|
|||
|
1cc34d3073
|
|||
|
96a919cc4e
|
|||
|
e7e3bfcadf
|
|||
|
a8617a5040
|
|||
|
d94f8d004f
|
|||
|
d93b066979
|
|||
|
320664a547
|
|||
|
98f4698d5b
|
|||
|
82397dd087
|
|||
|
4ec10ceb47
|
|||
|
4b03b45a0d
|
|||
|
7a72d32649
|
|||
|
5152dd13ea
|
|||
|
fd377aa7af
|
|||
|
67044148f1
|
|||
|
92bc43e4df
|
|||
|
a1a7b34c86
|
|||
|
40c0e052cf
|
|||
|
9a75228e38
|
|||
|
a9fd75cc45
|
|||
|
a713b30d93
|
|||
|
e516f0a862
|
|||
|
429b966c4b
|
|||
|
f14da0d3a2
|
|||
|
d201182bd2
|
|||
|
6f6422c15e
|
|||
|
9f6ae639ee
|
|||
|
35f4d7d885
|
|||
|
a9c8f49797
|
|||
|
5e9341a19c
|
|||
|
645a6dca93
|
|||
|
ea8e7ead2d
|
|||
|
5f2f083d72
|
|||
|
5cf40e27de
|
|||
|
1ab7295918
|
|||
|
07f191171c
|
|||
|
4a5dac248e
|
|||
|
3b983a6444
|
|||
|
4607b77355
|
|||
|
7957e4894a
|
|||
|
f94f80c375
|
|||
|
74fa2215a6
|
|||
|
0d11435feb
|
|||
|
e22598b0a6
|
|||
|
84cfe643f5
|
|||
|
05ac04e9a2
|
|||
|
66f283d6e8
|
|||
|
c779c7523c
|
|||
|
ac7cb29afe
|
|||
|
935aa77223
|
|||
|
24e5b3b824
|
|||
|
0391893b32
|
|||
|
b8d24876c8
|
|||
|
0493661f9a
|
|||
|
b40afde00f
|
|||
|
78a4022531
|
|||
|
8a291c80b7
|
|||
|
1395d65b76
|
|||
|
eb4942e0ed
|
|||
|
f254cfa81e
|
|||
|
4927795260
|
|||
|
e4019dadc8
|
|||
|
5e7d77e1a1
|
|||
|
bfcbed035c
|
@@ -136,6 +136,7 @@
|
|||||||
"reactionNegative": "Negative",
|
"reactionNegative": "Negative",
|
||||||
"reactionNeutral": "Neutral",
|
"reactionNeutral": "Neutral",
|
||||||
"customReaction": "Custom Reaction",
|
"customReaction": "Custom Reaction",
|
||||||
|
"customReactionHint": "Custom Reaction allow you to use user uploaded stickers as the symbol of the reaction for the post. Exclusive for Stellar Program members.",
|
||||||
"customReactions": "Custom Reactions",
|
"customReactions": "Custom Reactions",
|
||||||
"stickerPlaceholder": "Sticker Placeholder",
|
"stickerPlaceholder": "Sticker Placeholder",
|
||||||
"reactionAttitude": "Reaction Attitude",
|
"reactionAttitude": "Reaction Attitude",
|
||||||
@@ -179,6 +180,7 @@
|
|||||||
"noFortuneData": "No fortune data available for this month.",
|
"noFortuneData": "No fortune data available for this month.",
|
||||||
"creatorHub": "Creator Hub",
|
"creatorHub": "Creator Hub",
|
||||||
"creatorHubDescription": "Manage posts, analytics, and more.",
|
"creatorHubDescription": "Manage posts, analytics, and more.",
|
||||||
|
"publicationSites": "Publication Sites",
|
||||||
"developerPortal": "Developer Portal",
|
"developerPortal": "Developer Portal",
|
||||||
"developerPortalDescription": "Build with Solar Network™.",
|
"developerPortalDescription": "Build with Solar Network™.",
|
||||||
"statusCreateHint": "What's on your mind? Add a status.",
|
"statusCreateHint": "What's on your mind? Add a status.",
|
||||||
@@ -1087,6 +1089,7 @@
|
|||||||
"levelingStage10": "Immortal",
|
"levelingStage10": "Immortal",
|
||||||
"levelingStage11": "Divine",
|
"levelingStage11": "Divine",
|
||||||
"levelingStage12": "Transcendent",
|
"levelingStage12": "Transcendent",
|
||||||
|
"uploadTasks": "Upload Tasks",
|
||||||
"uploadAttachment": "Upload Attachment",
|
"uploadAttachment": "Upload Attachment",
|
||||||
"attachmentPreview": "Attachment Preview",
|
"attachmentPreview": "Attachment Preview",
|
||||||
"selectPool": "Select Pool",
|
"selectPool": "Select Pool",
|
||||||
@@ -1301,7 +1304,9 @@
|
|||||||
"thoughtInputHint": "Ask sn-chan anything...",
|
"thoughtInputHint": "Ask sn-chan anything...",
|
||||||
"thoughtNewConversation": "Start New Conversation",
|
"thoughtNewConversation": "Start New Conversation",
|
||||||
"thoughtParseError": "Failed to parse AI response",
|
"thoughtParseError": "Failed to parse AI response",
|
||||||
"thoughtFunctionCall": "Function Call",
|
"thoughtFunctionCall": "Use {}",
|
||||||
|
"thoughtFunctionCallBegin": "Calling tool {}",
|
||||||
|
"thoughtFunctionCallFinish": "{} responded",
|
||||||
"aiThought": "AI Thought",
|
"aiThought": "AI Thought",
|
||||||
"aiThoughtTitle": "Let sn-chan think",
|
"aiThoughtTitle": "Let sn-chan think",
|
||||||
"postReferenceUnavailable": "Referenced post is unavailable",
|
"postReferenceUnavailable": "Referenced post is unavailable",
|
||||||
@@ -1320,5 +1325,151 @@
|
|||||||
"popularity": "Popularity",
|
"popularity": "Popularity",
|
||||||
"descendingOrder": "Descending Order",
|
"descendingOrder": "Descending Order",
|
||||||
"selectDate": "Select Date",
|
"selectDate": "Select Date",
|
||||||
"pinnedPosts": "Pinned Posts"
|
"pinnedPosts": "Pinned Posts",
|
||||||
}
|
"thoughtUnpaidHint": "Thinking unavaiable due to unpaid orders",
|
||||||
|
"more": "More",
|
||||||
|
"collapse": "Collapse",
|
||||||
|
"pollConfirmDiscard": "Are you sure you want to leave? All the poll data you're editing will not be saved.",
|
||||||
|
"discard": "Discard",
|
||||||
|
"fund": "Fund",
|
||||||
|
"fundsRecent": "Recent Funds",
|
||||||
|
"fundCreateNew": "Create New",
|
||||||
|
"fundCreateNewHint": "Create a new fund for your message. Select recipients and amount.",
|
||||||
|
"amountOfSplits": "Amount of Splits",
|
||||||
|
"enterNumberOfSplits": "Enter Splits Amount",
|
||||||
|
"orCreateWith": "Or\ncreate with",
|
||||||
|
"unindexedFiles": "Unindexed files",
|
||||||
|
"folder": "Folder",
|
||||||
|
"clearCompleted": "Clear Completed",
|
||||||
|
"contentCantEmpty": "Content cannot be empty",
|
||||||
|
"features": "Features",
|
||||||
|
"unnamed": "Unnamed",
|
||||||
|
"fundEnvelopeLoadFailed": "Failed to load fund envelope",
|
||||||
|
"fundEnvelope": "Fund Envelope",
|
||||||
|
"fundEnvelopeRemaining": "Remaining: {} {}",
|
||||||
|
"fundEnvelopeSplit": "Split: {}",
|
||||||
|
"fundEnvelopeSplitEvenly": "Evenly",
|
||||||
|
"fundEnvelopeSplitRandomly": "Randomly",
|
||||||
|
"fundEnvelopeClaimSuccess": "Fund claimed successfully!",
|
||||||
|
"fundEnvelopeStatusCreated": "Created",
|
||||||
|
"fundEnvelopeStatusPartial": "Partially Claimed",
|
||||||
|
"fundEnvelopeStatusCompleted": "Fully Claimed",
|
||||||
|
"fundEnvelopeStatusExpired": "Expired",
|
||||||
|
"fundEnvelopeStatusUnknown": "Unknown",
|
||||||
|
"fundEnvelopeRecipients": "Recipients ({}/{} claimed)",
|
||||||
|
"fundEnvelopeExpiredDaysAgo": {
|
||||||
|
"one": "Expired {} day ago",
|
||||||
|
"other": "Expired {} days ago"
|
||||||
|
},
|
||||||
|
"fundEnvelopeExpiresSoon": "Expires soon",
|
||||||
|
"fundEnvelopeExpiresInHours": {
|
||||||
|
"one": "Expires in {} hour",
|
||||||
|
"other": "Expires in {} hours"
|
||||||
|
},
|
||||||
|
"fundEnvelopeExpiresInDays": {
|
||||||
|
"one": "Expires in {} day",
|
||||||
|
"other": "Expires in {} days"
|
||||||
|
},
|
||||||
|
"fundEnvelopeRemainingWithSplits": "{} {} / {} splits",
|
||||||
|
"fundEnvelopeUnknownUser": "Unknown User",
|
||||||
|
"deleteSite": "Delete Site",
|
||||||
|
"deleteSiteConfirm": "Are you sure you want to delete this site?",
|
||||||
|
"siteDeletedSuccess": "Site deleted successfully",
|
||||||
|
"siteSlug": "Slug",
|
||||||
|
"siteSlugHint": "my-site",
|
||||||
|
"siteSlugRequired": "Please enter a slug",
|
||||||
|
"siteSlugInvalid": "Slug can only contain lowercase letters, numbers, and dashes",
|
||||||
|
"siteName": "Site Name",
|
||||||
|
"siteNameHint": "My Publication Site",
|
||||||
|
"siteNameRequired": "Please enter a site name",
|
||||||
|
"siteMode": "Mode",
|
||||||
|
"siteModeFullyManaged": "Fully Managed",
|
||||||
|
"siteModeSelfManaged": "Self-Managed",
|
||||||
|
"editPublicationSite": "Edit Publication Site",
|
||||||
|
"deletePublicationSite": "Delete Publication Site",
|
||||||
|
"publicationSiteSavedSuccess": "Publication site saved successfully",
|
||||||
|
"publicationSiteDeleteConfirm": "Are you sure you want to delete this publication site? This action cannot be undone.",
|
||||||
|
"publicationSiteDeletedSuccess": "Publication site deleted successfully",
|
||||||
|
"newPublicationSite": "New Publication Site",
|
||||||
|
"siteDetails": "Site Details",
|
||||||
|
"siteInformation": "Site Information",
|
||||||
|
"siteDomain": "Domain",
|
||||||
|
"siteCreated": "Created",
|
||||||
|
"siteUpdated": "Updated",
|
||||||
|
"failedToLoadSite": "Failed to load site",
|
||||||
|
"sitePages": "Pages",
|
||||||
|
"noPagesYet": "No pages yet",
|
||||||
|
"createFirstPage": "Create your first page to get started",
|
||||||
|
"failedToLoadPages": "Failed to load pages",
|
||||||
|
"fileManagement": "File Management",
|
||||||
|
"siteFiles": "Files",
|
||||||
|
"siteFolder": "Folder",
|
||||||
|
"siteRoot": "Root",
|
||||||
|
"noFilesUploadedYet": "No files uploaded yet",
|
||||||
|
"uploadFirstFile": "Upload your first file to get started",
|
||||||
|
"failedToLoadFiles": "Failed to load files",
|
||||||
|
"noFilesFoundInFolder": "No files found in the selected folder",
|
||||||
|
"fileActions": "File Actions",
|
||||||
|
"purgeFiles": "Purge Files",
|
||||||
|
"purgeFilesDescription": "Remove all uploaded files from the site",
|
||||||
|
"deploySite": "Deploy Site",
|
||||||
|
"deploySiteDescription": "Upload and deploy a new version from ZIP archive",
|
||||||
|
"confirmPurge": "Confirm Purge",
|
||||||
|
"purgeFilesConfirm": "This will permanently delete all files uploaded to this site. This action cannot be undone. Are you sure you want to continue?",
|
||||||
|
"purgeAllFiles": "Purge All Files",
|
||||||
|
"allFilesPurgedSuccess": "All files purged successfully",
|
||||||
|
"failedToPurgeFiles": "Failed to purge files: {}",
|
||||||
|
"siteDeployedSuccess": "Site deployed successfully",
|
||||||
|
"failedToDeploySite": "Failed to deploy site: {}",
|
||||||
|
"createPage": "Create Page",
|
||||||
|
"editPage": "Edit Page",
|
||||||
|
"pageType": "Page Type",
|
||||||
|
"htmlPage": "HTML Page",
|
||||||
|
"redirectPage": "Redirect Page",
|
||||||
|
"pageTypeRequired": "Please select a page type",
|
||||||
|
"pagePath": "Page Path",
|
||||||
|
"pagePathHint": "/about, /contact, etc.",
|
||||||
|
"pagePathRequired": "Please enter a page path",
|
||||||
|
"pagePathInvalid": "Page path can only contain letters, numbers, hyphens, underscores, and slashes",
|
||||||
|
"pagePathMustStartWithSlash": "Page path must start with /",
|
||||||
|
"pagePathNoConsecutiveSlashes": "Page path cannot have consecutive slashes",
|
||||||
|
"pageTitle": "Page Title",
|
||||||
|
"pageTitleHint": "About Us, Contact, etc.",
|
||||||
|
"pageTitleRequired": "Please enter a page title",
|
||||||
|
"pageContentHtml": "Page Content (HTML)",
|
||||||
|
"pageContentHint": "<h1>Hello World</h1><p>This is my page content...</p>",
|
||||||
|
"pageContentRequired": "Please enter HTML content for the page",
|
||||||
|
"redirectTarget": "Redirect Target",
|
||||||
|
"redirectTargetHint": "/new-page, https://example.com, etc.",
|
||||||
|
"redirectTargetRequired": "Please enter a redirect target",
|
||||||
|
"redirectTargetInvalid": "Target must be a relative path (/) or absolute URL (http/https)",
|
||||||
|
"deletePage": "Delete Page",
|
||||||
|
"deletePageConfirm": "Are you sure you want to delete this page?",
|
||||||
|
"savePage": "Save Page",
|
||||||
|
"pageCreatedSuccess": "Page created successfully",
|
||||||
|
"pageUpdatedSuccess": "Page updated successfully",
|
||||||
|
"pageDeletedSuccess": "Page deleted successfully",
|
||||||
|
"uploadFiles": "Upload Files",
|
||||||
|
"uploadPath": "Upload Path",
|
||||||
|
"uploadPathHint": "/ (root) or /assets/images/",
|
||||||
|
"uploadPathRequired": "Please enter an upload path",
|
||||||
|
"uploadPathMustStartWithSlash": "Path must start with /",
|
||||||
|
"uploadPathNoSpaces": "Path cannot contain spaces",
|
||||||
|
"uploadPathNoConsecutiveSlashes": "Path cannot have consecutive slashes",
|
||||||
|
"percentCompleted": "{}% completed",
|
||||||
|
"filesToUpload": "{} files to upload",
|
||||||
|
"fileSizeKb": "Size: {} KB",
|
||||||
|
"uploadingEllipsis": "Uploading...",
|
||||||
|
"uploadFilesCount": {
|
||||||
|
"one": "Upload {} File",
|
||||||
|
"other": "Upload {} Files"
|
||||||
|
},
|
||||||
|
"allUploadsCompleted": "All uploads completed",
|
||||||
|
"someUploadsFailed": "Some uploads failed",
|
||||||
|
"uploadingInProgress": "Uploading in progress",
|
||||||
|
"readyToUpload": "Ready to upload",
|
||||||
|
"allFilesUploadedSuccess": "All files uploaded successfully",
|
||||||
|
"lotteryLastNumberSpecial": "The last selected number will be your special number.",
|
||||||
|
"lotteryMultiplierRequired": "Please enter a multiplier",
|
||||||
|
"lotteryMultiplierRange": "Multiplier must be between 1 and 10"
|
||||||
|
}
|
||||||
@@ -251,10 +251,10 @@
|
|||||||
"translatorBadgeName": "翻译者",
|
"translatorBadgeName": "翻译者",
|
||||||
"translatorBadgeDescription": "协助将 Solar Network 翻译成不同语言",
|
"translatorBadgeDescription": "协助将 Solar Network 翻译成不同语言",
|
||||||
"wallet": "钱包",
|
"wallet": "钱包",
|
||||||
"walletCurrencyPoints": "新太阳点",
|
"walletCurrencyPoints": "源能点",
|
||||||
"walletCurrencyShortPoints": "NSP",
|
"walletCurrencyShortPoints": "NSP",
|
||||||
"walletCurrencyGolds": "太阳币",
|
"walletCurrencyGolds": "星辰碎片",
|
||||||
"walletCurrencyShortGolds": "TSD",
|
"walletCurrencyShortGolds": "SHD",
|
||||||
"retry": "重试",
|
"retry": "重试",
|
||||||
"creatorHubUnselectedHint": "选择/创建一个发布者以开始使用。",
|
"creatorHubUnselectedHint": "选择/创建一个发布者以开始使用。",
|
||||||
"relationships": "关系",
|
"relationships": "关系",
|
||||||
@@ -1090,5 +1090,6 @@
|
|||||||
"thoughtNewConversation": "开始新对话",
|
"thoughtNewConversation": "开始新对话",
|
||||||
"thoughtParseError": "解析 AI 响应失败",
|
"thoughtParseError": "解析 AI 响应失败",
|
||||||
"aiThought": "寻思",
|
"aiThought": "寻思",
|
||||||
"aiThoughtTitle": "让 SN 酱寻思寻思"
|
"aiThoughtTitle": "让 SN 酱寻思寻思",
|
||||||
|
"thoughtUnpaidHint": "寻思因为有未支付的订单而被禁用"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
description: This file stores settings for Dart & Flutter DevTools.
|
description: This file stores settings for Dart & Flutter DevTools.
|
||||||
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||||
extensions:
|
extensions:
|
||||||
|
- drift: true
|
||||||
|
- provider: true
|
||||||
|
- shared_preferences: true
|
||||||
1
drift_schemas/app_database/drift_schema_v7.json
Normal file
1
drift_schemas/app_database/drift_schema_v7.json
Normal file
File diff suppressed because one or more lines are too long
@@ -1,7 +1,5 @@
|
|||||||
PODS:
|
PODS:
|
||||||
- Alamofire (5.10.2)
|
- Alamofire (5.10.2)
|
||||||
- app_links (6.4.1):
|
|
||||||
- Flutter
|
|
||||||
- connectivity_plus (0.0.1):
|
- connectivity_plus (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- croppy (0.0.1):
|
- croppy (0.0.1):
|
||||||
@@ -52,18 +50,18 @@ PODS:
|
|||||||
- Firebase/Messaging (12.4.0):
|
- Firebase/Messaging (12.4.0):
|
||||||
- Firebase/CoreOnly
|
- Firebase/CoreOnly
|
||||||
- FirebaseMessaging (~> 12.4.0)
|
- FirebaseMessaging (~> 12.4.0)
|
||||||
- firebase_analytics (12.0.3):
|
- firebase_analytics (12.0.4):
|
||||||
- firebase_core
|
- firebase_core
|
||||||
- FirebaseAnalytics (= 12.4.0)
|
- FirebaseAnalytics (= 12.4.0)
|
||||||
- Flutter
|
- Flutter
|
||||||
- firebase_core (4.2.0):
|
- firebase_core (4.2.1):
|
||||||
- Firebase/CoreOnly (= 12.4.0)
|
- Firebase/CoreOnly (= 12.4.0)
|
||||||
- Flutter
|
- Flutter
|
||||||
- firebase_crashlytics (5.0.3):
|
- firebase_crashlytics (5.0.5):
|
||||||
- Firebase/Crashlytics (= 12.4.0)
|
- Firebase/Crashlytics (= 12.4.0)
|
||||||
- firebase_core
|
- firebase_core
|
||||||
- Flutter
|
- Flutter
|
||||||
- firebase_messaging (16.0.3):
|
- firebase_messaging (16.0.4):
|
||||||
- Firebase/Messaging (= 12.4.0)
|
- Firebase/Messaging (= 12.4.0)
|
||||||
- firebase_core
|
- firebase_core
|
||||||
- Flutter
|
- Flutter
|
||||||
@@ -142,15 +140,13 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- flutter_native_splash (2.4.3):
|
- flutter_native_splash (2.4.3):
|
||||||
- Flutter
|
- Flutter
|
||||||
- flutter_platform_alert (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- flutter_secure_storage (6.0.0):
|
- flutter_secure_storage (6.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- flutter_timezone (0.0.1):
|
- flutter_timezone (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- flutter_udid (0.0.1):
|
- flutter_udid (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- SAMKeychain
|
- KeychainAccess
|
||||||
- flutter_webrtc (1.2.0):
|
- flutter_webrtc (1.2.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- WebRTC-SDK (= 137.7151.04)
|
- WebRTC-SDK (= 137.7151.04)
|
||||||
@@ -218,7 +214,8 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- irondash_engine_context (0.0.1):
|
- irondash_engine_context (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- Kingfisher (8.6.1)
|
- KeychainAccess (4.2.2)
|
||||||
|
- Kingfisher (8.6.2)
|
||||||
- KingfisherWebP (1.7.2):
|
- KingfisherWebP (1.7.2):
|
||||||
- Kingfisher (~> 8.0)
|
- Kingfisher (~> 8.0)
|
||||||
- libwebp (>= 1.1.0)
|
- libwebp (>= 1.1.0)
|
||||||
@@ -252,24 +249,24 @@ PODS:
|
|||||||
- nanopb/encode (3.30910.0)
|
- nanopb/encode (3.30910.0)
|
||||||
- native_exif (0.0.1):
|
- native_exif (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- objective_c (0.0.1):
|
||||||
|
- Flutter
|
||||||
- OrderedSet (6.0.3)
|
- OrderedSet (6.0.3)
|
||||||
- package_info_plus (0.4.5):
|
- package_info_plus (0.4.5):
|
||||||
- Flutter
|
- Flutter
|
||||||
- pasteboard (0.0.1):
|
- pasteboard (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- path_provider_foundation (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- FlutterMacOS
|
|
||||||
- pointer_interceptor_ios (0.0.1):
|
- pointer_interceptor_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- PromisesObjC (2.4.0)
|
- PromisesObjC (2.4.0)
|
||||||
- PromisesSwift (2.4.0):
|
- PromisesSwift (2.4.0):
|
||||||
- PromisesObjC (= 2.4.0)
|
- PromisesObjC (= 2.4.0)
|
||||||
|
- protocol_handler_ios (0.0.1):
|
||||||
|
- Flutter
|
||||||
- receive_sharing_intent (1.8.1):
|
- receive_sharing_intent (1.8.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- record_ios (1.1.0):
|
- record_ios (1.1.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- SAMKeychain (1.5.3)
|
|
||||||
- SDWebImage (5.21.3):
|
- SDWebImage (5.21.3):
|
||||||
- SDWebImage/Core (= 5.21.3)
|
- SDWebImage/Core (= 5.21.3)
|
||||||
- SDWebImage/Core (5.21.3)
|
- SDWebImage/Core (5.21.3)
|
||||||
@@ -315,15 +312,12 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- url_launcher_ios (0.0.1):
|
- url_launcher_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- volume_controller (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- wakelock_plus (0.0.1):
|
- wakelock_plus (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- WebRTC-SDK (137.7151.04)
|
- WebRTC-SDK (137.7151.04)
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- Alamofire
|
- Alamofire
|
||||||
- app_links (from `.symlinks/plugins/app_links/ios`)
|
|
||||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/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`)
|
||||||
@@ -339,7 +333,6 @@ DEPENDENCIES:
|
|||||||
- flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
|
- flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
|
||||||
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
||||||
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
||||||
- flutter_platform_alert (from `.symlinks/plugins/flutter_platform_alert/ios`)
|
|
||||||
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
||||||
- flutter_timezone (from `.symlinks/plugins/flutter_timezone/ios`)
|
- flutter_timezone (from `.symlinks/plugins/flutter_timezone/ios`)
|
||||||
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
|
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
|
||||||
@@ -354,10 +347,11 @@ DEPENDENCIES:
|
|||||||
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
|
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
|
||||||
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
|
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
|
||||||
- native_exif (from `.symlinks/plugins/native_exif/ios`)
|
- native_exif (from `.symlinks/plugins/native_exif/ios`)
|
||||||
|
- objective_c (from `.symlinks/plugins/objective_c/ios`)
|
||||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||||
- pasteboard (from `.symlinks/plugins/pasteboard/ios`)
|
- pasteboard (from `.symlinks/plugins/pasteboard/ios`)
|
||||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
|
||||||
- pointer_interceptor_ios (from `.symlinks/plugins/pointer_interceptor_ios/ios`)
|
- pointer_interceptor_ios (from `.symlinks/plugins/pointer_interceptor_ios/ios`)
|
||||||
|
- protocol_handler_ios (from `.symlinks/plugins/protocol_handler_ios/ios`)
|
||||||
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
|
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
|
||||||
- record_ios (from `.symlinks/plugins/record_ios/ios`)
|
- record_ios (from `.symlinks/plugins/record_ios/ios`)
|
||||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||||
@@ -368,7 +362,6 @@ DEPENDENCIES:
|
|||||||
- super_native_extensions (from `.symlinks/plugins/super_native_extensions/ios`)
|
- super_native_extensions (from `.symlinks/plugins/super_native_extensions/ios`)
|
||||||
- syncfusion_flutter_pdfviewer (from `.symlinks/plugins/syncfusion_flutter_pdfviewer/ios`)
|
- syncfusion_flutter_pdfviewer (from `.symlinks/plugins/syncfusion_flutter_pdfviewer/ios`)
|
||||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||||
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
|
|
||||||
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
||||||
|
|
||||||
SPEC REPOS:
|
SPEC REPOS:
|
||||||
@@ -390,6 +383,7 @@ SPEC REPOS:
|
|||||||
- GoogleAppMeasurement
|
- GoogleAppMeasurement
|
||||||
- GoogleDataTransport
|
- GoogleDataTransport
|
||||||
- GoogleUtilities
|
- GoogleUtilities
|
||||||
|
- KeychainAccess
|
||||||
- Kingfisher
|
- Kingfisher
|
||||||
- KingfisherWebP
|
- KingfisherWebP
|
||||||
- libwebp
|
- libwebp
|
||||||
@@ -397,15 +391,12 @@ SPEC REPOS:
|
|||||||
- OrderedSet
|
- OrderedSet
|
||||||
- PromisesObjC
|
- PromisesObjC
|
||||||
- PromisesSwift
|
- PromisesSwift
|
||||||
- SAMKeychain
|
|
||||||
- SDWebImage
|
- SDWebImage
|
||||||
- sqlite3
|
- sqlite3
|
||||||
- SwiftyGif
|
- SwiftyGif
|
||||||
- WebRTC-SDK
|
- WebRTC-SDK
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
app_links:
|
|
||||||
:path: ".symlinks/plugins/app_links/ios"
|
|
||||||
connectivity_plus:
|
connectivity_plus:
|
||||||
:path: ".symlinks/plugins/connectivity_plus/ios"
|
:path: ".symlinks/plugins/connectivity_plus/ios"
|
||||||
croppy:
|
croppy:
|
||||||
@@ -436,8 +427,6 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/flutter_local_notifications/ios"
|
:path: ".symlinks/plugins/flutter_local_notifications/ios"
|
||||||
flutter_native_splash:
|
flutter_native_splash:
|
||||||
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
||||||
flutter_platform_alert:
|
|
||||||
:path: ".symlinks/plugins/flutter_platform_alert/ios"
|
|
||||||
flutter_secure_storage:
|
flutter_secure_storage:
|
||||||
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
||||||
flutter_timezone:
|
flutter_timezone:
|
||||||
@@ -462,14 +451,16 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/media_kit_video/ios"
|
:path: ".symlinks/plugins/media_kit_video/ios"
|
||||||
native_exif:
|
native_exif:
|
||||||
:path: ".symlinks/plugins/native_exif/ios"
|
:path: ".symlinks/plugins/native_exif/ios"
|
||||||
|
objective_c:
|
||||||
|
:path: ".symlinks/plugins/objective_c/ios"
|
||||||
package_info_plus:
|
package_info_plus:
|
||||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||||
pasteboard:
|
pasteboard:
|
||||||
:path: ".symlinks/plugins/pasteboard/ios"
|
:path: ".symlinks/plugins/pasteboard/ios"
|
||||||
path_provider_foundation:
|
|
||||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
|
||||||
pointer_interceptor_ios:
|
pointer_interceptor_ios:
|
||||||
:path: ".symlinks/plugins/pointer_interceptor_ios/ios"
|
:path: ".symlinks/plugins/pointer_interceptor_ios/ios"
|
||||||
|
protocol_handler_ios:
|
||||||
|
:path: ".symlinks/plugins/protocol_handler_ios/ios"
|
||||||
receive_sharing_intent:
|
receive_sharing_intent:
|
||||||
:path: ".symlinks/plugins/receive_sharing_intent/ios"
|
:path: ".symlinks/plugins/receive_sharing_intent/ios"
|
||||||
record_ios:
|
record_ios:
|
||||||
@@ -490,14 +481,11 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/syncfusion_flutter_pdfviewer/ios"
|
:path: ".symlinks/plugins/syncfusion_flutter_pdfviewer/ios"
|
||||||
url_launcher_ios:
|
url_launcher_ios:
|
||||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||||
volume_controller:
|
|
||||||
:path: ".symlinks/plugins/volume_controller/ios"
|
|
||||||
wakelock_plus:
|
wakelock_plus:
|
||||||
:path: ".symlinks/plugins/wakelock_plus/ios"
|
:path: ".symlinks/plugins/wakelock_plus/ios"
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
|
Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
|
||||||
app_links: 3dbc685f76b1693c66a6d9dd1e9ab6f73d97dc0a
|
|
||||||
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
|
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
|
||||||
croppy: 979e8ddc254f4642bffe7d52dc7193354b27ba30
|
croppy: 979e8ddc254f4642bffe7d52dc7193354b27ba30
|
||||||
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
|
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
|
||||||
@@ -506,10 +494,10 @@ SPEC CHECKSUMS:
|
|||||||
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
|
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
|
||||||
file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6
|
file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6
|
||||||
Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e
|
Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e
|
||||||
firebase_analytics: 1d024068b1d4707d5ba7a42a12976ddf3316d835
|
firebase_analytics: 67fbdd9f3c04e55048024f3da21cfc36f05e56cf
|
||||||
firebase_core: 744984dbbed8b3036abf34f0b98d80f130a7e464
|
firebase_core: f1aafb21c14f497e5498f7ffc4dc63cbb52b2594
|
||||||
firebase_crashlytics: f3a9a4338ab99b67042f64e9e22e1bf349cb44ed
|
firebase_crashlytics: c039028126cb45e32f4c217aa392408b0963d081
|
||||||
firebase_messaging: 82c70650c426a0a14873e1acdb9ec2b443c4e8b4
|
firebase_messaging: c17a29984eafce4b2997fe078bb0a9e0b06f5dde
|
||||||
FirebaseAnalytics: 0fc2b20091f0ddd21bf73397cf8f0eb5346dc24f
|
FirebaseAnalytics: 0fc2b20091f0ddd21bf73397cf8f0eb5346dc24f
|
||||||
FirebaseCore: bb595f3114953664e3c1dc032f008a244147cfd3
|
FirebaseCore: bb595f3114953664e3c1dc032f008a244147cfd3
|
||||||
FirebaseCoreExtension: 7e1f7118ee970e001a8013719fb90950ee5e0018
|
FirebaseCoreExtension: 7e1f7118ee970e001a8013719fb90950ee5e0018
|
||||||
@@ -525,10 +513,9 @@ SPEC CHECKSUMS:
|
|||||||
flutter_keyboard_visibility: 4625131e43015dbbe759d9b20daaf77e0e3f6619
|
flutter_keyboard_visibility: 4625131e43015dbbe759d9b20daaf77e0e3f6619
|
||||||
flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb
|
flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb
|
||||||
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
|
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
|
||||||
flutter_platform_alert: bf3b5fcd4ac14bd637e20527e9c471633071afd3
|
|
||||||
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
|
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
|
||||||
flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544
|
flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544
|
||||||
flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9
|
flutter_udid: 92a5d31fe0526b7b6002a2318df702e12e7eb300
|
||||||
flutter_webrtc: c3e21fc0dcd9d8eb246ae4d5256fcbeb2f5ecd22
|
flutter_webrtc: c3e21fc0dcd9d8eb246ae4d5256fcbeb2f5ecd22
|
||||||
gal: baecd024ebfd13c441269ca7404792a7152fde89
|
gal: baecd024ebfd13c441269ca7404792a7152fde89
|
||||||
GoogleAdsOnDeviceConversion: e03a386840803ea7eef3fd22a061930142c039c1
|
GoogleAdsOnDeviceConversion: e03a386840803ea7eef3fd22a061930142c039c1
|
||||||
@@ -537,7 +524,8 @@ SPEC CHECKSUMS:
|
|||||||
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
||||||
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
|
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
|
||||||
irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
|
irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
|
||||||
Kingfisher: 7ac7a7288653787a54206b11a3c74f49ab650f1f
|
KeychainAccess: c0c4f7f38f6fc7bbe58f5702e25f7bd2f65abf51
|
||||||
|
Kingfisher: 23d18f54677d973b713e54ce6a8f5eef6e7056ba
|
||||||
KingfisherWebP: 38b9721821947f547afb78f933f75f4f9e0ae402
|
KingfisherWebP: 38b9721821947f547afb78f933f75f4f9e0ae402
|
||||||
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
|
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
|
||||||
livekit_client: 86c8af579274e4b7a215185a8080db2d4e176f40
|
livekit_client: 86c8af579274e4b7a215185a8080db2d4e176f40
|
||||||
@@ -546,16 +534,16 @@ SPEC CHECKSUMS:
|
|||||||
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
|
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
|
||||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||||
native_exif: 0eb73d3d5b3ca892719228df8d2d1b13d1ae396c
|
native_exif: 0eb73d3d5b3ca892719228df8d2d1b13d1ae396c
|
||||||
|
objective_c: 89e720c30d716b036faf9c9684022048eee1eee2
|
||||||
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
||||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||||
pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c
|
pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c
|
||||||
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
|
|
||||||
pointer_interceptor_ios: da06a662d5bfd329602b45b2ab41bc0fb5fdb0f0
|
pointer_interceptor_ios: da06a662d5bfd329602b45b2ab41bc0fb5fdb0f0
|
||||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||||
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
|
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
|
||||||
|
protocol_handler_ios: 59f23ee71f3ec602d67902ca7f669a80957888d5
|
||||||
receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
|
receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
|
||||||
record_ios: f75fa1d57f840012775c0e93a38a7f3ceea1a374
|
record_ios: f75fa1d57f840012775c0e93a38a7f3ceea1a374
|
||||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
|
||||||
SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a
|
SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a
|
||||||
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||||
@@ -567,7 +555,6 @@ SPEC CHECKSUMS:
|
|||||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||||
syncfusion_flutter_pdfviewer: 90dc48305d2e33d4aa20681d1e98ddeda891bc14
|
syncfusion_flutter_pdfviewer: 90dc48305d2e33d4aa20681d1e98ddeda891bc14
|
||||||
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
||||||
volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12
|
|
||||||
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
||||||
WebRTC-SDK: 40d4f5ba05cadff14e4db5614aec402a633f007e
|
WebRTC-SDK: 40d4f5ba05cadff14e4db5614aec402a633f007e
|
||||||
|
|
||||||
|
|||||||
@@ -48,3 +48,11 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Placeholder Implementations for Preview ---
|
||||||
|
|
||||||
|
struct ContentView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
ContentView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,17 +2,19 @@ import 'dart:convert';
|
|||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:island/database/message.dart';
|
import 'package:island/database/message.dart';
|
||||||
import 'package:island/database/draft.dart';
|
import 'package:island/database/draft.dart';
|
||||||
|
import 'package:island/models/account.dart';
|
||||||
|
import 'package:island/models/chat.dart';
|
||||||
import 'package:island/models/post.dart';
|
import 'package:island/models/post.dart';
|
||||||
|
|
||||||
part 'drift_db.g.dart';
|
part 'drift_db.g.dart';
|
||||||
|
|
||||||
// Define the database
|
// Define the database
|
||||||
@DriftDatabase(tables: [ChatMessages, PostDrafts])
|
@DriftDatabase(tables: [ChatRooms, ChatMembers, ChatMessages, PostDrafts])
|
||||||
class AppDatabase extends _$AppDatabase {
|
class AppDatabase extends _$AppDatabase {
|
||||||
AppDatabase(super.e);
|
AppDatabase(super.e);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get schemaVersion => 7;
|
int get schemaVersion => 8;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MigrationStrategy get migration => MigrationStrategy(
|
MigrationStrategy get migration => MigrationStrategy(
|
||||||
@@ -55,6 +57,11 @@ class AppDatabase extends _$AppDatabase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (from < 8) {
|
||||||
|
// Add new tables for separate sender and room data
|
||||||
|
await m.createTable(chatRooms);
|
||||||
|
await m.createTable(chatMembers);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -153,6 +160,7 @@ class AppDatabase extends _$AppDatabase {
|
|||||||
String roomId,
|
String roomId,
|
||||||
String query, {
|
String query, {
|
||||||
bool? withAttachments,
|
bool? withAttachments,
|
||||||
|
Future<SnAccount?> Function(String accountId)? fetchAccount,
|
||||||
}) async {
|
}) async {
|
||||||
var selectStatement = select(chatMessages)
|
var selectStatement = select(chatMessages)
|
||||||
..where((m) => m.roomId.equals(roomId));
|
..where((m) => m.roomId.equals(roomId));
|
||||||
@@ -178,7 +186,11 @@ class AppDatabase extends _$AppDatabase {
|
|||||||
await (selectStatement
|
await (selectStatement
|
||||||
..orderBy([(m) => OrderingTerm.desc(m.createdAt)]))
|
..orderBy([(m) => OrderingTerm.desc(m.createdAt)]))
|
||||||
.get();
|
.get();
|
||||||
return messages.map((msg) => companionToMessage(msg)).toList();
|
final messageFutures =
|
||||||
|
messages
|
||||||
|
.map((msg) => companionToMessage(msg, fetchAccount: fetchAccount))
|
||||||
|
.toList();
|
||||||
|
return await Future.wait(messageFutures);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert between Drift and model objects
|
// Convert between Drift and model objects
|
||||||
@@ -206,12 +218,88 @@ class AppDatabase extends _$AppDatabase {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
LocalChatMessage companionToMessage(ChatMessage dbMessage) {
|
Future<LocalChatMessage> companionToMessage(
|
||||||
|
ChatMessage dbMessage, {
|
||||||
|
Future<SnAccount?> Function(String accountId)? fetchAccount,
|
||||||
|
}) async {
|
||||||
final data = jsonDecode(dbMessage.data);
|
final data = jsonDecode(dbMessage.data);
|
||||||
|
SnChatMember? sender;
|
||||||
|
try {
|
||||||
|
final senderRow =
|
||||||
|
await (select(chatMembers)
|
||||||
|
..where((m) => m.id.equals(dbMessage.senderId))).getSingle();
|
||||||
|
SnAccount senderAccount;
|
||||||
|
senderAccount = SnAccount.fromJson(senderRow.account);
|
||||||
|
|
||||||
|
sender = SnChatMember(
|
||||||
|
id: senderRow.id,
|
||||||
|
chatRoomId: senderRow.chatRoomId,
|
||||||
|
accountId: senderRow.accountId,
|
||||||
|
account: senderAccount,
|
||||||
|
nick: senderRow.nick,
|
||||||
|
role: senderRow.role,
|
||||||
|
notify: senderRow.notify,
|
||||||
|
joinedAt: senderRow.joinedAt,
|
||||||
|
breakUntil: senderRow.breakUntil,
|
||||||
|
timeoutUntil: senderRow.timeoutUntil,
|
||||||
|
isBot: senderRow.isBot,
|
||||||
|
status: null,
|
||||||
|
lastTyped: senderRow.lastTyped,
|
||||||
|
createdAt: senderRow.createdAt,
|
||||||
|
updatedAt: senderRow.updatedAt,
|
||||||
|
deletedAt: senderRow.deletedAt,
|
||||||
|
chatRoom: null,
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
// Fallback to dummy sender with senderId as display name
|
||||||
|
sender = SnChatMember(
|
||||||
|
id: 'unknown',
|
||||||
|
chatRoomId: dbMessage.roomId,
|
||||||
|
accountId: dbMessage.senderId,
|
||||||
|
account: SnAccount(
|
||||||
|
id: 'unknown',
|
||||||
|
name: 'unknown',
|
||||||
|
nick: dbMessage.senderId, // Show the ID instead of Unknown
|
||||||
|
profile: SnAccountProfile(
|
||||||
|
picture: null,
|
||||||
|
id: 'unknown',
|
||||||
|
experience: 0,
|
||||||
|
level: 1,
|
||||||
|
levelingProgress: 0.0,
|
||||||
|
background: null,
|
||||||
|
verification: null,
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
deletedAt: null,
|
||||||
|
),
|
||||||
|
language: '',
|
||||||
|
isSuperuser: false,
|
||||||
|
automatedId: null,
|
||||||
|
perkSubscription: null,
|
||||||
|
deletedAt: null,
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
),
|
||||||
|
nick: dbMessage.senderId, // Show the senderId as fallback
|
||||||
|
role: 0,
|
||||||
|
notify: 0,
|
||||||
|
joinedAt: null,
|
||||||
|
breakUntil: null,
|
||||||
|
timeoutUntil: null,
|
||||||
|
isBot: false,
|
||||||
|
status: null,
|
||||||
|
lastTyped: null,
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
deletedAt: null,
|
||||||
|
chatRoom: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
return LocalChatMessage(
|
return LocalChatMessage(
|
||||||
id: dbMessage.id,
|
id: dbMessage.id,
|
||||||
roomId: dbMessage.roomId,
|
roomId: dbMessage.roomId,
|
||||||
senderId: dbMessage.senderId,
|
senderId: dbMessage.senderId,
|
||||||
|
sender: sender,
|
||||||
data: data,
|
data: data,
|
||||||
createdAt: dbMessage.createdAt,
|
createdAt: dbMessage.createdAt,
|
||||||
status: dbMessage.status,
|
status: dbMessage.status,
|
||||||
@@ -231,6 +319,85 @@ class AppDatabase extends _$AppDatabase {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ChatRoomsCompanion companionFromRoom(SnChatRoom room) {
|
||||||
|
return ChatRoomsCompanion(
|
||||||
|
id: Value(room.id),
|
||||||
|
name: Value(room.name),
|
||||||
|
description: Value(room.description),
|
||||||
|
type: Value(room.type),
|
||||||
|
isPublic: Value(room.isPublic),
|
||||||
|
isCommunity: Value(room.isCommunity),
|
||||||
|
picture: Value(room.picture?.toJson()),
|
||||||
|
background: Value(room.background?.toJson()),
|
||||||
|
realmId: Value(room.realmId),
|
||||||
|
createdAt: Value(room.createdAt),
|
||||||
|
updatedAt: Value(room.updatedAt),
|
||||||
|
deletedAt: Value(room.deletedAt),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatMembersCompanion companionFromMember(SnChatMember member) {
|
||||||
|
return ChatMembersCompanion(
|
||||||
|
id: Value(member.id),
|
||||||
|
chatRoomId: Value(member.chatRoomId),
|
||||||
|
accountId: Value(member.accountId),
|
||||||
|
account: Value(member.account.toJson()),
|
||||||
|
nick: Value(member.nick),
|
||||||
|
role: Value(member.role),
|
||||||
|
notify: Value(member.notify),
|
||||||
|
joinedAt: Value(member.joinedAt),
|
||||||
|
breakUntil: Value(member.breakUntil),
|
||||||
|
timeoutUntil: Value(member.timeoutUntil),
|
||||||
|
isBot: Value(member.isBot),
|
||||||
|
status: Value(
|
||||||
|
member.status == null ? null : jsonEncode(member.status!.toJson()),
|
||||||
|
),
|
||||||
|
lastTyped: Value(member.lastTyped),
|
||||||
|
createdAt: Value(member.createdAt),
|
||||||
|
updatedAt: Value(member.updatedAt),
|
||||||
|
deletedAt: Value(member.deletedAt),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> saveChatRooms(List<SnChatRoom> rooms) async {
|
||||||
|
await transaction(() async {
|
||||||
|
// 1. Identify rooms to remove
|
||||||
|
final remoteRoomIds = rooms.map((r) => r.id).toSet();
|
||||||
|
final currentRooms = await select(chatRooms).get();
|
||||||
|
final currentRoomIds = currentRooms.map((r) => r.id).toSet();
|
||||||
|
final idsToRemove = currentRoomIds.difference(remoteRoomIds);
|
||||||
|
|
||||||
|
if (idsToRemove.isNotEmpty) {
|
||||||
|
final idsList = idsToRemove.toList();
|
||||||
|
// Remove messages
|
||||||
|
await (delete(chatMessages)..where((t) => t.roomId.isIn(idsList))).go();
|
||||||
|
// Remove members
|
||||||
|
await (delete(chatMembers)
|
||||||
|
..where((t) => t.chatRoomId.isIn(idsList))).go();
|
||||||
|
// Remove rooms
|
||||||
|
await (delete(chatRooms)..where((t) => t.id.isIn(idsList))).go();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Upsert remote rooms
|
||||||
|
await batch((batch) {
|
||||||
|
for (final room in rooms) {
|
||||||
|
batch.insert(
|
||||||
|
chatRooms,
|
||||||
|
companionFromRoom(room),
|
||||||
|
mode: InsertMode.insertOrReplace,
|
||||||
|
);
|
||||||
|
for (final member in room.members ?? []) {
|
||||||
|
batch.insert(
|
||||||
|
chatMembers,
|
||||||
|
companionFromMember(member),
|
||||||
|
mode: InsertMode.insertOrReplace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Methods for post drafts
|
// Methods for post drafts
|
||||||
Future<List<SnPost>> getAllPostDrafts() async {
|
Future<List<SnPost>> getAllPostDrafts() async {
|
||||||
final drafts = await select(postDrafts).get();
|
final drafts = await select(postDrafts).get();
|
||||||
@@ -276,4 +443,19 @@ class AppDatabase extends _$AppDatabase {
|
|||||||
return await (select(postDrafts)
|
return await (select(postDrafts)
|
||||||
..where((tbl) => tbl.id.equals(id))).getSingleOrNull();
|
..where((tbl) => tbl.id.equals(id))).getSingleOrNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> saveMember(SnChatMember member) async {
|
||||||
|
await into(
|
||||||
|
chatMembers,
|
||||||
|
).insert(companionFromMember(member), mode: InsertMode.insertOrReplace);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> saveMessageWithSender(LocalChatMessage message) async {
|
||||||
|
// First save the sender if it exists
|
||||||
|
if (message.sender != null) {
|
||||||
|
await saveMember(message.sender!);
|
||||||
|
}
|
||||||
|
// Then save the message
|
||||||
|
return await saveMessage(messageToCompanion(message));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
616
lib/database/drift_db.steps.dart
Normal file
616
lib/database/drift_db.steps.dart
Normal file
@@ -0,0 +1,616 @@
|
|||||||
|
// dart format width=80
|
||||||
|
import 'package:drift/internal/versioned_schema.dart' as i0;
|
||||||
|
import 'package:drift/drift.dart' as i1;
|
||||||
|
import 'package:drift/drift.dart'; // ignore_for_file: type=lint,unused_import
|
||||||
|
|
||||||
|
// GENERATED BY drift_dev, DO NOT MODIFY.
|
||||||
|
final class Schema7 extends i0.VersionedSchema {
|
||||||
|
Schema7({required super.database}) : super(version: 7);
|
||||||
|
@override
|
||||||
|
late final List<i1.DatabaseSchemaEntity> entities = [
|
||||||
|
chatRooms,
|
||||||
|
chatMembers,
|
||||||
|
chatMessages,
|
||||||
|
postDrafts,
|
||||||
|
];
|
||||||
|
late final Shape0 chatRooms = Shape0(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'chat_rooms',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_1,
|
||||||
|
_column_2,
|
||||||
|
_column_3,
|
||||||
|
_column_4,
|
||||||
|
_column_5,
|
||||||
|
_column_6,
|
||||||
|
_column_7,
|
||||||
|
_column_8,
|
||||||
|
_column_9,
|
||||||
|
_column_10,
|
||||||
|
_column_11,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape1 chatMembers = Shape1(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'chat_members',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_12,
|
||||||
|
_column_13,
|
||||||
|
_column_14,
|
||||||
|
_column_15,
|
||||||
|
_column_16,
|
||||||
|
_column_17,
|
||||||
|
_column_18,
|
||||||
|
_column_19,
|
||||||
|
_column_20,
|
||||||
|
_column_21,
|
||||||
|
_column_22,
|
||||||
|
_column_23,
|
||||||
|
_column_9,
|
||||||
|
_column_10,
|
||||||
|
_column_11,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape2 chatMessages = Shape2(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'chat_messages',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_24,
|
||||||
|
_column_25,
|
||||||
|
_column_26,
|
||||||
|
_column_27,
|
||||||
|
_column_28,
|
||||||
|
_column_9,
|
||||||
|
_column_29,
|
||||||
|
_column_30,
|
||||||
|
_column_31,
|
||||||
|
_column_11,
|
||||||
|
_column_32,
|
||||||
|
_column_33,
|
||||||
|
_column_34,
|
||||||
|
_column_35,
|
||||||
|
_column_36,
|
||||||
|
_column_37,
|
||||||
|
_column_38,
|
||||||
|
_column_39,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape3 postDrafts = Shape3(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'post_drafts',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_40,
|
||||||
|
_column_2,
|
||||||
|
_column_26,
|
||||||
|
_column_41,
|
||||||
|
_column_42,
|
||||||
|
_column_43,
|
||||||
|
_column_44,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Shape0 extends i0.VersionedTable {
|
||||||
|
Shape0({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<String> get id =>
|
||||||
|
columnsByName['id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get name =>
|
||||||
|
columnsByName['name']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get description =>
|
||||||
|
columnsByName['description']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<int> get type =>
|
||||||
|
columnsByName['type']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<bool> get isPublic =>
|
||||||
|
columnsByName['is_public']! as i1.GeneratedColumn<bool>;
|
||||||
|
i1.GeneratedColumn<bool> get isCommunity =>
|
||||||
|
columnsByName['is_community']! as i1.GeneratedColumn<bool>;
|
||||||
|
i1.GeneratedColumn<String> get picture =>
|
||||||
|
columnsByName['picture']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get background =>
|
||||||
|
columnsByName['background']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get realmId =>
|
||||||
|
columnsByName['realm_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<DateTime> get createdAt =>
|
||||||
|
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<DateTime> get updatedAt =>
|
||||||
|
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<DateTime> get deletedAt =>
|
||||||
|
columnsByName['deleted_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<String> _column_0(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'id',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_1(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'name',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_2(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'description',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<int> _column_3(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>(
|
||||||
|
'type',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.int,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<bool> _column_4(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<bool>(
|
||||||
|
'is_public',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.bool,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'CHECK ("is_public" IN (0, 1))',
|
||||||
|
),
|
||||||
|
defaultValue: const CustomExpression('0'),
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<bool> _column_5(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<bool>(
|
||||||
|
'is_community',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.bool,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'CHECK ("is_community" IN (0, 1))',
|
||||||
|
),
|
||||||
|
defaultValue: const CustomExpression('0'),
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_6(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'picture',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_7(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'background',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_8(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'realm_id',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<DateTime> _column_9(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<DateTime>(
|
||||||
|
'created_at',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.dateTime,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<DateTime> _column_10(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<DateTime>(
|
||||||
|
'updated_at',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.dateTime,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<DateTime> _column_11(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<DateTime>(
|
||||||
|
'deleted_at',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.dateTime,
|
||||||
|
);
|
||||||
|
|
||||||
|
class Shape1 extends i0.VersionedTable {
|
||||||
|
Shape1({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<String> get id =>
|
||||||
|
columnsByName['id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get chatRoomId =>
|
||||||
|
columnsByName['chat_room_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get accountId =>
|
||||||
|
columnsByName['account_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get account =>
|
||||||
|
columnsByName['account']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get nick =>
|
||||||
|
columnsByName['nick']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<int> get role =>
|
||||||
|
columnsByName['role']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<int> get notify =>
|
||||||
|
columnsByName['notify']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<DateTime> get joinedAt =>
|
||||||
|
columnsByName['joined_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<DateTime> get breakUntil =>
|
||||||
|
columnsByName['break_until']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<DateTime> get timeoutUntil =>
|
||||||
|
columnsByName['timeout_until']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<bool> get isBot =>
|
||||||
|
columnsByName['is_bot']! as i1.GeneratedColumn<bool>;
|
||||||
|
i1.GeneratedColumn<String> get status =>
|
||||||
|
columnsByName['status']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<DateTime> get lastTyped =>
|
||||||
|
columnsByName['last_typed']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<DateTime> get createdAt =>
|
||||||
|
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<DateTime> get updatedAt =>
|
||||||
|
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<DateTime> get deletedAt =>
|
||||||
|
columnsByName['deleted_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<String> _column_12(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'chat_room_id',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'REFERENCES chat_rooms (id)',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_13(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'account_id',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_14(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'account',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_15(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'nick',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<int> _column_16(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>(
|
||||||
|
'role',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.int,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<int> _column_17(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>(
|
||||||
|
'notify',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.int,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<DateTime> _column_18(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<DateTime>(
|
||||||
|
'joined_at',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.dateTime,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<DateTime> _column_19(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<DateTime>(
|
||||||
|
'break_until',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.dateTime,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<DateTime> _column_20(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<DateTime>(
|
||||||
|
'timeout_until',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.dateTime,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<bool> _column_21(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<bool>(
|
||||||
|
'is_bot',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.bool,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'CHECK ("is_bot" IN (0, 1))',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_22(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'status',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<DateTime> _column_23(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<DateTime>(
|
||||||
|
'last_typed',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.dateTime,
|
||||||
|
);
|
||||||
|
|
||||||
|
class Shape2 extends i0.VersionedTable {
|
||||||
|
Shape2({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<String> get id =>
|
||||||
|
columnsByName['id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get roomId =>
|
||||||
|
columnsByName['room_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get senderId =>
|
||||||
|
columnsByName['sender_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get content =>
|
||||||
|
columnsByName['content']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get nonce =>
|
||||||
|
columnsByName['nonce']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get data =>
|
||||||
|
columnsByName['data']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<DateTime> get createdAt =>
|
||||||
|
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<int> get status =>
|
||||||
|
columnsByName['status']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<bool> get isDeleted =>
|
||||||
|
columnsByName['is_deleted']! as i1.GeneratedColumn<bool>;
|
||||||
|
i1.GeneratedColumn<DateTime> get updatedAt =>
|
||||||
|
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<DateTime> get deletedAt =>
|
||||||
|
columnsByName['deleted_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<String> get type =>
|
||||||
|
columnsByName['type']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get meta =>
|
||||||
|
columnsByName['meta']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get membersMentioned =>
|
||||||
|
columnsByName['members_mentioned']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<DateTime> get editedAt =>
|
||||||
|
columnsByName['edited_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<String> get attachments =>
|
||||||
|
columnsByName['attachments']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get reactions =>
|
||||||
|
columnsByName['reactions']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get repliedMessageId =>
|
||||||
|
columnsByName['replied_message_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get forwardedMessageId =>
|
||||||
|
columnsByName['forwarded_message_id']! as i1.GeneratedColumn<String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<String> _column_24(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'room_id',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'REFERENCES chat_rooms (id)',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_25(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'sender_id',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'REFERENCES chat_members (id)',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_26(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'content',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_27(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'nonce',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_28(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'data',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<int> _column_29(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>(
|
||||||
|
'status',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.int,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<bool> _column_30(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<bool>(
|
||||||
|
'is_deleted',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.bool,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'CHECK ("is_deleted" IN (0, 1))',
|
||||||
|
),
|
||||||
|
defaultValue: const CustomExpression('0'),
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<DateTime> _column_31(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<DateTime>(
|
||||||
|
'updated_at',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.dateTime,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_32(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'type',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
defaultValue: const CustomExpression('\'text\''),
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_33(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'meta',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
defaultValue: const CustomExpression('\'{}\''),
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_34(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'members_mentioned',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
defaultValue: const CustomExpression('\'[]\''),
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<DateTime> _column_35(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<DateTime>(
|
||||||
|
'edited_at',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.dateTime,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_36(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'attachments',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
defaultValue: const CustomExpression('\'[]\''),
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_37(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'reactions',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
defaultValue: const CustomExpression('\'[]\''),
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_38(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'replied_message_id',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_39(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'forwarded_message_id',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
);
|
||||||
|
|
||||||
|
class Shape3 extends i0.VersionedTable {
|
||||||
|
Shape3({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<String> get id =>
|
||||||
|
columnsByName['id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get title =>
|
||||||
|
columnsByName['title']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get description =>
|
||||||
|
columnsByName['description']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get content =>
|
||||||
|
columnsByName['content']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<int> get visibility =>
|
||||||
|
columnsByName['visibility']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<int> get type =>
|
||||||
|
columnsByName['type']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<DateTime> get lastModified =>
|
||||||
|
columnsByName['last_modified']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<String> get postData =>
|
||||||
|
columnsByName['post_data']! as i1.GeneratedColumn<String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<String> _column_40(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'title',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<int> _column_41(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>(
|
||||||
|
'visibility',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.int,
|
||||||
|
defaultValue: const CustomExpression('0'),
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<int> _column_42(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>(
|
||||||
|
'type',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.int,
|
||||||
|
defaultValue: const CustomExpression('0'),
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<DateTime> _column_43(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<DateTime>(
|
||||||
|
'last_modified',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.dateTime,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_44(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'post_data',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
);
|
||||||
|
i0.MigrationStepWithVersion migrationSteps({
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7,
|
||||||
|
}) {
|
||||||
|
return (currentVersion, database) async {
|
||||||
|
switch (currentVersion) {
|
||||||
|
case 6:
|
||||||
|
final schema = Schema7(database: database);
|
||||||
|
final migrator = i1.Migrator(database, schema);
|
||||||
|
await from6To7(migrator, schema);
|
||||||
|
return 7;
|
||||||
|
default:
|
||||||
|
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.OnUpgrade stepByStep({
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7,
|
||||||
|
}) => i0.VersionedSchema.stepByStepHelper(
|
||||||
|
step: migrationSteps(from6To7: from6To7),
|
||||||
|
);
|
||||||
@@ -36,10 +36,52 @@ class ListMapConverter
|
|||||||
String toSql(List<Map<String, dynamic>> value) => json.encode(value);
|
String toSql(List<Map<String, dynamic>> value) => json.encode(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ChatRooms extends Table {
|
||||||
|
TextColumn get id => text()();
|
||||||
|
TextColumn get name => text().nullable()();
|
||||||
|
TextColumn get description => text().nullable()();
|
||||||
|
IntColumn get type => integer()();
|
||||||
|
BoolColumn get isPublic =>
|
||||||
|
boolean().nullable().withDefault(const Constant(false))();
|
||||||
|
BoolColumn get isCommunity =>
|
||||||
|
boolean().nullable().withDefault(const Constant(false))();
|
||||||
|
TextColumn get picture => text().map(const MapConverter()).nullable()();
|
||||||
|
TextColumn get background => text().map(const MapConverter()).nullable()();
|
||||||
|
TextColumn get realmId => text().nullable()();
|
||||||
|
DateTimeColumn get createdAt => dateTime()();
|
||||||
|
DateTimeColumn get updatedAt => dateTime()();
|
||||||
|
DateTimeColumn get deletedAt => dateTime().nullable()();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<Column> get primaryKey => {id};
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChatMembers extends Table {
|
||||||
|
TextColumn get id => text()();
|
||||||
|
TextColumn get chatRoomId => text().references(ChatRooms, #id)();
|
||||||
|
TextColumn get accountId => text()();
|
||||||
|
TextColumn get account => text().map(const MapConverter())();
|
||||||
|
TextColumn get nick => text().nullable()();
|
||||||
|
IntColumn get role => integer()();
|
||||||
|
IntColumn get notify => integer()();
|
||||||
|
DateTimeColumn get joinedAt => dateTime().nullable()();
|
||||||
|
DateTimeColumn get breakUntil => dateTime().nullable()();
|
||||||
|
DateTimeColumn get timeoutUntil => dateTime().nullable()();
|
||||||
|
BoolColumn get isBot => boolean()();
|
||||||
|
TextColumn get status => text().nullable()();
|
||||||
|
DateTimeColumn get lastTyped => dateTime().nullable()();
|
||||||
|
DateTimeColumn get createdAt => dateTime()();
|
||||||
|
DateTimeColumn get updatedAt => dateTime()();
|
||||||
|
DateTimeColumn get deletedAt => dateTime().nullable()();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<Column> get primaryKey => {id};
|
||||||
|
}
|
||||||
|
|
||||||
class ChatMessages extends Table {
|
class ChatMessages extends Table {
|
||||||
TextColumn get id => text()();
|
TextColumn get id => text()();
|
||||||
TextColumn get roomId => text()();
|
TextColumn get roomId => text().references(ChatRooms, #id)();
|
||||||
TextColumn get senderId => text()();
|
TextColumn get senderId => text().references(ChatMembers, #id)();
|
||||||
TextColumn get content => text().nullable()();
|
TextColumn get content => text().nullable()();
|
||||||
TextColumn get nonce => text().nullable()();
|
TextColumn get nonce => text().nullable()();
|
||||||
TextColumn get data => text()();
|
TextColumn get data => text()();
|
||||||
@@ -72,6 +114,7 @@ class LocalChatMessage {
|
|||||||
final String id;
|
final String id;
|
||||||
final String roomId;
|
final String roomId;
|
||||||
final String senderId;
|
final String senderId;
|
||||||
|
final SnChatMember? sender;
|
||||||
final Map<String, dynamic> data;
|
final Map<String, dynamic> data;
|
||||||
final DateTime createdAt;
|
final DateTime createdAt;
|
||||||
MessageStatus status;
|
MessageStatus status;
|
||||||
@@ -94,6 +137,7 @@ class LocalChatMessage {
|
|||||||
required this.id,
|
required this.id,
|
||||||
required this.roomId,
|
required this.roomId,
|
||||||
required this.senderId,
|
required this.senderId,
|
||||||
|
required this.sender,
|
||||||
required this.data,
|
required this.data,
|
||||||
required this.createdAt,
|
required this.createdAt,
|
||||||
required this.nonce,
|
required this.nonce,
|
||||||
@@ -114,7 +158,12 @@ class LocalChatMessage {
|
|||||||
});
|
});
|
||||||
|
|
||||||
SnChatMessage toRemoteMessage() {
|
SnChatMessage toRemoteMessage() {
|
||||||
return SnChatMessage.fromJson(data);
|
if (sender == null) {
|
||||||
|
throw Exception('Cannot create remote message without sender');
|
||||||
|
}
|
||||||
|
final msgData = Map<String, dynamic>.from(data);
|
||||||
|
msgData['sender'] = sender!.toJson();
|
||||||
|
return SnChatMessage.fromJson(msgData);
|
||||||
}
|
}
|
||||||
|
|
||||||
static LocalChatMessage fromRemoteMessage(
|
static LocalChatMessage fromRemoteMessage(
|
||||||
@@ -122,11 +171,26 @@ class LocalChatMessage {
|
|||||||
MessageStatus status, {
|
MessageStatus status, {
|
||||||
String? nonce,
|
String? nonce,
|
||||||
}) {
|
}) {
|
||||||
|
final jsonData = message.toJson();
|
||||||
|
jsonData.remove('sender');
|
||||||
|
// Ensure proper defaults for collections to prevent type cast errors
|
||||||
|
if (jsonData['meta'] == null) jsonData['meta'] = <String, dynamic>{};
|
||||||
|
if (jsonData['members_mentioned'] == null) {
|
||||||
|
jsonData['members_mentioned'] = <String>[];
|
||||||
|
}
|
||||||
|
if (jsonData['attachments'] == null) {
|
||||||
|
jsonData['attachments'] = <Map<String, dynamic>>[];
|
||||||
|
}
|
||||||
|
if (jsonData['reactions'] == null) {
|
||||||
|
jsonData['reactions'] = <Map<String, dynamic>>[];
|
||||||
|
}
|
||||||
|
final msgData = Map<String, dynamic>.from(jsonData);
|
||||||
return LocalChatMessage(
|
return LocalChatMessage(
|
||||||
id: message.id,
|
id: message.id,
|
||||||
roomId: message.chatRoomId,
|
roomId: message.chatRoomId,
|
||||||
senderId: message.senderId,
|
senderId: message.senderId,
|
||||||
data: message.toJson(),
|
sender: message.sender,
|
||||||
|
data: msgData,
|
||||||
createdAt: message.createdAt,
|
createdAt: message.createdAt,
|
||||||
status: status,
|
status: status,
|
||||||
nonce: nonce ?? message.nonce,
|
nonce: nonce ?? message.nonce,
|
||||||
|
|||||||
@@ -59,6 +59,8 @@ void main() async {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await EasyLocalization.ensureInitialized();
|
await EasyLocalization.ensureInitialized();
|
||||||
|
// Disable logs
|
||||||
|
EasyLocalization.logger.enableBuildModes = [];
|
||||||
|
|
||||||
if (kIsWeb || !Platform.isLinux) {
|
if (kIsWeb || !Platform.isLinux) {
|
||||||
await Firebase.initializeApp(
|
await Firebase.initializeApp(
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
import 'package:island/models/activity.dart';
|
||||||
import 'package:island/models/auth.dart';
|
import 'package:island/models/auth.dart';
|
||||||
import 'package:island/models/file.dart';
|
import 'package:island/models/file.dart';
|
||||||
import 'package:island/models/wallet.dart';
|
import 'package:island/models/wallet.dart';
|
||||||
@@ -263,3 +264,15 @@ sealed class SnSocialCreditRecord with _$SnSocialCreditRecord {
|
|||||||
factory SnSocialCreditRecord.fromJson(Map<String, dynamic> json) =>
|
factory SnSocialCreditRecord.fromJson(Map<String, dynamic> json) =>
|
||||||
_$SnSocialCreditRecordFromJson(json);
|
_$SnSocialCreditRecordFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
sealed class SnFriendOverviewItem with _$SnFriendOverviewItem {
|
||||||
|
const factory SnFriendOverviewItem({
|
||||||
|
required SnAccount account,
|
||||||
|
required SnAccountStatus status,
|
||||||
|
required List<SnPresenceActivity> activities,
|
||||||
|
}) = _SnFriendOverviewItem;
|
||||||
|
|
||||||
|
factory SnFriendOverviewItem.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SnFriendOverviewItemFromJson(json);
|
||||||
|
}
|
||||||
|
|||||||
@@ -3912,4 +3912,309 @@ as DateTime?,
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$SnFriendOverviewItem {
|
||||||
|
|
||||||
|
SnAccount get account; SnAccountStatus get status; List<SnPresenceActivity> get activities;
|
||||||
|
/// Create a copy of SnFriendOverviewItem
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnFriendOverviewItemCopyWith<SnFriendOverviewItem> get copyWith => _$SnFriendOverviewItemCopyWithImpl<SnFriendOverviewItem>(this as SnFriendOverviewItem, _$identity);
|
||||||
|
|
||||||
|
/// Serializes this SnFriendOverviewItem to a JSON map.
|
||||||
|
Map<String, dynamic> toJson();
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnFriendOverviewItem&&(identical(other.account, account) || other.account == account)&&(identical(other.status, status) || other.status == status)&&const DeepCollectionEquality().equals(other.activities, activities));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,account,status,const DeepCollectionEquality().hash(activities));
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SnFriendOverviewItem(account: $account, status: $status, activities: $activities)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class $SnFriendOverviewItemCopyWith<$Res> {
|
||||||
|
factory $SnFriendOverviewItemCopyWith(SnFriendOverviewItem value, $Res Function(SnFriendOverviewItem) _then) = _$SnFriendOverviewItemCopyWithImpl;
|
||||||
|
@useResult
|
||||||
|
$Res call({
|
||||||
|
SnAccount account, SnAccountStatus status, List<SnPresenceActivity> activities
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
$SnAccountCopyWith<$Res> get account;$SnAccountStatusCopyWith<$Res> get status;
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class _$SnFriendOverviewItemCopyWithImpl<$Res>
|
||||||
|
implements $SnFriendOverviewItemCopyWith<$Res> {
|
||||||
|
_$SnFriendOverviewItemCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final SnFriendOverviewItem _self;
|
||||||
|
final $Res Function(SnFriendOverviewItem) _then;
|
||||||
|
|
||||||
|
/// Create a copy of SnFriendOverviewItem
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline') @override $Res call({Object? account = null,Object? status = null,Object? activities = null,}) {
|
||||||
|
return _then(_self.copyWith(
|
||||||
|
account: null == account ? _self.account : account // ignore: cast_nullable_to_non_nullable
|
||||||
|
as SnAccount,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
|
||||||
|
as SnAccountStatus,activities: null == activities ? _self.activities : activities // ignore: cast_nullable_to_non_nullable
|
||||||
|
as List<SnPresenceActivity>,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
/// Create a copy of SnFriendOverviewItem
|
||||||
|
/// 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));
|
||||||
|
});
|
||||||
|
}/// Create a copy of SnFriendOverviewItem
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnAccountStatusCopyWith<$Res> get status {
|
||||||
|
|
||||||
|
return $SnAccountStatusCopyWith<$Res>(_self.status, (value) {
|
||||||
|
return _then(_self.copyWith(status: value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Adds pattern-matching-related methods to [SnFriendOverviewItem].
|
||||||
|
extension SnFriendOverviewItemPatterns on SnFriendOverviewItem {
|
||||||
|
/// 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( _SnFriendOverviewItem value)? $default,{required TResult orElse(),}){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnFriendOverviewItem() 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( _SnFriendOverviewItem value) $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnFriendOverviewItem():
|
||||||
|
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( _SnFriendOverviewItem value)? $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnFriendOverviewItem() 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( SnAccount account, SnAccountStatus status, List<SnPresenceActivity> activities)? $default,{required TResult orElse(),}) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnFriendOverviewItem() when $default != null:
|
||||||
|
return $default(_that.account,_that.status,_that.activities);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( SnAccount account, SnAccountStatus status, List<SnPresenceActivity> activities) $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnFriendOverviewItem():
|
||||||
|
return $default(_that.account,_that.status,_that.activities);}
|
||||||
|
}
|
||||||
|
/// 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( SnAccount account, SnAccountStatus status, List<SnPresenceActivity> activities)? $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnFriendOverviewItem() when $default != null:
|
||||||
|
return $default(_that.account,_that.status,_that.activities);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
|
||||||
|
class _SnFriendOverviewItem implements SnFriendOverviewItem {
|
||||||
|
const _SnFriendOverviewItem({required this.account, required this.status, required final List<SnPresenceActivity> activities}): _activities = activities;
|
||||||
|
factory _SnFriendOverviewItem.fromJson(Map<String, dynamic> json) => _$SnFriendOverviewItemFromJson(json);
|
||||||
|
|
||||||
|
@override final SnAccount account;
|
||||||
|
@override final SnAccountStatus status;
|
||||||
|
final List<SnPresenceActivity> _activities;
|
||||||
|
@override List<SnPresenceActivity> get activities {
|
||||||
|
if (_activities is EqualUnmodifiableListView) return _activities;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableListView(_activities);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Create a copy of SnFriendOverviewItem
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$SnFriendOverviewItemCopyWith<_SnFriendOverviewItem> get copyWith => __$SnFriendOverviewItemCopyWithImpl<_SnFriendOverviewItem>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$SnFriendOverviewItemToJson(this, );
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnFriendOverviewItem&&(identical(other.account, account) || other.account == account)&&(identical(other.status, status) || other.status == status)&&const DeepCollectionEquality().equals(other._activities, _activities));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,account,status,const DeepCollectionEquality().hash(_activities));
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SnFriendOverviewItem(account: $account, status: $status, activities: $activities)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class _$SnFriendOverviewItemCopyWith<$Res> implements $SnFriendOverviewItemCopyWith<$Res> {
|
||||||
|
factory _$SnFriendOverviewItemCopyWith(_SnFriendOverviewItem value, $Res Function(_SnFriendOverviewItem) _then) = __$SnFriendOverviewItemCopyWithImpl;
|
||||||
|
@override @useResult
|
||||||
|
$Res call({
|
||||||
|
SnAccount account, SnAccountStatus status, List<SnPresenceActivity> activities
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
@override $SnAccountCopyWith<$Res> get account;@override $SnAccountStatusCopyWith<$Res> get status;
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class __$SnFriendOverviewItemCopyWithImpl<$Res>
|
||||||
|
implements _$SnFriendOverviewItemCopyWith<$Res> {
|
||||||
|
__$SnFriendOverviewItemCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final _SnFriendOverviewItem _self;
|
||||||
|
final $Res Function(_SnFriendOverviewItem) _then;
|
||||||
|
|
||||||
|
/// Create a copy of SnFriendOverviewItem
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @pragma('vm:prefer-inline') $Res call({Object? account = null,Object? status = null,Object? activities = null,}) {
|
||||||
|
return _then(_SnFriendOverviewItem(
|
||||||
|
account: null == account ? _self.account : account // ignore: cast_nullable_to_non_nullable
|
||||||
|
as SnAccount,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
|
||||||
|
as SnAccountStatus,activities: null == activities ? _self._activities : activities // ignore: cast_nullable_to_non_nullable
|
||||||
|
as List<SnPresenceActivity>,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a copy of SnFriendOverviewItem
|
||||||
|
/// 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));
|
||||||
|
});
|
||||||
|
}/// Create a copy of SnFriendOverviewItem
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnAccountStatusCopyWith<$Res> get status {
|
||||||
|
|
||||||
|
return $SnAccountStatusCopyWith<$Res>(_self.status, (value) {
|
||||||
|
return _then(_self.copyWith(status: value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// dart format on
|
// dart format on
|
||||||
|
|||||||
@@ -449,3 +449,22 @@ Map<String, dynamic> _$SnSocialCreditRecordToJson(
|
|||||||
'updated_at': instance.updatedAt.toIso8601String(),
|
'updated_at': instance.updatedAt.toIso8601String(),
|
||||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_SnFriendOverviewItem _$SnFriendOverviewItemFromJson(
|
||||||
|
Map<String, dynamic> json,
|
||||||
|
) => _SnFriendOverviewItem(
|
||||||
|
account: SnAccount.fromJson(json['account'] as Map<String, dynamic>),
|
||||||
|
status: SnAccountStatus.fromJson(json['status'] as Map<String, dynamic>),
|
||||||
|
activities:
|
||||||
|
(json['activities'] as List<dynamic>)
|
||||||
|
.map((e) => SnPresenceActivity.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$SnFriendOverviewItemToJson(
|
||||||
|
_SnFriendOverviewItem instance,
|
||||||
|
) => <String, dynamic>{
|
||||||
|
'account': instance.account.toJson(),
|
||||||
|
'status': instance.status.toJson(),
|
||||||
|
'activities': instance.activities.map((e) => e.toJson()).toList(),
|
||||||
|
};
|
||||||
|
|||||||
57
lib/models/drive_task.dart
Normal file
57
lib/models/drive_task.dart
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
import 'package:island/models/file.dart';
|
||||||
|
|
||||||
|
part 'drive_task.freezed.dart';
|
||||||
|
part 'drive_task.g.dart';
|
||||||
|
|
||||||
|
enum DriveTaskStatus {
|
||||||
|
pending,
|
||||||
|
inProgress,
|
||||||
|
paused,
|
||||||
|
completed,
|
||||||
|
failed,
|
||||||
|
expired,
|
||||||
|
cancelled,
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
sealed class DriveTask with _$DriveTask {
|
||||||
|
const DriveTask._();
|
||||||
|
|
||||||
|
const factory DriveTask({
|
||||||
|
required String id,
|
||||||
|
required String taskId,
|
||||||
|
required String fileName,
|
||||||
|
required String contentType,
|
||||||
|
required int fileSize,
|
||||||
|
required int uploadedBytes,
|
||||||
|
required int totalChunks,
|
||||||
|
required int uploadedChunks,
|
||||||
|
required DriveTaskStatus status,
|
||||||
|
required DateTime createdAt,
|
||||||
|
required DateTime updatedAt,
|
||||||
|
required String type, // Task type (e.g., 'FileUpload')
|
||||||
|
double? transmissionProgress, // Local file upload progress (0.0-1.0)
|
||||||
|
String? errorMessage,
|
||||||
|
String? statusMessage,
|
||||||
|
SnCloudFile? result,
|
||||||
|
String? poolId,
|
||||||
|
String? bundleId,
|
||||||
|
String? encryptPassword,
|
||||||
|
String? expiredAt,
|
||||||
|
}) = _DriveTask;
|
||||||
|
|
||||||
|
factory DriveTask.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$DriveTaskFromJson(json);
|
||||||
|
|
||||||
|
double get progress => totalChunks > 0 ? uploadedChunks / totalChunks : 0.0;
|
||||||
|
|
||||||
|
Duration get estimatedTimeRemaining {
|
||||||
|
if (uploadedBytes == 0 || fileSize == 0) return Duration.zero;
|
||||||
|
final remainingBytes = fileSize - uploadedBytes;
|
||||||
|
final uploadRate =
|
||||||
|
uploadedBytes / createdAt.difference(DateTime.now()).inSeconds.abs();
|
||||||
|
if (uploadRate == 0) return Duration.zero;
|
||||||
|
return Duration(seconds: (remainingBytes / uploadRate).round());
|
||||||
|
}
|
||||||
|
}
|
||||||
356
lib/models/drive_task.freezed.dart
Normal file
356
lib/models/drive_task.freezed.dart
Normal file
@@ -0,0 +1,356 @@
|
|||||||
|
// 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 'drive_task.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// FreezedGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// dart format off
|
||||||
|
T _$identity<T>(T value) => value;
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$DriveTask {
|
||||||
|
|
||||||
|
String get id; String get taskId; String get fileName; String get contentType; int get fileSize; int get uploadedBytes; int get totalChunks; int get uploadedChunks; DriveTaskStatus get status; DateTime get createdAt; DateTime get updatedAt; String get type;// Task type (e.g., 'FileUpload')
|
||||||
|
double? get transmissionProgress;// Local file upload progress (0.0-1.0)
|
||||||
|
String? get errorMessage; String? get statusMessage; SnCloudFile? get result; String? get poolId; String? get bundleId; String? get encryptPassword; String? get expiredAt;
|
||||||
|
/// Create a copy of DriveTask
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$DriveTaskCopyWith<DriveTask> get copyWith => _$DriveTaskCopyWithImpl<DriveTask>(this as DriveTask, _$identity);
|
||||||
|
|
||||||
|
/// Serializes this DriveTask to a JSON map.
|
||||||
|
Map<String, dynamic> toJson();
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is DriveTask&&(identical(other.id, id) || other.id == id)&&(identical(other.taskId, taskId) || other.taskId == taskId)&&(identical(other.fileName, fileName) || other.fileName == fileName)&&(identical(other.contentType, contentType) || other.contentType == contentType)&&(identical(other.fileSize, fileSize) || other.fileSize == fileSize)&&(identical(other.uploadedBytes, uploadedBytes) || other.uploadedBytes == uploadedBytes)&&(identical(other.totalChunks, totalChunks) || other.totalChunks == totalChunks)&&(identical(other.uploadedChunks, uploadedChunks) || other.uploadedChunks == uploadedChunks)&&(identical(other.status, status) || other.status == status)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.type, type) || other.type == type)&&(identical(other.transmissionProgress, transmissionProgress) || other.transmissionProgress == transmissionProgress)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.statusMessage, statusMessage) || other.statusMessage == statusMessage)&&(identical(other.result, result) || other.result == result)&&(identical(other.poolId, poolId) || other.poolId == poolId)&&(identical(other.bundleId, bundleId) || other.bundleId == bundleId)&&(identical(other.encryptPassword, encryptPassword) || other.encryptPassword == encryptPassword)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hashAll([runtimeType,id,taskId,fileName,contentType,fileSize,uploadedBytes,totalChunks,uploadedChunks,status,createdAt,updatedAt,type,transmissionProgress,errorMessage,statusMessage,result,poolId,bundleId,encryptPassword,expiredAt]);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'DriveTask(id: $id, taskId: $taskId, fileName: $fileName, contentType: $contentType, fileSize: $fileSize, uploadedBytes: $uploadedBytes, totalChunks: $totalChunks, uploadedChunks: $uploadedChunks, status: $status, createdAt: $createdAt, updatedAt: $updatedAt, type: $type, transmissionProgress: $transmissionProgress, errorMessage: $errorMessage, statusMessage: $statusMessage, result: $result, poolId: $poolId, bundleId: $bundleId, encryptPassword: $encryptPassword, expiredAt: $expiredAt)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class $DriveTaskCopyWith<$Res> {
|
||||||
|
factory $DriveTaskCopyWith(DriveTask value, $Res Function(DriveTask) _then) = _$DriveTaskCopyWithImpl;
|
||||||
|
@useResult
|
||||||
|
$Res call({
|
||||||
|
String id, String taskId, String fileName, String contentType, int fileSize, int uploadedBytes, int totalChunks, int uploadedChunks, DriveTaskStatus status, DateTime createdAt, DateTime updatedAt, String type, double? transmissionProgress, String? errorMessage, String? statusMessage, SnCloudFile? result, String? poolId, String? bundleId, String? encryptPassword, String? expiredAt
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
$SnCloudFileCopyWith<$Res>? get result;
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class _$DriveTaskCopyWithImpl<$Res>
|
||||||
|
implements $DriveTaskCopyWith<$Res> {
|
||||||
|
_$DriveTaskCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final DriveTask _self;
|
||||||
|
final $Res Function(DriveTask) _then;
|
||||||
|
|
||||||
|
/// Create a copy of DriveTask
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? taskId = null,Object? fileName = null,Object? contentType = null,Object? fileSize = null,Object? uploadedBytes = null,Object? totalChunks = null,Object? uploadedChunks = null,Object? status = null,Object? createdAt = null,Object? updatedAt = null,Object? type = null,Object? transmissionProgress = freezed,Object? errorMessage = freezed,Object? statusMessage = freezed,Object? result = freezed,Object? poolId = freezed,Object? bundleId = freezed,Object? encryptPassword = freezed,Object? expiredAt = freezed,}) {
|
||||||
|
return _then(_self.copyWith(
|
||||||
|
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,taskId: null == taskId ? _self.taskId : taskId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,fileName: null == fileName ? _self.fileName : fileName // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,contentType: null == contentType ? _self.contentType : contentType // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,fileSize: null == fileSize ? _self.fileSize : fileSize // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,uploadedBytes: null == uploadedBytes ? _self.uploadedBytes : uploadedBytes // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,totalChunks: null == totalChunks ? _self.totalChunks : totalChunks // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,uploadedChunks: null == uploadedChunks ? _self.uploadedChunks : uploadedChunks // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DriveTaskStatus,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,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,transmissionProgress: freezed == transmissionProgress ? _self.transmissionProgress : transmissionProgress // ignore: cast_nullable_to_non_nullable
|
||||||
|
as double?,errorMessage: freezed == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,statusMessage: freezed == statusMessage ? _self.statusMessage : statusMessage // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,result: freezed == result ? _self.result : result // ignore: cast_nullable_to_non_nullable
|
||||||
|
as SnCloudFile?,poolId: freezed == poolId ? _self.poolId : poolId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,bundleId: freezed == bundleId ? _self.bundleId : bundleId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,encryptPassword: freezed == encryptPassword ? _self.encryptPassword : encryptPassword // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
/// Create a copy of DriveTask
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnCloudFileCopyWith<$Res>? get result {
|
||||||
|
if (_self.result == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $SnCloudFileCopyWith<$Res>(_self.result!, (value) {
|
||||||
|
return _then(_self.copyWith(result: value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Adds pattern-matching-related methods to [DriveTask].
|
||||||
|
extension DriveTaskPatterns on DriveTask {
|
||||||
|
/// 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( _DriveTask value)? $default,{required TResult orElse(),}){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _DriveTask() 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( _DriveTask value) $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _DriveTask():
|
||||||
|
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( _DriveTask value)? $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _DriveTask() 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 taskId, String fileName, String contentType, int fileSize, int uploadedBytes, int totalChunks, int uploadedChunks, DriveTaskStatus status, DateTime createdAt, DateTime updatedAt, String type, double? transmissionProgress, String? errorMessage, String? statusMessage, SnCloudFile? result, String? poolId, String? bundleId, String? encryptPassword, String? expiredAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _DriveTask() when $default != null:
|
||||||
|
return $default(_that.id,_that.taskId,_that.fileName,_that.contentType,_that.fileSize,_that.uploadedBytes,_that.totalChunks,_that.uploadedChunks,_that.status,_that.createdAt,_that.updatedAt,_that.type,_that.transmissionProgress,_that.errorMessage,_that.statusMessage,_that.result,_that.poolId,_that.bundleId,_that.encryptPassword,_that.expiredAt);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 taskId, String fileName, String contentType, int fileSize, int uploadedBytes, int totalChunks, int uploadedChunks, DriveTaskStatus status, DateTime createdAt, DateTime updatedAt, String type, double? transmissionProgress, String? errorMessage, String? statusMessage, SnCloudFile? result, String? poolId, String? bundleId, String? encryptPassword, String? expiredAt) $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _DriveTask():
|
||||||
|
return $default(_that.id,_that.taskId,_that.fileName,_that.contentType,_that.fileSize,_that.uploadedBytes,_that.totalChunks,_that.uploadedChunks,_that.status,_that.createdAt,_that.updatedAt,_that.type,_that.transmissionProgress,_that.errorMessage,_that.statusMessage,_that.result,_that.poolId,_that.bundleId,_that.encryptPassword,_that.expiredAt);}
|
||||||
|
}
|
||||||
|
/// 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 taskId, String fileName, String contentType, int fileSize, int uploadedBytes, int totalChunks, int uploadedChunks, DriveTaskStatus status, DateTime createdAt, DateTime updatedAt, String type, double? transmissionProgress, String? errorMessage, String? statusMessage, SnCloudFile? result, String? poolId, String? bundleId, String? encryptPassword, String? expiredAt)? $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _DriveTask() when $default != null:
|
||||||
|
return $default(_that.id,_that.taskId,_that.fileName,_that.contentType,_that.fileSize,_that.uploadedBytes,_that.totalChunks,_that.uploadedChunks,_that.status,_that.createdAt,_that.updatedAt,_that.type,_that.transmissionProgress,_that.errorMessage,_that.statusMessage,_that.result,_that.poolId,_that.bundleId,_that.encryptPassword,_that.expiredAt);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
|
||||||
|
class _DriveTask extends DriveTask {
|
||||||
|
const _DriveTask({required this.id, required this.taskId, required this.fileName, required this.contentType, required this.fileSize, required this.uploadedBytes, required this.totalChunks, required this.uploadedChunks, required this.status, required this.createdAt, required this.updatedAt, required this.type, this.transmissionProgress, this.errorMessage, this.statusMessage, this.result, this.poolId, this.bundleId, this.encryptPassword, this.expiredAt}): super._();
|
||||||
|
factory _DriveTask.fromJson(Map<String, dynamic> json) => _$DriveTaskFromJson(json);
|
||||||
|
|
||||||
|
@override final String id;
|
||||||
|
@override final String taskId;
|
||||||
|
@override final String fileName;
|
||||||
|
@override final String contentType;
|
||||||
|
@override final int fileSize;
|
||||||
|
@override final int uploadedBytes;
|
||||||
|
@override final int totalChunks;
|
||||||
|
@override final int uploadedChunks;
|
||||||
|
@override final DriveTaskStatus status;
|
||||||
|
@override final DateTime createdAt;
|
||||||
|
@override final DateTime updatedAt;
|
||||||
|
@override final String type;
|
||||||
|
// Task type (e.g., 'FileUpload')
|
||||||
|
@override final double? transmissionProgress;
|
||||||
|
// Local file upload progress (0.0-1.0)
|
||||||
|
@override final String? errorMessage;
|
||||||
|
@override final String? statusMessage;
|
||||||
|
@override final SnCloudFile? result;
|
||||||
|
@override final String? poolId;
|
||||||
|
@override final String? bundleId;
|
||||||
|
@override final String? encryptPassword;
|
||||||
|
@override final String? expiredAt;
|
||||||
|
|
||||||
|
/// Create a copy of DriveTask
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$DriveTaskCopyWith<_DriveTask> get copyWith => __$DriveTaskCopyWithImpl<_DriveTask>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$DriveTaskToJson(this, );
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is _DriveTask&&(identical(other.id, id) || other.id == id)&&(identical(other.taskId, taskId) || other.taskId == taskId)&&(identical(other.fileName, fileName) || other.fileName == fileName)&&(identical(other.contentType, contentType) || other.contentType == contentType)&&(identical(other.fileSize, fileSize) || other.fileSize == fileSize)&&(identical(other.uploadedBytes, uploadedBytes) || other.uploadedBytes == uploadedBytes)&&(identical(other.totalChunks, totalChunks) || other.totalChunks == totalChunks)&&(identical(other.uploadedChunks, uploadedChunks) || other.uploadedChunks == uploadedChunks)&&(identical(other.status, status) || other.status == status)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.type, type) || other.type == type)&&(identical(other.transmissionProgress, transmissionProgress) || other.transmissionProgress == transmissionProgress)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.statusMessage, statusMessage) || other.statusMessage == statusMessage)&&(identical(other.result, result) || other.result == result)&&(identical(other.poolId, poolId) || other.poolId == poolId)&&(identical(other.bundleId, bundleId) || other.bundleId == bundleId)&&(identical(other.encryptPassword, encryptPassword) || other.encryptPassword == encryptPassword)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hashAll([runtimeType,id,taskId,fileName,contentType,fileSize,uploadedBytes,totalChunks,uploadedChunks,status,createdAt,updatedAt,type,transmissionProgress,errorMessage,statusMessage,result,poolId,bundleId,encryptPassword,expiredAt]);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'DriveTask(id: $id, taskId: $taskId, fileName: $fileName, contentType: $contentType, fileSize: $fileSize, uploadedBytes: $uploadedBytes, totalChunks: $totalChunks, uploadedChunks: $uploadedChunks, status: $status, createdAt: $createdAt, updatedAt: $updatedAt, type: $type, transmissionProgress: $transmissionProgress, errorMessage: $errorMessage, statusMessage: $statusMessage, result: $result, poolId: $poolId, bundleId: $bundleId, encryptPassword: $encryptPassword, expiredAt: $expiredAt)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class _$DriveTaskCopyWith<$Res> implements $DriveTaskCopyWith<$Res> {
|
||||||
|
factory _$DriveTaskCopyWith(_DriveTask value, $Res Function(_DriveTask) _then) = __$DriveTaskCopyWithImpl;
|
||||||
|
@override @useResult
|
||||||
|
$Res call({
|
||||||
|
String id, String taskId, String fileName, String contentType, int fileSize, int uploadedBytes, int totalChunks, int uploadedChunks, DriveTaskStatus status, DateTime createdAt, DateTime updatedAt, String type, double? transmissionProgress, String? errorMessage, String? statusMessage, SnCloudFile? result, String? poolId, String? bundleId, String? encryptPassword, String? expiredAt
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
@override $SnCloudFileCopyWith<$Res>? get result;
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class __$DriveTaskCopyWithImpl<$Res>
|
||||||
|
implements _$DriveTaskCopyWith<$Res> {
|
||||||
|
__$DriveTaskCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final _DriveTask _self;
|
||||||
|
final $Res Function(_DriveTask) _then;
|
||||||
|
|
||||||
|
/// Create a copy of DriveTask
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? taskId = null,Object? fileName = null,Object? contentType = null,Object? fileSize = null,Object? uploadedBytes = null,Object? totalChunks = null,Object? uploadedChunks = null,Object? status = null,Object? createdAt = null,Object? updatedAt = null,Object? type = null,Object? transmissionProgress = freezed,Object? errorMessage = freezed,Object? statusMessage = freezed,Object? result = freezed,Object? poolId = freezed,Object? bundleId = freezed,Object? encryptPassword = freezed,Object? expiredAt = freezed,}) {
|
||||||
|
return _then(_DriveTask(
|
||||||
|
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,taskId: null == taskId ? _self.taskId : taskId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,fileName: null == fileName ? _self.fileName : fileName // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,contentType: null == contentType ? _self.contentType : contentType // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,fileSize: null == fileSize ? _self.fileSize : fileSize // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,uploadedBytes: null == uploadedBytes ? _self.uploadedBytes : uploadedBytes // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,totalChunks: null == totalChunks ? _self.totalChunks : totalChunks // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,uploadedChunks: null == uploadedChunks ? _self.uploadedChunks : uploadedChunks // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DriveTaskStatus,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,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,transmissionProgress: freezed == transmissionProgress ? _self.transmissionProgress : transmissionProgress // ignore: cast_nullable_to_non_nullable
|
||||||
|
as double?,errorMessage: freezed == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,statusMessage: freezed == statusMessage ? _self.statusMessage : statusMessage // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,result: freezed == result ? _self.result : result // ignore: cast_nullable_to_non_nullable
|
||||||
|
as SnCloudFile?,poolId: freezed == poolId ? _self.poolId : poolId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,bundleId: freezed == bundleId ? _self.bundleId : bundleId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,encryptPassword: freezed == encryptPassword ? _self.encryptPassword : encryptPassword // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a copy of DriveTask
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnCloudFileCopyWith<$Res>? get result {
|
||||||
|
if (_self.result == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $SnCloudFileCopyWith<$Res>(_self.result!, (value) {
|
||||||
|
return _then(_self.copyWith(result: value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// dart format on
|
||||||
67
lib/models/drive_task.g.dart
Normal file
67
lib/models/drive_task.g.dart
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'drive_task.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
_DriveTask _$DriveTaskFromJson(Map<String, dynamic> json) => _DriveTask(
|
||||||
|
id: json['id'] as String,
|
||||||
|
taskId: json['task_id'] as String,
|
||||||
|
fileName: json['file_name'] as String,
|
||||||
|
contentType: json['content_type'] as String,
|
||||||
|
fileSize: (json['file_size'] as num).toInt(),
|
||||||
|
uploadedBytes: (json['uploaded_bytes'] as num).toInt(),
|
||||||
|
totalChunks: (json['total_chunks'] as num).toInt(),
|
||||||
|
uploadedChunks: (json['uploaded_chunks'] as num).toInt(),
|
||||||
|
status: $enumDecode(_$DriveTaskStatusEnumMap, json['status']),
|
||||||
|
createdAt: DateTime.parse(json['created_at'] as String),
|
||||||
|
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||||
|
type: json['type'] as String,
|
||||||
|
transmissionProgress: (json['transmission_progress'] as num?)?.toDouble(),
|
||||||
|
errorMessage: json['error_message'] as String?,
|
||||||
|
statusMessage: json['status_message'] as String?,
|
||||||
|
result:
|
||||||
|
json['result'] == null
|
||||||
|
? null
|
||||||
|
: SnCloudFile.fromJson(json['result'] as Map<String, dynamic>),
|
||||||
|
poolId: json['pool_id'] as String?,
|
||||||
|
bundleId: json['bundle_id'] as String?,
|
||||||
|
encryptPassword: json['encrypt_password'] as String?,
|
||||||
|
expiredAt: json['expired_at'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$DriveTaskToJson(_DriveTask instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'task_id': instance.taskId,
|
||||||
|
'file_name': instance.fileName,
|
||||||
|
'content_type': instance.contentType,
|
||||||
|
'file_size': instance.fileSize,
|
||||||
|
'uploaded_bytes': instance.uploadedBytes,
|
||||||
|
'total_chunks': instance.totalChunks,
|
||||||
|
'uploaded_chunks': instance.uploadedChunks,
|
||||||
|
'status': _$DriveTaskStatusEnumMap[instance.status]!,
|
||||||
|
'created_at': instance.createdAt.toIso8601String(),
|
||||||
|
'updated_at': instance.updatedAt.toIso8601String(),
|
||||||
|
'type': instance.type,
|
||||||
|
'transmission_progress': instance.transmissionProgress,
|
||||||
|
'error_message': instance.errorMessage,
|
||||||
|
'status_message': instance.statusMessage,
|
||||||
|
'result': instance.result?.toJson(),
|
||||||
|
'pool_id': instance.poolId,
|
||||||
|
'bundle_id': instance.bundleId,
|
||||||
|
'encrypt_password': instance.encryptPassword,
|
||||||
|
'expired_at': instance.expiredAt,
|
||||||
|
};
|
||||||
|
|
||||||
|
const _$DriveTaskStatusEnumMap = {
|
||||||
|
DriveTaskStatus.pending: 'pending',
|
||||||
|
DriveTaskStatus.inProgress: 'inProgress',
|
||||||
|
DriveTaskStatus.paused: 'paused',
|
||||||
|
DriveTaskStatus.completed: 'completed',
|
||||||
|
DriveTaskStatus.failed: 'failed',
|
||||||
|
DriveTaskStatus.expired: 'expired',
|
||||||
|
DriveTaskStatus.cancelled: 'cancelled',
|
||||||
|
};
|
||||||
@@ -60,3 +60,19 @@ sealed class SnCloudFile with _$SnCloudFile {
|
|||||||
factory SnCloudFile.fromJson(Map<String, dynamic> json) =>
|
factory SnCloudFile.fromJson(Map<String, dynamic> json) =>
|
||||||
_$SnCloudFileFromJson(json);
|
_$SnCloudFileFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
sealed class SnCloudFileIndex with _$SnCloudFileIndex {
|
||||||
|
const factory SnCloudFileIndex({
|
||||||
|
required String id,
|
||||||
|
required String path,
|
||||||
|
required String fileId,
|
||||||
|
required SnCloudFile file,
|
||||||
|
required DateTime createdAt,
|
||||||
|
required DateTime updatedAt,
|
||||||
|
required DateTime? deletedAt,
|
||||||
|
}) = _SnCloudFileIndex;
|
||||||
|
|
||||||
|
factory SnCloudFileIndex.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SnCloudFileIndexFromJson(json);
|
||||||
|
}
|
||||||
|
|||||||
@@ -622,4 +622,297 @@ $SnFilePoolCopyWith<$Res>? get pool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$SnCloudFileIndex {
|
||||||
|
|
||||||
|
String get id; String get path; String get fileId; SnCloudFile get file; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
|
||||||
|
/// Create a copy of SnCloudFileIndex
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnCloudFileIndexCopyWith<SnCloudFileIndex> get copyWith => _$SnCloudFileIndexCopyWithImpl<SnCloudFileIndex>(this as SnCloudFileIndex, _$identity);
|
||||||
|
|
||||||
|
/// Serializes this SnCloudFileIndex to a JSON map.
|
||||||
|
Map<String, dynamic> toJson();
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnCloudFileIndex&&(identical(other.id, id) || other.id == id)&&(identical(other.path, path) || other.path == path)&&(identical(other.fileId, fileId) || other.fileId == fileId)&&(identical(other.file, file) || other.file == file)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,id,path,fileId,file,createdAt,updatedAt,deletedAt);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SnCloudFileIndex(id: $id, path: $path, fileId: $fileId, file: $file, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class $SnCloudFileIndexCopyWith<$Res> {
|
||||||
|
factory $SnCloudFileIndexCopyWith(SnCloudFileIndex value, $Res Function(SnCloudFileIndex) _then) = _$SnCloudFileIndexCopyWithImpl;
|
||||||
|
@useResult
|
||||||
|
$Res call({
|
||||||
|
String id, String path, String fileId, SnCloudFile file, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
$SnCloudFileCopyWith<$Res> get file;
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class _$SnCloudFileIndexCopyWithImpl<$Res>
|
||||||
|
implements $SnCloudFileIndexCopyWith<$Res> {
|
||||||
|
_$SnCloudFileIndexCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final SnCloudFileIndex _self;
|
||||||
|
final $Res Function(SnCloudFileIndex) _then;
|
||||||
|
|
||||||
|
/// Create a copy of SnCloudFileIndex
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? path = null,Object? fileId = null,Object? file = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||||
|
return _then(_self.copyWith(
|
||||||
|
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,path: null == path ? _self.path : path // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,fileId: null == fileId ? _self.fileId : fileId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,file: null == file ? _self.file : file // ignore: cast_nullable_to_non_nullable
|
||||||
|
as SnCloudFile,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
/// Create a copy of SnCloudFileIndex
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnCloudFileCopyWith<$Res> get file {
|
||||||
|
|
||||||
|
return $SnCloudFileCopyWith<$Res>(_self.file, (value) {
|
||||||
|
return _then(_self.copyWith(file: value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Adds pattern-matching-related methods to [SnCloudFileIndex].
|
||||||
|
extension SnCloudFileIndexPatterns on SnCloudFileIndex {
|
||||||
|
/// 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( _SnCloudFileIndex value)? $default,{required TResult orElse(),}){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnCloudFileIndex() 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( _SnCloudFileIndex value) $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnCloudFileIndex():
|
||||||
|
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( _SnCloudFileIndex value)? $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnCloudFileIndex() 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 path, String fileId, SnCloudFile file, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnCloudFileIndex() when $default != null:
|
||||||
|
return $default(_that.id,_that.path,_that.fileId,_that.file,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||||
|
return orElse();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A `switch`-like method, using callbacks.
|
||||||
|
///
|
||||||
|
/// As opposed to `map`, this offers destructuring.
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case Subclass2(:final field2):
|
||||||
|
/// return ...;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String path, String fileId, SnCloudFile file, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnCloudFileIndex():
|
||||||
|
return $default(_that.id,_that.path,_that.fileId,_that.file,_that.createdAt,_that.updatedAt,_that.deletedAt);}
|
||||||
|
}
|
||||||
|
/// A variant of `when` that fallback to returning `null`
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return null;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String path, String fileId, SnCloudFile file, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnCloudFileIndex() when $default != null:
|
||||||
|
return $default(_that.id,_that.path,_that.fileId,_that.file,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
|
||||||
|
class _SnCloudFileIndex implements SnCloudFileIndex {
|
||||||
|
const _SnCloudFileIndex({required this.id, required this.path, required this.fileId, required this.file, required this.createdAt, required this.updatedAt, required this.deletedAt});
|
||||||
|
factory _SnCloudFileIndex.fromJson(Map<String, dynamic> json) => _$SnCloudFileIndexFromJson(json);
|
||||||
|
|
||||||
|
@override final String id;
|
||||||
|
@override final String path;
|
||||||
|
@override final String fileId;
|
||||||
|
@override final SnCloudFile file;
|
||||||
|
@override final DateTime createdAt;
|
||||||
|
@override final DateTime updatedAt;
|
||||||
|
@override final DateTime? deletedAt;
|
||||||
|
|
||||||
|
/// Create a copy of SnCloudFileIndex
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$SnCloudFileIndexCopyWith<_SnCloudFileIndex> get copyWith => __$SnCloudFileIndexCopyWithImpl<_SnCloudFileIndex>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$SnCloudFileIndexToJson(this, );
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnCloudFileIndex&&(identical(other.id, id) || other.id == id)&&(identical(other.path, path) || other.path == path)&&(identical(other.fileId, fileId) || other.fileId == fileId)&&(identical(other.file, file) || other.file == file)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,id,path,fileId,file,createdAt,updatedAt,deletedAt);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SnCloudFileIndex(id: $id, path: $path, fileId: $fileId, file: $file, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class _$SnCloudFileIndexCopyWith<$Res> implements $SnCloudFileIndexCopyWith<$Res> {
|
||||||
|
factory _$SnCloudFileIndexCopyWith(_SnCloudFileIndex value, $Res Function(_SnCloudFileIndex) _then) = __$SnCloudFileIndexCopyWithImpl;
|
||||||
|
@override @useResult
|
||||||
|
$Res call({
|
||||||
|
String id, String path, String fileId, SnCloudFile file, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
@override $SnCloudFileCopyWith<$Res> get file;
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class __$SnCloudFileIndexCopyWithImpl<$Res>
|
||||||
|
implements _$SnCloudFileIndexCopyWith<$Res> {
|
||||||
|
__$SnCloudFileIndexCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final _SnCloudFileIndex _self;
|
||||||
|
final $Res Function(_SnCloudFileIndex) _then;
|
||||||
|
|
||||||
|
/// Create a copy of SnCloudFileIndex
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? path = null,Object? fileId = null,Object? file = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||||
|
return _then(_SnCloudFileIndex(
|
||||||
|
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,path: null == path ? _self.path : path // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,fileId: null == fileId ? _self.fileId : fileId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,file: null == file ? _self.file : file // ignore: cast_nullable_to_non_nullable
|
||||||
|
as SnCloudFile,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a copy of SnCloudFileIndex
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnCloudFileCopyWith<$Res> get file {
|
||||||
|
|
||||||
|
return $SnCloudFileCopyWith<$Res>(_self.file, (value) {
|
||||||
|
return _then(_self.copyWith(file: value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// dart format on
|
// dart format on
|
||||||
|
|||||||
@@ -78,3 +78,28 @@ Map<String, dynamic> _$SnCloudFileToJson(_SnCloudFile instance) =>
|
|||||||
'updated_at': instance.updatedAt.toIso8601String(),
|
'updated_at': instance.updatedAt.toIso8601String(),
|
||||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_SnCloudFileIndex _$SnCloudFileIndexFromJson(Map<String, dynamic> json) =>
|
||||||
|
_SnCloudFileIndex(
|
||||||
|
id: json['id'] as String,
|
||||||
|
path: json['path'] as String,
|
||||||
|
fileId: json['file_id'] as String,
|
||||||
|
file: SnCloudFile.fromJson(json['file'] 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> _$SnCloudFileIndexToJson(_SnCloudFileIndex instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'path': instance.path,
|
||||||
|
'file_id': instance.fileId,
|
||||||
|
'file': instance.file.toJson(),
|
||||||
|
'created_at': instance.createdAt.toIso8601String(),
|
||||||
|
'updated_at': instance.updatedAt.toIso8601String(),
|
||||||
|
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||||
|
};
|
||||||
|
|||||||
12
lib/models/file_list_item.dart
Normal file
12
lib/models/file_list_item.dart
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
import 'package:island/models/file.dart';
|
||||||
|
|
||||||
|
part 'file_list_item.freezed.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
sealed class FileListItem with _$FileListItem {
|
||||||
|
const factory FileListItem.file(SnCloudFileIndex fileIndex) = FileItem;
|
||||||
|
const factory FileListItem.folder(String folderName) = FolderItem;
|
||||||
|
const factory FileListItem.unindexedFile(SnCloudFile file) =
|
||||||
|
UnindexedFileItem;
|
||||||
|
}
|
||||||
396
lib/models/file_list_item.freezed.dart
Normal file
396
lib/models/file_list_item.freezed.dart
Normal file
@@ -0,0 +1,396 @@
|
|||||||
|
// 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 'file_list_item.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// FreezedGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// dart format off
|
||||||
|
T _$identity<T>(T value) => value;
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$FileListItem {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is FileListItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => runtimeType.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'FileListItem()';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class $FileListItemCopyWith<$Res> {
|
||||||
|
$FileListItemCopyWith(FileListItem _, $Res Function(FileListItem) __);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Adds pattern-matching-related methods to [FileListItem].
|
||||||
|
extension FileListItemPatterns on FileListItem {
|
||||||
|
/// 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( FileItem value)? file,TResult Function( FolderItem value)? folder,TResult Function( UnindexedFileItem value)? unindexedFile,required TResult orElse(),}){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case FileItem() when file != null:
|
||||||
|
return file(_that);case FolderItem() when folder != null:
|
||||||
|
return folder(_that);case UnindexedFileItem() when unindexedFile != null:
|
||||||
|
return unindexedFile(_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?>({required TResult Function( FileItem value) file,required TResult Function( FolderItem value) folder,required TResult Function( UnindexedFileItem value) unindexedFile,}){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case FileItem():
|
||||||
|
return file(_that);case FolderItem():
|
||||||
|
return folder(_that);case UnindexedFileItem():
|
||||||
|
return unindexedFile(_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( FileItem value)? file,TResult? Function( FolderItem value)? folder,TResult? Function( UnindexedFileItem value)? unindexedFile,}){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case FileItem() when file != null:
|
||||||
|
return file(_that);case FolderItem() when folder != null:
|
||||||
|
return folder(_that);case UnindexedFileItem() when unindexedFile != null:
|
||||||
|
return unindexedFile(_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( SnCloudFileIndex fileIndex)? file,TResult Function( String folderName)? folder,TResult Function( SnCloudFile file)? unindexedFile,required TResult orElse(),}) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case FileItem() when file != null:
|
||||||
|
return file(_that.fileIndex);case FolderItem() when folder != null:
|
||||||
|
return folder(_that.folderName);case UnindexedFileItem() when unindexedFile != null:
|
||||||
|
return unindexedFile(_that.file);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?>({required TResult Function( SnCloudFileIndex fileIndex) file,required TResult Function( String folderName) folder,required TResult Function( SnCloudFile file) unindexedFile,}) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case FileItem():
|
||||||
|
return file(_that.fileIndex);case FolderItem():
|
||||||
|
return folder(_that.folderName);case UnindexedFileItem():
|
||||||
|
return unindexedFile(_that.file);}
|
||||||
|
}
|
||||||
|
/// 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( SnCloudFileIndex fileIndex)? file,TResult? Function( String folderName)? folder,TResult? Function( SnCloudFile file)? unindexedFile,}) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case FileItem() when file != null:
|
||||||
|
return file(_that.fileIndex);case FolderItem() when folder != null:
|
||||||
|
return folder(_that.folderName);case UnindexedFileItem() when unindexedFile != null:
|
||||||
|
return unindexedFile(_that.file);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
|
||||||
|
|
||||||
|
class FileItem implements FileListItem {
|
||||||
|
const FileItem(this.fileIndex);
|
||||||
|
|
||||||
|
|
||||||
|
final SnCloudFileIndex fileIndex;
|
||||||
|
|
||||||
|
/// Create a copy of FileListItem
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$FileItemCopyWith<FileItem> get copyWith => _$FileItemCopyWithImpl<FileItem>(this, _$identity);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is FileItem&&(identical(other.fileIndex, fileIndex) || other.fileIndex == fileIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,fileIndex);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'FileListItem.file(fileIndex: $fileIndex)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class $FileItemCopyWith<$Res> implements $FileListItemCopyWith<$Res> {
|
||||||
|
factory $FileItemCopyWith(FileItem value, $Res Function(FileItem) _then) = _$FileItemCopyWithImpl;
|
||||||
|
@useResult
|
||||||
|
$Res call({
|
||||||
|
SnCloudFileIndex fileIndex
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
$SnCloudFileIndexCopyWith<$Res> get fileIndex;
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class _$FileItemCopyWithImpl<$Res>
|
||||||
|
implements $FileItemCopyWith<$Res> {
|
||||||
|
_$FileItemCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final FileItem _self;
|
||||||
|
final $Res Function(FileItem) _then;
|
||||||
|
|
||||||
|
/// Create a copy of FileListItem
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline') $Res call({Object? fileIndex = null,}) {
|
||||||
|
return _then(FileItem(
|
||||||
|
null == fileIndex ? _self.fileIndex : fileIndex // ignore: cast_nullable_to_non_nullable
|
||||||
|
as SnCloudFileIndex,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a copy of FileListItem
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnCloudFileIndexCopyWith<$Res> get fileIndex {
|
||||||
|
|
||||||
|
return $SnCloudFileIndexCopyWith<$Res>(_self.fileIndex, (value) {
|
||||||
|
return _then(_self.copyWith(fileIndex: value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
|
||||||
|
|
||||||
|
class FolderItem implements FileListItem {
|
||||||
|
const FolderItem(this.folderName);
|
||||||
|
|
||||||
|
|
||||||
|
final String folderName;
|
||||||
|
|
||||||
|
/// Create a copy of FileListItem
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$FolderItemCopyWith<FolderItem> get copyWith => _$FolderItemCopyWithImpl<FolderItem>(this, _$identity);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is FolderItem&&(identical(other.folderName, folderName) || other.folderName == folderName));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,folderName);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'FileListItem.folder(folderName: $folderName)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class $FolderItemCopyWith<$Res> implements $FileListItemCopyWith<$Res> {
|
||||||
|
factory $FolderItemCopyWith(FolderItem value, $Res Function(FolderItem) _then) = _$FolderItemCopyWithImpl;
|
||||||
|
@useResult
|
||||||
|
$Res call({
|
||||||
|
String folderName
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class _$FolderItemCopyWithImpl<$Res>
|
||||||
|
implements $FolderItemCopyWith<$Res> {
|
||||||
|
_$FolderItemCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final FolderItem _self;
|
||||||
|
final $Res Function(FolderItem) _then;
|
||||||
|
|
||||||
|
/// Create a copy of FileListItem
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline') $Res call({Object? folderName = null,}) {
|
||||||
|
return _then(FolderItem(
|
||||||
|
null == folderName ? _self.folderName : folderName // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
|
||||||
|
|
||||||
|
class UnindexedFileItem implements FileListItem {
|
||||||
|
const UnindexedFileItem(this.file);
|
||||||
|
|
||||||
|
|
||||||
|
final SnCloudFile file;
|
||||||
|
|
||||||
|
/// Create a copy of FileListItem
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$UnindexedFileItemCopyWith<UnindexedFileItem> get copyWith => _$UnindexedFileItemCopyWithImpl<UnindexedFileItem>(this, _$identity);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is UnindexedFileItem&&(identical(other.file, file) || other.file == file));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,file);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'FileListItem.unindexedFile(file: $file)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class $UnindexedFileItemCopyWith<$Res> implements $FileListItemCopyWith<$Res> {
|
||||||
|
factory $UnindexedFileItemCopyWith(UnindexedFileItem value, $Res Function(UnindexedFileItem) _then) = _$UnindexedFileItemCopyWithImpl;
|
||||||
|
@useResult
|
||||||
|
$Res call({
|
||||||
|
SnCloudFile file
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
$SnCloudFileCopyWith<$Res> get file;
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class _$UnindexedFileItemCopyWithImpl<$Res>
|
||||||
|
implements $UnindexedFileItemCopyWith<$Res> {
|
||||||
|
_$UnindexedFileItemCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final UnindexedFileItem _self;
|
||||||
|
final $Res Function(UnindexedFileItem) _then;
|
||||||
|
|
||||||
|
/// Create a copy of FileListItem
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline') $Res call({Object? file = null,}) {
|
||||||
|
return _then(UnindexedFileItem(
|
||||||
|
null == file ? _self.file : file // ignore: cast_nullable_to_non_nullable
|
||||||
|
as SnCloudFile,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a copy of FileListItem
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnCloudFileCopyWith<$Res> get file {
|
||||||
|
|
||||||
|
return $SnCloudFileCopyWith<$Res>(_self.file, (value) {
|
||||||
|
return _then(_self.copyWith(file: value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// dart format on
|
||||||
19
lib/models/folder.dart
Normal file
19
lib/models/folder.dart
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'folder.freezed.dart';
|
||||||
|
part 'folder.g.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
sealed class SnCloudFolder with _$SnCloudFolder {
|
||||||
|
const factory SnCloudFolder({
|
||||||
|
required String id,
|
||||||
|
required String name,
|
||||||
|
required String? parentFolderId,
|
||||||
|
required String accountId,
|
||||||
|
required DateTime createdAt,
|
||||||
|
required DateTime updatedAt,
|
||||||
|
}) = _SnCloudFolder;
|
||||||
|
|
||||||
|
factory SnCloudFolder.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SnCloudFolderFromJson(json);
|
||||||
|
}
|
||||||
286
lib/models/folder.freezed.dart
Normal file
286
lib/models/folder.freezed.dart
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
// 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 'folder.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// FreezedGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// dart format off
|
||||||
|
T _$identity<T>(T value) => value;
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$SnCloudFolder {
|
||||||
|
|
||||||
|
String get id; String get name; String? get parentFolderId; String get accountId; DateTime get createdAt; DateTime get updatedAt;
|
||||||
|
/// Create a copy of SnCloudFolder
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnCloudFolderCopyWith<SnCloudFolder> get copyWith => _$SnCloudFolderCopyWithImpl<SnCloudFolder>(this as SnCloudFolder, _$identity);
|
||||||
|
|
||||||
|
/// Serializes this SnCloudFolder to a JSON map.
|
||||||
|
Map<String, dynamic> toJson();
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnCloudFolder&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.parentFolderId, parentFolderId) || other.parentFolderId == parentFolderId)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,id,name,parentFolderId,accountId,createdAt,updatedAt);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SnCloudFolder(id: $id, name: $name, parentFolderId: $parentFolderId, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class $SnCloudFolderCopyWith<$Res> {
|
||||||
|
factory $SnCloudFolderCopyWith(SnCloudFolder value, $Res Function(SnCloudFolder) _then) = _$SnCloudFolderCopyWithImpl;
|
||||||
|
@useResult
|
||||||
|
$Res call({
|
||||||
|
String id, String name, String? parentFolderId, String accountId, DateTime createdAt, DateTime updatedAt
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class _$SnCloudFolderCopyWithImpl<$Res>
|
||||||
|
implements $SnCloudFolderCopyWith<$Res> {
|
||||||
|
_$SnCloudFolderCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final SnCloudFolder _self;
|
||||||
|
final $Res Function(SnCloudFolder) _then;
|
||||||
|
|
||||||
|
/// Create a copy of SnCloudFolder
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? parentFolderId = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,}) {
|
||||||
|
return _then(_self.copyWith(
|
||||||
|
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,parentFolderId: freezed == parentFolderId ? _self.parentFolderId : parentFolderId // 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,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Adds pattern-matching-related methods to [SnCloudFolder].
|
||||||
|
extension SnCloudFolderPatterns on SnCloudFolder {
|
||||||
|
/// 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( _SnCloudFolder value)? $default,{required TResult orElse(),}){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnCloudFolder() 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( _SnCloudFolder value) $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnCloudFolder():
|
||||||
|
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( _SnCloudFolder value)? $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnCloudFolder() 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 name, String? parentFolderId, String accountId, DateTime createdAt, DateTime updatedAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnCloudFolder() when $default != null:
|
||||||
|
return $default(_that.id,_that.name,_that.parentFolderId,_that.accountId,_that.createdAt,_that.updatedAt);case _:
|
||||||
|
return orElse();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A `switch`-like method, using callbacks.
|
||||||
|
///
|
||||||
|
/// As opposed to `map`, this offers destructuring.
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case Subclass2(:final field2):
|
||||||
|
/// return ...;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String name, String? parentFolderId, String accountId, DateTime createdAt, DateTime updatedAt) $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnCloudFolder():
|
||||||
|
return $default(_that.id,_that.name,_that.parentFolderId,_that.accountId,_that.createdAt,_that.updatedAt);}
|
||||||
|
}
|
||||||
|
/// A variant of `when` that fallback to returning `null`
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return null;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String name, String? parentFolderId, String accountId, DateTime createdAt, DateTime updatedAt)? $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnCloudFolder() when $default != null:
|
||||||
|
return $default(_that.id,_that.name,_that.parentFolderId,_that.accountId,_that.createdAt,_that.updatedAt);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
|
||||||
|
class _SnCloudFolder implements SnCloudFolder {
|
||||||
|
const _SnCloudFolder({required this.id, required this.name, required this.parentFolderId, required this.accountId, required this.createdAt, required this.updatedAt});
|
||||||
|
factory _SnCloudFolder.fromJson(Map<String, dynamic> json) => _$SnCloudFolderFromJson(json);
|
||||||
|
|
||||||
|
@override final String id;
|
||||||
|
@override final String name;
|
||||||
|
@override final String? parentFolderId;
|
||||||
|
@override final String accountId;
|
||||||
|
@override final DateTime createdAt;
|
||||||
|
@override final DateTime updatedAt;
|
||||||
|
|
||||||
|
/// Create a copy of SnCloudFolder
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$SnCloudFolderCopyWith<_SnCloudFolder> get copyWith => __$SnCloudFolderCopyWithImpl<_SnCloudFolder>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$SnCloudFolderToJson(this, );
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnCloudFolder&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.parentFolderId, parentFolderId) || other.parentFolderId == parentFolderId)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,id,name,parentFolderId,accountId,createdAt,updatedAt);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SnCloudFolder(id: $id, name: $name, parentFolderId: $parentFolderId, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class _$SnCloudFolderCopyWith<$Res> implements $SnCloudFolderCopyWith<$Res> {
|
||||||
|
factory _$SnCloudFolderCopyWith(_SnCloudFolder value, $Res Function(_SnCloudFolder) _then) = __$SnCloudFolderCopyWithImpl;
|
||||||
|
@override @useResult
|
||||||
|
$Res call({
|
||||||
|
String id, String name, String? parentFolderId, String accountId, DateTime createdAt, DateTime updatedAt
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class __$SnCloudFolderCopyWithImpl<$Res>
|
||||||
|
implements _$SnCloudFolderCopyWith<$Res> {
|
||||||
|
__$SnCloudFolderCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final _SnCloudFolder _self;
|
||||||
|
final $Res Function(_SnCloudFolder) _then;
|
||||||
|
|
||||||
|
/// Create a copy of SnCloudFolder
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? parentFolderId = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,}) {
|
||||||
|
return _then(_SnCloudFolder(
|
||||||
|
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,parentFolderId: freezed == parentFolderId ? _self.parentFolderId : parentFolderId // 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,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// dart format on
|
||||||
27
lib/models/folder.g.dart
Normal file
27
lib/models/folder.g.dart
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'folder.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
_SnCloudFolder _$SnCloudFolderFromJson(Map<String, dynamic> json) =>
|
||||||
|
_SnCloudFolder(
|
||||||
|
id: json['id'] as String,
|
||||||
|
name: json['name'] as String,
|
||||||
|
parentFolderId: json['parent_folder_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),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$SnCloudFolderToJson(_SnCloudFolder instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'name': instance.name,
|
||||||
|
'parent_folder_id': instance.parentFolderId,
|
||||||
|
'account_id': instance.accountId,
|
||||||
|
'created_at': instance.createdAt.toIso8601String(),
|
||||||
|
'updated_at': instance.updatedAt.toIso8601String(),
|
||||||
|
};
|
||||||
@@ -46,6 +46,18 @@ sealed class SnPoll with _$SnPoll {
|
|||||||
}) = _SnPoll;
|
}) = _SnPoll;
|
||||||
|
|
||||||
factory SnPoll.fromJson(Map<String, dynamic> json) => _$SnPollFromJson(json);
|
factory SnPoll.fromJson(Map<String, dynamic> json) => _$SnPollFromJson(json);
|
||||||
|
|
||||||
|
factory SnPoll.fromPollWithStats(SnPollWithStats pollWithStats) => SnPoll(
|
||||||
|
id: pollWithStats.id,
|
||||||
|
questions: pollWithStats.questions,
|
||||||
|
title: pollWithStats.title,
|
||||||
|
description: pollWithStats.description,
|
||||||
|
endedAt: pollWithStats.endedAt,
|
||||||
|
publisherId: pollWithStats.publisherId,
|
||||||
|
createdAt: pollWithStats.createdAt,
|
||||||
|
updatedAt: pollWithStats.updatedAt,
|
||||||
|
deletedAt: pollWithStats.deletedAt,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
|
|||||||
41
lib/models/publication_site.dart
Normal file
41
lib/models/publication_site.dart
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'publication_site.freezed.dart';
|
||||||
|
part 'publication_site.g.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
sealed class SnPublicationSite with _$SnPublicationSite {
|
||||||
|
const factory SnPublicationSite({
|
||||||
|
required String id,
|
||||||
|
required String slug,
|
||||||
|
required String name,
|
||||||
|
String? description,
|
||||||
|
int? mode,
|
||||||
|
required String publisherId,
|
||||||
|
required String accountId,
|
||||||
|
required DateTime createdAt,
|
||||||
|
required DateTime updatedAt,
|
||||||
|
required List<SnPublicationPage> pages,
|
||||||
|
}) = _SnPublicationSite;
|
||||||
|
|
||||||
|
factory SnPublicationSite.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SnPublicationSiteFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
sealed class SnPublicationPage with _$SnPublicationPage {
|
||||||
|
const factory SnPublicationPage({
|
||||||
|
required String id,
|
||||||
|
String? preset,
|
||||||
|
String? path,
|
||||||
|
Map<String, dynamic>? config,
|
||||||
|
required String siteId,
|
||||||
|
required DateTime createdAt,
|
||||||
|
required DateTime updatedAt,
|
||||||
|
}) = _SnPublicationPage;
|
||||||
|
|
||||||
|
factory SnPublicationPage.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SnPublicationPageFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PublicationPagePreset { landing, profile, posts, custom }
|
||||||
587
lib/models/publication_site.freezed.dart
Normal file
587
lib/models/publication_site.freezed.dart
Normal file
@@ -0,0 +1,587 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// coverage:ignore-file
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||||
|
|
||||||
|
part of 'publication_site.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// FreezedGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// dart format off
|
||||||
|
T _$identity<T>(T value) => value;
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$SnPublicationSite {
|
||||||
|
|
||||||
|
String get id; String get slug; String get name; String? get description; int? get mode; String get publisherId; String get accountId; DateTime get createdAt; DateTime get updatedAt; List<SnPublicationPage> get pages;
|
||||||
|
/// Create a copy of SnPublicationSite
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnPublicationSiteCopyWith<SnPublicationSite> get copyWith => _$SnPublicationSiteCopyWithImpl<SnPublicationSite>(this as SnPublicationSite, _$identity);
|
||||||
|
|
||||||
|
/// Serializes this SnPublicationSite to a JSON map.
|
||||||
|
Map<String, dynamic> toJson();
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPublicationSite&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.mode, mode) || other.mode == mode)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&const DeepCollectionEquality().equals(other.pages, pages));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,id,slug,name,description,mode,publisherId,accountId,createdAt,updatedAt,const DeepCollectionEquality().hash(pages));
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SnPublicationSite(id: $id, slug: $slug, name: $name, description: $description, mode: $mode, publisherId: $publisherId, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, pages: $pages)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class $SnPublicationSiteCopyWith<$Res> {
|
||||||
|
factory $SnPublicationSiteCopyWith(SnPublicationSite value, $Res Function(SnPublicationSite) _then) = _$SnPublicationSiteCopyWithImpl;
|
||||||
|
@useResult
|
||||||
|
$Res call({
|
||||||
|
String id, String slug, String name, String? description, int? mode, String publisherId, String accountId, DateTime createdAt, DateTime updatedAt, List<SnPublicationPage> pages
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class _$SnPublicationSiteCopyWithImpl<$Res>
|
||||||
|
implements $SnPublicationSiteCopyWith<$Res> {
|
||||||
|
_$SnPublicationSiteCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final SnPublicationSite _self;
|
||||||
|
final $Res Function(SnPublicationSite) _then;
|
||||||
|
|
||||||
|
/// Create a copy of SnPublicationSite
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? slug = null,Object? name = null,Object? description = freezed,Object? mode = freezed,Object? publisherId = null,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? pages = null,}) {
|
||||||
|
return _then(_self.copyWith(
|
||||||
|
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,slug: null == slug ? _self.slug : slug // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,mode: freezed == mode ? _self.mode : mode // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int?,publisherId: null == publisherId ? _self.publisherId : publisherId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,pages: null == pages ? _self.pages : pages // ignore: cast_nullable_to_non_nullable
|
||||||
|
as List<SnPublicationPage>,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Adds pattern-matching-related methods to [SnPublicationSite].
|
||||||
|
extension SnPublicationSitePatterns on SnPublicationSite {
|
||||||
|
/// A variant of `map` that fallback to returning `orElse`.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return orElse();
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnPublicationSite value)? $default,{required TResult orElse(),}){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnPublicationSite() when $default != null:
|
||||||
|
return $default(_that);case _:
|
||||||
|
return orElse();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A `switch`-like method, using callbacks.
|
||||||
|
///
|
||||||
|
/// Callbacks receives the raw object, upcasted.
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case final Subclass2 value:
|
||||||
|
/// return ...;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnPublicationSite value) $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnPublicationSite():
|
||||||
|
return $default(_that);}
|
||||||
|
}
|
||||||
|
/// A variant of `map` that fallback to returning `null`.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return null;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnPublicationSite value)? $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnPublicationSite() when $default != null:
|
||||||
|
return $default(_that);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A variant of `when` that fallback to an `orElse` callback.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return orElse();
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String slug, String name, String? description, int? mode, String publisherId, String accountId, DateTime createdAt, DateTime updatedAt, List<SnPublicationPage> pages)? $default,{required TResult orElse(),}) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnPublicationSite() when $default != null:
|
||||||
|
return $default(_that.id,_that.slug,_that.name,_that.description,_that.mode,_that.publisherId,_that.accountId,_that.createdAt,_that.updatedAt,_that.pages);case _:
|
||||||
|
return orElse();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A `switch`-like method, using callbacks.
|
||||||
|
///
|
||||||
|
/// As opposed to `map`, this offers destructuring.
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case Subclass2(:final field2):
|
||||||
|
/// return ...;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String slug, String name, String? description, int? mode, String publisherId, String accountId, DateTime createdAt, DateTime updatedAt, List<SnPublicationPage> pages) $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnPublicationSite():
|
||||||
|
return $default(_that.id,_that.slug,_that.name,_that.description,_that.mode,_that.publisherId,_that.accountId,_that.createdAt,_that.updatedAt,_that.pages);}
|
||||||
|
}
|
||||||
|
/// A variant of `when` that fallback to returning `null`
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return null;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String slug, String name, String? description, int? mode, String publisherId, String accountId, DateTime createdAt, DateTime updatedAt, List<SnPublicationPage> pages)? $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnPublicationSite() when $default != null:
|
||||||
|
return $default(_that.id,_that.slug,_that.name,_that.description,_that.mode,_that.publisherId,_that.accountId,_that.createdAt,_that.updatedAt,_that.pages);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
|
||||||
|
class _SnPublicationSite implements SnPublicationSite {
|
||||||
|
const _SnPublicationSite({required this.id, required this.slug, required this.name, this.description, this.mode, required this.publisherId, required this.accountId, required this.createdAt, required this.updatedAt, required final List<SnPublicationPage> pages}): _pages = pages;
|
||||||
|
factory _SnPublicationSite.fromJson(Map<String, dynamic> json) => _$SnPublicationSiteFromJson(json);
|
||||||
|
|
||||||
|
@override final String id;
|
||||||
|
@override final String slug;
|
||||||
|
@override final String name;
|
||||||
|
@override final String? description;
|
||||||
|
@override final int? mode;
|
||||||
|
@override final String publisherId;
|
||||||
|
@override final String accountId;
|
||||||
|
@override final DateTime createdAt;
|
||||||
|
@override final DateTime updatedAt;
|
||||||
|
final List<SnPublicationPage> _pages;
|
||||||
|
@override List<SnPublicationPage> get pages {
|
||||||
|
if (_pages is EqualUnmodifiableListView) return _pages;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableListView(_pages);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Create a copy of SnPublicationSite
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$SnPublicationSiteCopyWith<_SnPublicationSite> get copyWith => __$SnPublicationSiteCopyWithImpl<_SnPublicationSite>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$SnPublicationSiteToJson(this, );
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPublicationSite&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.mode, mode) || other.mode == mode)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&const DeepCollectionEquality().equals(other._pages, _pages));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,id,slug,name,description,mode,publisherId,accountId,createdAt,updatedAt,const DeepCollectionEquality().hash(_pages));
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SnPublicationSite(id: $id, slug: $slug, name: $name, description: $description, mode: $mode, publisherId: $publisherId, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, pages: $pages)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class _$SnPublicationSiteCopyWith<$Res> implements $SnPublicationSiteCopyWith<$Res> {
|
||||||
|
factory _$SnPublicationSiteCopyWith(_SnPublicationSite value, $Res Function(_SnPublicationSite) _then) = __$SnPublicationSiteCopyWithImpl;
|
||||||
|
@override @useResult
|
||||||
|
$Res call({
|
||||||
|
String id, String slug, String name, String? description, int? mode, String publisherId, String accountId, DateTime createdAt, DateTime updatedAt, List<SnPublicationPage> pages
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class __$SnPublicationSiteCopyWithImpl<$Res>
|
||||||
|
implements _$SnPublicationSiteCopyWith<$Res> {
|
||||||
|
__$SnPublicationSiteCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final _SnPublicationSite _self;
|
||||||
|
final $Res Function(_SnPublicationSite) _then;
|
||||||
|
|
||||||
|
/// Create a copy of SnPublicationSite
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? slug = null,Object? name = null,Object? description = freezed,Object? mode = freezed,Object? publisherId = null,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? pages = null,}) {
|
||||||
|
return _then(_SnPublicationSite(
|
||||||
|
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,slug: null == slug ? _self.slug : slug // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,mode: freezed == mode ? _self.mode : mode // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int?,publisherId: null == publisherId ? _self.publisherId : publisherId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,pages: null == pages ? _self._pages : pages // ignore: cast_nullable_to_non_nullable
|
||||||
|
as List<SnPublicationPage>,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$SnPublicationPage {
|
||||||
|
|
||||||
|
String get id; String? get preset; String? get path; Map<String, dynamic>? get config; String get siteId; DateTime get createdAt; DateTime get updatedAt;
|
||||||
|
/// Create a copy of SnPublicationPage
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnPublicationPageCopyWith<SnPublicationPage> get copyWith => _$SnPublicationPageCopyWithImpl<SnPublicationPage>(this as SnPublicationPage, _$identity);
|
||||||
|
|
||||||
|
/// Serializes this SnPublicationPage to a JSON map.
|
||||||
|
Map<String, dynamic> toJson();
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPublicationPage&&(identical(other.id, id) || other.id == id)&&(identical(other.preset, preset) || other.preset == preset)&&(identical(other.path, path) || other.path == path)&&const DeepCollectionEquality().equals(other.config, config)&&(identical(other.siteId, siteId) || other.siteId == siteId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,id,preset,path,const DeepCollectionEquality().hash(config),siteId,createdAt,updatedAt);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SnPublicationPage(id: $id, preset: $preset, path: $path, config: $config, siteId: $siteId, createdAt: $createdAt, updatedAt: $updatedAt)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class $SnPublicationPageCopyWith<$Res> {
|
||||||
|
factory $SnPublicationPageCopyWith(SnPublicationPage value, $Res Function(SnPublicationPage) _then) = _$SnPublicationPageCopyWithImpl;
|
||||||
|
@useResult
|
||||||
|
$Res call({
|
||||||
|
String id, String? preset, String? path, Map<String, dynamic>? config, String siteId, DateTime createdAt, DateTime updatedAt
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class _$SnPublicationPageCopyWithImpl<$Res>
|
||||||
|
implements $SnPublicationPageCopyWith<$Res> {
|
||||||
|
_$SnPublicationPageCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final SnPublicationPage _self;
|
||||||
|
final $Res Function(SnPublicationPage) _then;
|
||||||
|
|
||||||
|
/// Create a copy of SnPublicationPage
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? preset = freezed,Object? path = freezed,Object? config = freezed,Object? siteId = null,Object? createdAt = null,Object? updatedAt = null,}) {
|
||||||
|
return _then(_self.copyWith(
|
||||||
|
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,preset: freezed == preset ? _self.preset : preset // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,path: freezed == path ? _self.path : path // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,config: freezed == config ? _self.config : config // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<String, dynamic>?,siteId: null == siteId ? _self.siteId : siteId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Adds pattern-matching-related methods to [SnPublicationPage].
|
||||||
|
extension SnPublicationPagePatterns on SnPublicationPage {
|
||||||
|
/// A variant of `map` that fallback to returning `orElse`.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return orElse();
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnPublicationPage value)? $default,{required TResult orElse(),}){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnPublicationPage() when $default != null:
|
||||||
|
return $default(_that);case _:
|
||||||
|
return orElse();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A `switch`-like method, using callbacks.
|
||||||
|
///
|
||||||
|
/// Callbacks receives the raw object, upcasted.
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case final Subclass2 value:
|
||||||
|
/// return ...;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnPublicationPage value) $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnPublicationPage():
|
||||||
|
return $default(_that);}
|
||||||
|
}
|
||||||
|
/// A variant of `map` that fallback to returning `null`.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return null;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnPublicationPage value)? $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnPublicationPage() when $default != null:
|
||||||
|
return $default(_that);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A variant of `when` that fallback to an `orElse` callback.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return orElse();
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String? preset, String? path, Map<String, dynamic>? config, String siteId, DateTime createdAt, DateTime updatedAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnPublicationPage() when $default != null:
|
||||||
|
return $default(_that.id,_that.preset,_that.path,_that.config,_that.siteId,_that.createdAt,_that.updatedAt);case _:
|
||||||
|
return orElse();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A `switch`-like method, using callbacks.
|
||||||
|
///
|
||||||
|
/// As opposed to `map`, this offers destructuring.
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case Subclass2(:final field2):
|
||||||
|
/// return ...;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String? preset, String? path, Map<String, dynamic>? config, String siteId, DateTime createdAt, DateTime updatedAt) $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnPublicationPage():
|
||||||
|
return $default(_that.id,_that.preset,_that.path,_that.config,_that.siteId,_that.createdAt,_that.updatedAt);}
|
||||||
|
}
|
||||||
|
/// A variant of `when` that fallback to returning `null`
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return null;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String? preset, String? path, Map<String, dynamic>? config, String siteId, DateTime createdAt, DateTime updatedAt)? $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnPublicationPage() when $default != null:
|
||||||
|
return $default(_that.id,_that.preset,_that.path,_that.config,_that.siteId,_that.createdAt,_that.updatedAt);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
|
||||||
|
class _SnPublicationPage implements SnPublicationPage {
|
||||||
|
const _SnPublicationPage({required this.id, this.preset, this.path, final Map<String, dynamic>? config, required this.siteId, required this.createdAt, required this.updatedAt}): _config = config;
|
||||||
|
factory _SnPublicationPage.fromJson(Map<String, dynamic> json) => _$SnPublicationPageFromJson(json);
|
||||||
|
|
||||||
|
@override final String id;
|
||||||
|
@override final String? preset;
|
||||||
|
@override final String? path;
|
||||||
|
final Map<String, dynamic>? _config;
|
||||||
|
@override Map<String, dynamic>? get config {
|
||||||
|
final value = _config;
|
||||||
|
if (value == null) return null;
|
||||||
|
if (_config is EqualUnmodifiableMapView) return _config;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableMapView(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override final String siteId;
|
||||||
|
@override final DateTime createdAt;
|
||||||
|
@override final DateTime updatedAt;
|
||||||
|
|
||||||
|
/// Create a copy of SnPublicationPage
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$SnPublicationPageCopyWith<_SnPublicationPage> get copyWith => __$SnPublicationPageCopyWithImpl<_SnPublicationPage>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$SnPublicationPageToJson(this, );
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPublicationPage&&(identical(other.id, id) || other.id == id)&&(identical(other.preset, preset) || other.preset == preset)&&(identical(other.path, path) || other.path == path)&&const DeepCollectionEquality().equals(other._config, _config)&&(identical(other.siteId, siteId) || other.siteId == siteId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,id,preset,path,const DeepCollectionEquality().hash(_config),siteId,createdAt,updatedAt);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SnPublicationPage(id: $id, preset: $preset, path: $path, config: $config, siteId: $siteId, createdAt: $createdAt, updatedAt: $updatedAt)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class _$SnPublicationPageCopyWith<$Res> implements $SnPublicationPageCopyWith<$Res> {
|
||||||
|
factory _$SnPublicationPageCopyWith(_SnPublicationPage value, $Res Function(_SnPublicationPage) _then) = __$SnPublicationPageCopyWithImpl;
|
||||||
|
@override @useResult
|
||||||
|
$Res call({
|
||||||
|
String id, String? preset, String? path, Map<String, dynamic>? config, String siteId, DateTime createdAt, DateTime updatedAt
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class __$SnPublicationPageCopyWithImpl<$Res>
|
||||||
|
implements _$SnPublicationPageCopyWith<$Res> {
|
||||||
|
__$SnPublicationPageCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final _SnPublicationPage _self;
|
||||||
|
final $Res Function(_SnPublicationPage) _then;
|
||||||
|
|
||||||
|
/// Create a copy of SnPublicationPage
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? preset = freezed,Object? path = freezed,Object? config = freezed,Object? siteId = null,Object? createdAt = null,Object? updatedAt = null,}) {
|
||||||
|
return _then(_SnPublicationPage(
|
||||||
|
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,preset: freezed == preset ? _self.preset : preset // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,path: freezed == path ? _self.path : path // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,config: freezed == config ? _self._config : config // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<String, dynamic>?,siteId: null == siteId ? _self.siteId : siteId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// dart format on
|
||||||
60
lib/models/publication_site.g.dart
Normal file
60
lib/models/publication_site.g.dart
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'publication_site.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
_SnPublicationSite _$SnPublicationSiteFromJson(Map<String, dynamic> json) =>
|
||||||
|
_SnPublicationSite(
|
||||||
|
id: json['id'] as String,
|
||||||
|
slug: json['slug'] as String,
|
||||||
|
name: json['name'] as String,
|
||||||
|
description: json['description'] as String?,
|
||||||
|
mode: (json['mode'] as num?)?.toInt(),
|
||||||
|
publisherId: json['publisher_id'] as String,
|
||||||
|
accountId: json['account_id'] as String,
|
||||||
|
createdAt: DateTime.parse(json['created_at'] as String),
|
||||||
|
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||||
|
pages:
|
||||||
|
(json['pages'] as List<dynamic>)
|
||||||
|
.map((e) => SnPublicationPage.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$SnPublicationSiteToJson(_SnPublicationSite instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'slug': instance.slug,
|
||||||
|
'name': instance.name,
|
||||||
|
'description': instance.description,
|
||||||
|
'mode': instance.mode,
|
||||||
|
'publisher_id': instance.publisherId,
|
||||||
|
'account_id': instance.accountId,
|
||||||
|
'created_at': instance.createdAt.toIso8601String(),
|
||||||
|
'updated_at': instance.updatedAt.toIso8601String(),
|
||||||
|
'pages': instance.pages.map((e) => e.toJson()).toList(),
|
||||||
|
};
|
||||||
|
|
||||||
|
_SnPublicationPage _$SnPublicationPageFromJson(Map<String, dynamic> json) =>
|
||||||
|
_SnPublicationPage(
|
||||||
|
id: json['id'] as String,
|
||||||
|
preset: json['preset'] as String?,
|
||||||
|
path: json['path'] as String?,
|
||||||
|
config: json['config'] as Map<String, dynamic>?,
|
||||||
|
siteId: json['site_id'] as String,
|
||||||
|
createdAt: DateTime.parse(json['created_at'] as String),
|
||||||
|
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$SnPublicationPageToJson(_SnPublicationPage instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'preset': instance.preset,
|
||||||
|
'path': instance.path,
|
||||||
|
'config': instance.config,
|
||||||
|
'site_id': instance.siteId,
|
||||||
|
'created_at': instance.createdAt.toIso8601String(),
|
||||||
|
'updated_at': instance.updatedAt.toIso8601String(),
|
||||||
|
};
|
||||||
23
lib/models/reference.dart
Normal file
23
lib/models/reference.dart
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
import 'package:island/models/file.dart';
|
||||||
|
|
||||||
|
part 'reference.freezed.dart';
|
||||||
|
part 'reference.g.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
sealed class Reference with _$Reference {
|
||||||
|
const factory Reference({
|
||||||
|
required String id,
|
||||||
|
@JsonKey(name: 'file_id') required String fileId,
|
||||||
|
SnCloudFile? file,
|
||||||
|
required String usage,
|
||||||
|
@JsonKey(name: 'resource_id') required String resourceId,
|
||||||
|
@JsonKey(name: 'expired_at') DateTime? expiredAt,
|
||||||
|
@JsonKey(name: 'created_at') required DateTime createdAt,
|
||||||
|
@JsonKey(name: 'updated_at') required DateTime updatedAt,
|
||||||
|
@JsonKey(name: 'deleted_at') DateTime? deletedAt,
|
||||||
|
}) = _Reference;
|
||||||
|
|
||||||
|
factory Reference.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$ReferenceFromJson(json);
|
||||||
|
}
|
||||||
319
lib/models/reference.freezed.dart
Normal file
319
lib/models/reference.freezed.dart
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// coverage:ignore-file
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||||
|
|
||||||
|
part of 'reference.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// FreezedGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// dart format off
|
||||||
|
T _$identity<T>(T value) => value;
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$Reference {
|
||||||
|
|
||||||
|
String get id;@JsonKey(name: 'file_id') String get fileId; SnCloudFile? get file; String get usage;@JsonKey(name: 'resource_id') String get resourceId;@JsonKey(name: 'expired_at') DateTime? get expiredAt;@JsonKey(name: 'created_at') DateTime get createdAt;@JsonKey(name: 'updated_at') DateTime get updatedAt;@JsonKey(name: 'deleted_at') DateTime? get deletedAt;
|
||||||
|
/// Create a copy of Reference
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$ReferenceCopyWith<Reference> get copyWith => _$ReferenceCopyWithImpl<Reference>(this as Reference, _$identity);
|
||||||
|
|
||||||
|
/// Serializes this Reference to a JSON map.
|
||||||
|
Map<String, dynamic> toJson();
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is Reference&&(identical(other.id, id) || other.id == id)&&(identical(other.fileId, fileId) || other.fileId == fileId)&&(identical(other.file, file) || other.file == file)&&(identical(other.usage, usage) || other.usage == usage)&&(identical(other.resourceId, resourceId) || other.resourceId == resourceId)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,id,fileId,file,usage,resourceId,expiredAt,createdAt,updatedAt,deletedAt);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'Reference(id: $id, fileId: $fileId, file: $file, usage: $usage, resourceId: $resourceId, expiredAt: $expiredAt, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class $ReferenceCopyWith<$Res> {
|
||||||
|
factory $ReferenceCopyWith(Reference value, $Res Function(Reference) _then) = _$ReferenceCopyWithImpl;
|
||||||
|
@useResult
|
||||||
|
$Res call({
|
||||||
|
String id,@JsonKey(name: 'file_id') String fileId, SnCloudFile? file, String usage,@JsonKey(name: 'resource_id') String resourceId,@JsonKey(name: 'expired_at') DateTime? expiredAt,@JsonKey(name: 'created_at') DateTime createdAt,@JsonKey(name: 'updated_at') DateTime updatedAt,@JsonKey(name: 'deleted_at') DateTime? deletedAt
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
$SnCloudFileCopyWith<$Res>? get file;
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class _$ReferenceCopyWithImpl<$Res>
|
||||||
|
implements $ReferenceCopyWith<$Res> {
|
||||||
|
_$ReferenceCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final Reference _self;
|
||||||
|
final $Res Function(Reference) _then;
|
||||||
|
|
||||||
|
/// Create a copy of Reference
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? fileId = null,Object? file = freezed,Object? usage = null,Object? resourceId = null,Object? expiredAt = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||||
|
return _then(_self.copyWith(
|
||||||
|
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,fileId: null == fileId ? _self.fileId : fileId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,file: freezed == file ? _self.file : file // ignore: cast_nullable_to_non_nullable
|
||||||
|
as SnCloudFile?,usage: null == usage ? _self.usage : usage // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,resourceId: null == resourceId ? _self.resourceId : resourceId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
/// Create a copy of Reference
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnCloudFileCopyWith<$Res>? get file {
|
||||||
|
if (_self.file == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $SnCloudFileCopyWith<$Res>(_self.file!, (value) {
|
||||||
|
return _then(_self.copyWith(file: value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Adds pattern-matching-related methods to [Reference].
|
||||||
|
extension ReferencePatterns on Reference {
|
||||||
|
/// A variant of `map` that fallback to returning `orElse`.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return orElse();
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _Reference value)? $default,{required TResult orElse(),}){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _Reference() when $default != null:
|
||||||
|
return $default(_that);case _:
|
||||||
|
return orElse();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A `switch`-like method, using callbacks.
|
||||||
|
///
|
||||||
|
/// Callbacks receives the raw object, upcasted.
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case final Subclass2 value:
|
||||||
|
/// return ...;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _Reference value) $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _Reference():
|
||||||
|
return $default(_that);}
|
||||||
|
}
|
||||||
|
/// A variant of `map` that fallback to returning `null`.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return null;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _Reference value)? $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _Reference() when $default != null:
|
||||||
|
return $default(_that);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A variant of `when` that fallback to an `orElse` callback.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return orElse();
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, @JsonKey(name: 'file_id') String fileId, SnCloudFile? file, String usage, @JsonKey(name: 'resource_id') String resourceId, @JsonKey(name: 'expired_at') DateTime? expiredAt, @JsonKey(name: 'created_at') DateTime createdAt, @JsonKey(name: 'updated_at') DateTime updatedAt, @JsonKey(name: 'deleted_at') DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _Reference() when $default != null:
|
||||||
|
return $default(_that.id,_that.fileId,_that.file,_that.usage,_that.resourceId,_that.expiredAt,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||||
|
return orElse();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A `switch`-like method, using callbacks.
|
||||||
|
///
|
||||||
|
/// As opposed to `map`, this offers destructuring.
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case Subclass2(:final field2):
|
||||||
|
/// return ...;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, @JsonKey(name: 'file_id') String fileId, SnCloudFile? file, String usage, @JsonKey(name: 'resource_id') String resourceId, @JsonKey(name: 'expired_at') DateTime? expiredAt, @JsonKey(name: 'created_at') DateTime createdAt, @JsonKey(name: 'updated_at') DateTime updatedAt, @JsonKey(name: 'deleted_at') DateTime? deletedAt) $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _Reference():
|
||||||
|
return $default(_that.id,_that.fileId,_that.file,_that.usage,_that.resourceId,_that.expiredAt,_that.createdAt,_that.updatedAt,_that.deletedAt);}
|
||||||
|
}
|
||||||
|
/// A variant of `when` that fallback to returning `null`
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return null;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, @JsonKey(name: 'file_id') String fileId, SnCloudFile? file, String usage, @JsonKey(name: 'resource_id') String resourceId, @JsonKey(name: 'expired_at') DateTime? expiredAt, @JsonKey(name: 'created_at') DateTime createdAt, @JsonKey(name: 'updated_at') DateTime updatedAt, @JsonKey(name: 'deleted_at') DateTime? deletedAt)? $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _Reference() when $default != null:
|
||||||
|
return $default(_that.id,_that.fileId,_that.file,_that.usage,_that.resourceId,_that.expiredAt,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
|
||||||
|
class _Reference implements Reference {
|
||||||
|
const _Reference({required this.id, @JsonKey(name: 'file_id') required this.fileId, this.file, required this.usage, @JsonKey(name: 'resource_id') required this.resourceId, @JsonKey(name: 'expired_at') this.expiredAt, @JsonKey(name: 'created_at') required this.createdAt, @JsonKey(name: 'updated_at') required this.updatedAt, @JsonKey(name: 'deleted_at') this.deletedAt});
|
||||||
|
factory _Reference.fromJson(Map<String, dynamic> json) => _$ReferenceFromJson(json);
|
||||||
|
|
||||||
|
@override final String id;
|
||||||
|
@override@JsonKey(name: 'file_id') final String fileId;
|
||||||
|
@override final SnCloudFile? file;
|
||||||
|
@override final String usage;
|
||||||
|
@override@JsonKey(name: 'resource_id') final String resourceId;
|
||||||
|
@override@JsonKey(name: 'expired_at') final DateTime? expiredAt;
|
||||||
|
@override@JsonKey(name: 'created_at') final DateTime createdAt;
|
||||||
|
@override@JsonKey(name: 'updated_at') final DateTime updatedAt;
|
||||||
|
@override@JsonKey(name: 'deleted_at') final DateTime? deletedAt;
|
||||||
|
|
||||||
|
/// Create a copy of Reference
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$ReferenceCopyWith<_Reference> get copyWith => __$ReferenceCopyWithImpl<_Reference>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$ReferenceToJson(this, );
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is _Reference&&(identical(other.id, id) || other.id == id)&&(identical(other.fileId, fileId) || other.fileId == fileId)&&(identical(other.file, file) || other.file == file)&&(identical(other.usage, usage) || other.usage == usage)&&(identical(other.resourceId, resourceId) || other.resourceId == resourceId)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,id,fileId,file,usage,resourceId,expiredAt,createdAt,updatedAt,deletedAt);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'Reference(id: $id, fileId: $fileId, file: $file, usage: $usage, resourceId: $resourceId, expiredAt: $expiredAt, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class _$ReferenceCopyWith<$Res> implements $ReferenceCopyWith<$Res> {
|
||||||
|
factory _$ReferenceCopyWith(_Reference value, $Res Function(_Reference) _then) = __$ReferenceCopyWithImpl;
|
||||||
|
@override @useResult
|
||||||
|
$Res call({
|
||||||
|
String id,@JsonKey(name: 'file_id') String fileId, SnCloudFile? file, String usage,@JsonKey(name: 'resource_id') String resourceId,@JsonKey(name: 'expired_at') DateTime? expiredAt,@JsonKey(name: 'created_at') DateTime createdAt,@JsonKey(name: 'updated_at') DateTime updatedAt,@JsonKey(name: 'deleted_at') DateTime? deletedAt
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
@override $SnCloudFileCopyWith<$Res>? get file;
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class __$ReferenceCopyWithImpl<$Res>
|
||||||
|
implements _$ReferenceCopyWith<$Res> {
|
||||||
|
__$ReferenceCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final _Reference _self;
|
||||||
|
final $Res Function(_Reference) _then;
|
||||||
|
|
||||||
|
/// Create a copy of Reference
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? fileId = null,Object? file = freezed,Object? usage = null,Object? resourceId = null,Object? expiredAt = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||||
|
return _then(_Reference(
|
||||||
|
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,fileId: null == fileId ? _self.fileId : fileId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,file: freezed == file ? _self.file : file // ignore: cast_nullable_to_non_nullable
|
||||||
|
as SnCloudFile?,usage: null == usage ? _self.usage : usage // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,resourceId: null == resourceId ? _self.resourceId : resourceId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a copy of Reference
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnCloudFileCopyWith<$Res>? get file {
|
||||||
|
if (_self.file == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $SnCloudFileCopyWith<$Res>(_self.file!, (value) {
|
||||||
|
return _then(_self.copyWith(file: value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// dart format on
|
||||||
41
lib/models/reference.g.dart
Normal file
41
lib/models/reference.g.dart
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'reference.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
_Reference _$ReferenceFromJson(Map<String, dynamic> json) => _Reference(
|
||||||
|
id: json['id'] as String,
|
||||||
|
fileId: json['file_id'] as String,
|
||||||
|
file:
|
||||||
|
json['file'] == null
|
||||||
|
? null
|
||||||
|
: SnCloudFile.fromJson(json['file'] as Map<String, dynamic>),
|
||||||
|
usage: json['usage'] as String,
|
||||||
|
resourceId: json['resource_id'] as String,
|
||||||
|
expiredAt:
|
||||||
|
json['expired_at'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['expired_at'] as String),
|
||||||
|
createdAt: DateTime.parse(json['created_at'] as String),
|
||||||
|
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||||
|
deletedAt:
|
||||||
|
json['deleted_at'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['deleted_at'] as String),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$ReferenceToJson(_Reference instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'file_id': instance.fileId,
|
||||||
|
'file': instance.file?.toJson(),
|
||||||
|
'usage': instance.usage,
|
||||||
|
'resource_id': instance.resourceId,
|
||||||
|
'expired_at': instance.expiredAt?.toIso8601String(),
|
||||||
|
'created_at': instance.createdAt.toIso8601String(),
|
||||||
|
'updated_at': instance.updatedAt.toIso8601String(),
|
||||||
|
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||||
|
};
|
||||||
25
lib/models/site_file.dart
Normal file
25
lib/models/site_file.dart
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'site_file.freezed.dart';
|
||||||
|
part 'site_file.g.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
sealed class SnSiteFileEntry with _$SnSiteFileEntry {
|
||||||
|
const factory SnSiteFileEntry({
|
||||||
|
required bool isDirectory,
|
||||||
|
required String relativePath,
|
||||||
|
required int size, // Size in bytes (0 for directories)
|
||||||
|
required DateTime modified, // ISO 8601 timestamp
|
||||||
|
}) = _SnSiteFileEntry;
|
||||||
|
|
||||||
|
factory SnSiteFileEntry.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SnSiteFileEntryFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
sealed class SnFileContent with _$SnFileContent {
|
||||||
|
const factory SnFileContent({required String content}) = _SnFileContent;
|
||||||
|
|
||||||
|
factory SnFileContent.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SnFileContentFromJson(json);
|
||||||
|
}
|
||||||
539
lib/models/site_file.freezed.dart
Normal file
539
lib/models/site_file.freezed.dart
Normal file
@@ -0,0 +1,539 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// coverage:ignore-file
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||||
|
|
||||||
|
part of 'site_file.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// FreezedGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// dart format off
|
||||||
|
T _$identity<T>(T value) => value;
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$SnSiteFileEntry {
|
||||||
|
|
||||||
|
bool get isDirectory; String get relativePath; int get size;// Size in bytes (0 for directories)
|
||||||
|
DateTime get modified;
|
||||||
|
/// Create a copy of SnSiteFileEntry
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnSiteFileEntryCopyWith<SnSiteFileEntry> get copyWith => _$SnSiteFileEntryCopyWithImpl<SnSiteFileEntry>(this as SnSiteFileEntry, _$identity);
|
||||||
|
|
||||||
|
/// Serializes this SnSiteFileEntry to a JSON map.
|
||||||
|
Map<String, dynamic> toJson();
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnSiteFileEntry&&(identical(other.isDirectory, isDirectory) || other.isDirectory == isDirectory)&&(identical(other.relativePath, relativePath) || other.relativePath == relativePath)&&(identical(other.size, size) || other.size == size)&&(identical(other.modified, modified) || other.modified == modified));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,isDirectory,relativePath,size,modified);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SnSiteFileEntry(isDirectory: $isDirectory, relativePath: $relativePath, size: $size, modified: $modified)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class $SnSiteFileEntryCopyWith<$Res> {
|
||||||
|
factory $SnSiteFileEntryCopyWith(SnSiteFileEntry value, $Res Function(SnSiteFileEntry) _then) = _$SnSiteFileEntryCopyWithImpl;
|
||||||
|
@useResult
|
||||||
|
$Res call({
|
||||||
|
bool isDirectory, String relativePath, int size, DateTime modified
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class _$SnSiteFileEntryCopyWithImpl<$Res>
|
||||||
|
implements $SnSiteFileEntryCopyWith<$Res> {
|
||||||
|
_$SnSiteFileEntryCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final SnSiteFileEntry _self;
|
||||||
|
final $Res Function(SnSiteFileEntry) _then;
|
||||||
|
|
||||||
|
/// Create a copy of SnSiteFileEntry
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline') @override $Res call({Object? isDirectory = null,Object? relativePath = null,Object? size = null,Object? modified = null,}) {
|
||||||
|
return _then(_self.copyWith(
|
||||||
|
isDirectory: null == isDirectory ? _self.isDirectory : isDirectory // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,relativePath: null == relativePath ? _self.relativePath : relativePath // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,size: null == size ? _self.size : size // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,modified: null == modified ? _self.modified : modified // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Adds pattern-matching-related methods to [SnSiteFileEntry].
|
||||||
|
extension SnSiteFileEntryPatterns on SnSiteFileEntry {
|
||||||
|
/// A variant of `map` that fallback to returning `orElse`.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return orElse();
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnSiteFileEntry value)? $default,{required TResult orElse(),}){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnSiteFileEntry() when $default != null:
|
||||||
|
return $default(_that);case _:
|
||||||
|
return orElse();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A `switch`-like method, using callbacks.
|
||||||
|
///
|
||||||
|
/// Callbacks receives the raw object, upcasted.
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case final Subclass2 value:
|
||||||
|
/// return ...;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnSiteFileEntry value) $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnSiteFileEntry():
|
||||||
|
return $default(_that);}
|
||||||
|
}
|
||||||
|
/// A variant of `map` that fallback to returning `null`.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return null;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnSiteFileEntry value)? $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnSiteFileEntry() when $default != null:
|
||||||
|
return $default(_that);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A variant of `when` that fallback to an `orElse` callback.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return orElse();
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool isDirectory, String relativePath, int size, DateTime modified)? $default,{required TResult orElse(),}) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnSiteFileEntry() when $default != null:
|
||||||
|
return $default(_that.isDirectory,_that.relativePath,_that.size,_that.modified);case _:
|
||||||
|
return orElse();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A `switch`-like method, using callbacks.
|
||||||
|
///
|
||||||
|
/// As opposed to `map`, this offers destructuring.
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case Subclass2(:final field2):
|
||||||
|
/// return ...;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool isDirectory, String relativePath, int size, DateTime modified) $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnSiteFileEntry():
|
||||||
|
return $default(_that.isDirectory,_that.relativePath,_that.size,_that.modified);}
|
||||||
|
}
|
||||||
|
/// A variant of `when` that fallback to returning `null`
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return null;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool isDirectory, String relativePath, int size, DateTime modified)? $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnSiteFileEntry() when $default != null:
|
||||||
|
return $default(_that.isDirectory,_that.relativePath,_that.size,_that.modified);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
|
||||||
|
class _SnSiteFileEntry implements SnSiteFileEntry {
|
||||||
|
const _SnSiteFileEntry({required this.isDirectory, required this.relativePath, required this.size, required this.modified});
|
||||||
|
factory _SnSiteFileEntry.fromJson(Map<String, dynamic> json) => _$SnSiteFileEntryFromJson(json);
|
||||||
|
|
||||||
|
@override final bool isDirectory;
|
||||||
|
@override final String relativePath;
|
||||||
|
@override final int size;
|
||||||
|
// Size in bytes (0 for directories)
|
||||||
|
@override final DateTime modified;
|
||||||
|
|
||||||
|
/// Create a copy of SnSiteFileEntry
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$SnSiteFileEntryCopyWith<_SnSiteFileEntry> get copyWith => __$SnSiteFileEntryCopyWithImpl<_SnSiteFileEntry>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$SnSiteFileEntryToJson(this, );
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnSiteFileEntry&&(identical(other.isDirectory, isDirectory) || other.isDirectory == isDirectory)&&(identical(other.relativePath, relativePath) || other.relativePath == relativePath)&&(identical(other.size, size) || other.size == size)&&(identical(other.modified, modified) || other.modified == modified));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,isDirectory,relativePath,size,modified);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SnSiteFileEntry(isDirectory: $isDirectory, relativePath: $relativePath, size: $size, modified: $modified)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class _$SnSiteFileEntryCopyWith<$Res> implements $SnSiteFileEntryCopyWith<$Res> {
|
||||||
|
factory _$SnSiteFileEntryCopyWith(_SnSiteFileEntry value, $Res Function(_SnSiteFileEntry) _then) = __$SnSiteFileEntryCopyWithImpl;
|
||||||
|
@override @useResult
|
||||||
|
$Res call({
|
||||||
|
bool isDirectory, String relativePath, int size, DateTime modified
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class __$SnSiteFileEntryCopyWithImpl<$Res>
|
||||||
|
implements _$SnSiteFileEntryCopyWith<$Res> {
|
||||||
|
__$SnSiteFileEntryCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final _SnSiteFileEntry _self;
|
||||||
|
final $Res Function(_SnSiteFileEntry) _then;
|
||||||
|
|
||||||
|
/// Create a copy of SnSiteFileEntry
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @pragma('vm:prefer-inline') $Res call({Object? isDirectory = null,Object? relativePath = null,Object? size = null,Object? modified = null,}) {
|
||||||
|
return _then(_SnSiteFileEntry(
|
||||||
|
isDirectory: null == isDirectory ? _self.isDirectory : isDirectory // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,relativePath: null == relativePath ? _self.relativePath : relativePath // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,size: null == size ? _self.size : size // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,modified: null == modified ? _self.modified : modified // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$SnFileContent {
|
||||||
|
|
||||||
|
String get content;
|
||||||
|
/// Create a copy of SnFileContent
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnFileContentCopyWith<SnFileContent> get copyWith => _$SnFileContentCopyWithImpl<SnFileContent>(this as SnFileContent, _$identity);
|
||||||
|
|
||||||
|
/// Serializes this SnFileContent to a JSON map.
|
||||||
|
Map<String, dynamic> toJson();
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnFileContent&&(identical(other.content, content) || other.content == content));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,content);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SnFileContent(content: $content)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class $SnFileContentCopyWith<$Res> {
|
||||||
|
factory $SnFileContentCopyWith(SnFileContent value, $Res Function(SnFileContent) _then) = _$SnFileContentCopyWithImpl;
|
||||||
|
@useResult
|
||||||
|
$Res call({
|
||||||
|
String content
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class _$SnFileContentCopyWithImpl<$Res>
|
||||||
|
implements $SnFileContentCopyWith<$Res> {
|
||||||
|
_$SnFileContentCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final SnFileContent _self;
|
||||||
|
final $Res Function(SnFileContent) _then;
|
||||||
|
|
||||||
|
/// Create a copy of SnFileContent
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline') @override $Res call({Object? content = null,}) {
|
||||||
|
return _then(_self.copyWith(
|
||||||
|
content: null == content ? _self.content : content // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Adds pattern-matching-related methods to [SnFileContent].
|
||||||
|
extension SnFileContentPatterns on SnFileContent {
|
||||||
|
/// A variant of `map` that fallback to returning `orElse`.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return orElse();
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnFileContent value)? $default,{required TResult orElse(),}){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnFileContent() when $default != null:
|
||||||
|
return $default(_that);case _:
|
||||||
|
return orElse();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A `switch`-like method, using callbacks.
|
||||||
|
///
|
||||||
|
/// Callbacks receives the raw object, upcasted.
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case final Subclass2 value:
|
||||||
|
/// return ...;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnFileContent value) $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnFileContent():
|
||||||
|
return $default(_that);}
|
||||||
|
}
|
||||||
|
/// A variant of `map` that fallback to returning `null`.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return null;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnFileContent value)? $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnFileContent() when $default != null:
|
||||||
|
return $default(_that);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A variant of `when` that fallback to an `orElse` callback.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return orElse();
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String content)? $default,{required TResult orElse(),}) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnFileContent() when $default != null:
|
||||||
|
return $default(_that.content);case _:
|
||||||
|
return orElse();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A `switch`-like method, using callbacks.
|
||||||
|
///
|
||||||
|
/// As opposed to `map`, this offers destructuring.
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case Subclass2(:final field2):
|
||||||
|
/// return ...;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String content) $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnFileContent():
|
||||||
|
return $default(_that.content);}
|
||||||
|
}
|
||||||
|
/// A variant of `when` that fallback to returning `null`
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return null;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String content)? $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnFileContent() when $default != null:
|
||||||
|
return $default(_that.content);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
|
||||||
|
class _SnFileContent implements SnFileContent {
|
||||||
|
const _SnFileContent({required this.content});
|
||||||
|
factory _SnFileContent.fromJson(Map<String, dynamic> json) => _$SnFileContentFromJson(json);
|
||||||
|
|
||||||
|
@override final String content;
|
||||||
|
|
||||||
|
/// Create a copy of SnFileContent
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$SnFileContentCopyWith<_SnFileContent> get copyWith => __$SnFileContentCopyWithImpl<_SnFileContent>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$SnFileContentToJson(this, );
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnFileContent&&(identical(other.content, content) || other.content == content));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,content);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SnFileContent(content: $content)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class _$SnFileContentCopyWith<$Res> implements $SnFileContentCopyWith<$Res> {
|
||||||
|
factory _$SnFileContentCopyWith(_SnFileContent value, $Res Function(_SnFileContent) _then) = __$SnFileContentCopyWithImpl;
|
||||||
|
@override @useResult
|
||||||
|
$Res call({
|
||||||
|
String content
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class __$SnFileContentCopyWithImpl<$Res>
|
||||||
|
implements _$SnFileContentCopyWith<$Res> {
|
||||||
|
__$SnFileContentCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final _SnFileContent _self;
|
||||||
|
final $Res Function(_SnFileContent) _then;
|
||||||
|
|
||||||
|
/// Create a copy of SnFileContent
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @pragma('vm:prefer-inline') $Res call({Object? content = null,}) {
|
||||||
|
return _then(_SnFileContent(
|
||||||
|
content: null == content ? _self.content : content // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// dart format on
|
||||||
29
lib/models/site_file.g.dart
Normal file
29
lib/models/site_file.g.dart
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'site_file.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
_SnSiteFileEntry _$SnSiteFileEntryFromJson(Map<String, dynamic> json) =>
|
||||||
|
_SnSiteFileEntry(
|
||||||
|
isDirectory: json['is_directory'] as bool,
|
||||||
|
relativePath: json['relative_path'] as String,
|
||||||
|
size: (json['size'] as num).toInt(),
|
||||||
|
modified: DateTime.parse(json['modified'] as String),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$SnSiteFileEntryToJson(_SnSiteFileEntry instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'is_directory': instance.isDirectory,
|
||||||
|
'relative_path': instance.relativePath,
|
||||||
|
'size': instance.size,
|
||||||
|
'modified': instance.modified.toIso8601String(),
|
||||||
|
};
|
||||||
|
|
||||||
|
_SnFileContent _$SnFileContentFromJson(Map<String, dynamic> json) =>
|
||||||
|
_SnFileContent(content: json['content'] as String);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$SnFileContentToJson(_SnFileContent instance) =>
|
||||||
|
<String, dynamic>{'content': instance.content};
|
||||||
@@ -38,6 +38,31 @@ class ThinkingChunkTypeConverter
|
|||||||
int toJson(ThinkingChunkType object) => object.value;
|
int toJson(ThinkingChunkType object) => object.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ThinkingMessagePartType {
|
||||||
|
text(0),
|
||||||
|
functionCall(1),
|
||||||
|
functionResult(2);
|
||||||
|
|
||||||
|
const ThinkingMessagePartType(this.value);
|
||||||
|
final int value;
|
||||||
|
|
||||||
|
static ThinkingMessagePartType fromValue(int value) {
|
||||||
|
return values.firstWhere((e) => e.value == value, orElse: () => text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ThinkingMessagePartTypeConverter
|
||||||
|
implements JsonConverter<ThinkingMessagePartType, int> {
|
||||||
|
const ThinkingMessagePartTypeConverter();
|
||||||
|
|
||||||
|
@override
|
||||||
|
ThinkingMessagePartType fromJson(int json) =>
|
||||||
|
ThinkingMessagePartType.fromValue(json);
|
||||||
|
|
||||||
|
@override
|
||||||
|
int toJson(ThinkingMessagePartType object) => object.value;
|
||||||
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
sealed class StreamThinkingRequest with _$StreamThinkingRequest {
|
sealed class StreamThinkingRequest with _$StreamThinkingRequest {
|
||||||
const factory StreamThinkingRequest({
|
const factory StreamThinkingRequest({
|
||||||
@@ -46,6 +71,7 @@ sealed class StreamThinkingRequest with _$StreamThinkingRequest {
|
|||||||
@Default([]) List<String> accpetProposals,
|
@Default([]) List<String> accpetProposals,
|
||||||
List<String>? attachedPosts,
|
List<String>? attachedPosts,
|
||||||
List<Map<String, dynamic>>? attachedMessages,
|
List<Map<String, dynamic>>? attachedMessages,
|
||||||
|
@JsonKey(name: 'service_id') String? serviceId,
|
||||||
}) = _StreamThinkingRequest;
|
}) = _StreamThinkingRequest;
|
||||||
|
|
||||||
factory StreamThinkingRequest.fromJson(Map<String, dynamic> json) =>
|
factory StreamThinkingRequest.fromJson(Map<String, dynamic> json) =>
|
||||||
@@ -77,6 +103,43 @@ sealed class SnThinkingChunk with _$SnThinkingChunk {
|
|||||||
_$SnThinkingChunkFromJson(json);
|
_$SnThinkingChunkFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
sealed class SnFunctionCall with _$SnFunctionCall {
|
||||||
|
const factory SnFunctionCall({
|
||||||
|
required String id,
|
||||||
|
required String name,
|
||||||
|
required String arguments,
|
||||||
|
}) = _SnFunctionCall;
|
||||||
|
|
||||||
|
factory SnFunctionCall.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SnFunctionCallFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
sealed class SnFunctionResult with _$SnFunctionResult {
|
||||||
|
const factory SnFunctionResult({
|
||||||
|
required String callId,
|
||||||
|
required dynamic result,
|
||||||
|
required bool isError,
|
||||||
|
}) = _SnFunctionResult;
|
||||||
|
|
||||||
|
factory SnFunctionResult.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SnFunctionResultFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
sealed class SnThinkingMessagePart with _$SnThinkingMessagePart {
|
||||||
|
const factory SnThinkingMessagePart({
|
||||||
|
@ThinkingMessagePartTypeConverter() required ThinkingMessagePartType type,
|
||||||
|
String? text,
|
||||||
|
SnFunctionCall? functionCall,
|
||||||
|
SnFunctionResult? functionResult,
|
||||||
|
}) = _SnThinkingMessagePart;
|
||||||
|
|
||||||
|
factory SnThinkingMessagePart.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SnThinkingMessagePartFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
sealed class SnThinkingSequence with _$SnThinkingSequence {
|
sealed class SnThinkingSequence with _$SnThinkingSequence {
|
||||||
const factory SnThinkingSequence({
|
const factory SnThinkingSequence({
|
||||||
@@ -98,9 +161,8 @@ sealed class SnThinkingSequence with _$SnThinkingSequence {
|
|||||||
sealed class SnThinkingThought with _$SnThinkingThought {
|
sealed class SnThinkingThought with _$SnThinkingThought {
|
||||||
const factory SnThinkingThought({
|
const factory SnThinkingThought({
|
||||||
required String id,
|
required String id,
|
||||||
String? content,
|
@Default([]) List<SnThinkingMessagePart> parts,
|
||||||
@Default([]) List<SnCloudFile> files,
|
@Default([]) List<SnCloudFile> files,
|
||||||
@Default([]) List<SnThinkingChunk> chunks,
|
|
||||||
@ThinkingThoughtRoleConverter() required ThinkingThoughtRole role,
|
@ThinkingThoughtRoleConverter() required ThinkingThoughtRole role,
|
||||||
int? tokenCount,
|
int? tokenCount,
|
||||||
String? modelName,
|
String? modelName,
|
||||||
@@ -114,3 +176,26 @@ sealed class SnThinkingThought with _$SnThinkingThought {
|
|||||||
factory SnThinkingThought.fromJson(Map<String, dynamic> json) =>
|
factory SnThinkingThought.fromJson(Map<String, dynamic> json) =>
|
||||||
_$SnThinkingThoughtFromJson(json);
|
_$SnThinkingThoughtFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
sealed class ThoughtService with _$ThoughtService {
|
||||||
|
const factory ThoughtService({
|
||||||
|
@JsonKey(name: 'service_id') required String serviceId,
|
||||||
|
required double billingMultiplier,
|
||||||
|
required int perkLevel,
|
||||||
|
}) = _ThoughtService;
|
||||||
|
|
||||||
|
factory ThoughtService.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$ThoughtServiceFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
sealed class ThoughtServicesResponse with _$ThoughtServicesResponse {
|
||||||
|
const factory ThoughtServicesResponse({
|
||||||
|
@JsonKey(name: 'default_service') required String defaultService,
|
||||||
|
required List<ThoughtService> services,
|
||||||
|
}) = _ThoughtServicesResponse;
|
||||||
|
|
||||||
|
factory ThoughtServicesResponse.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$ThoughtServicesResponseFromJson(json);
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -24,6 +24,7 @@ _StreamThinkingRequest _$StreamThinkingRequestFromJson(
|
|||||||
(json['attached_messages'] as List<dynamic>?)
|
(json['attached_messages'] as List<dynamic>?)
|
||||||
?.map((e) => e as Map<String, dynamic>)
|
?.map((e) => e as Map<String, dynamic>)
|
||||||
.toList(),
|
.toList(),
|
||||||
|
serviceId: json['service_id'] as String?,
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$StreamThinkingRequestToJson(
|
Map<String, dynamic> _$StreamThinkingRequestToJson(
|
||||||
@@ -34,6 +35,7 @@ Map<String, dynamic> _$StreamThinkingRequestToJson(
|
|||||||
'accpet_proposals': instance.accpetProposals,
|
'accpet_proposals': instance.accpetProposals,
|
||||||
'attached_posts': instance.attachedPosts,
|
'attached_posts': instance.attachedPosts,
|
||||||
'attached_messages': instance.attachedMessages,
|
'attached_messages': instance.attachedMessages,
|
||||||
|
'service_id': instance.serviceId,
|
||||||
};
|
};
|
||||||
|
|
||||||
_SnThinkingChunk _$SnThinkingChunkFromJson(Map<String, dynamic> json) =>
|
_SnThinkingChunk _$SnThinkingChunkFromJson(Map<String, dynamic> json) =>
|
||||||
@@ -50,6 +52,64 @@ Map<String, dynamic> _$SnThinkingChunkToJson(_SnThinkingChunk instance) =>
|
|||||||
'data': instance.data,
|
'data': instance.data,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_SnFunctionCall _$SnFunctionCallFromJson(Map<String, dynamic> json) =>
|
||||||
|
_SnFunctionCall(
|
||||||
|
id: json['id'] as String,
|
||||||
|
name: json['name'] as String,
|
||||||
|
arguments: json['arguments'] as String,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$SnFunctionCallToJson(_SnFunctionCall instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'name': instance.name,
|
||||||
|
'arguments': instance.arguments,
|
||||||
|
};
|
||||||
|
|
||||||
|
_SnFunctionResult _$SnFunctionResultFromJson(Map<String, dynamic> json) =>
|
||||||
|
_SnFunctionResult(
|
||||||
|
callId: json['call_id'] as String,
|
||||||
|
result: json['result'],
|
||||||
|
isError: json['is_error'] as bool,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$SnFunctionResultToJson(_SnFunctionResult instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'call_id': instance.callId,
|
||||||
|
'result': instance.result,
|
||||||
|
'is_error': instance.isError,
|
||||||
|
};
|
||||||
|
|
||||||
|
_SnThinkingMessagePart _$SnThinkingMessagePartFromJson(
|
||||||
|
Map<String, dynamic> json,
|
||||||
|
) => _SnThinkingMessagePart(
|
||||||
|
type: const ThinkingMessagePartTypeConverter().fromJson(
|
||||||
|
(json['type'] as num).toInt(),
|
||||||
|
),
|
||||||
|
text: json['text'] as String?,
|
||||||
|
functionCall:
|
||||||
|
json['function_call'] == null
|
||||||
|
? null
|
||||||
|
: SnFunctionCall.fromJson(
|
||||||
|
json['function_call'] as Map<String, dynamic>,
|
||||||
|
),
|
||||||
|
functionResult:
|
||||||
|
json['function_result'] == null
|
||||||
|
? null
|
||||||
|
: SnFunctionResult.fromJson(
|
||||||
|
json['function_result'] as Map<String, dynamic>,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$SnThinkingMessagePartToJson(
|
||||||
|
_SnThinkingMessagePart instance,
|
||||||
|
) => <String, dynamic>{
|
||||||
|
'type': const ThinkingMessagePartTypeConverter().toJson(instance.type),
|
||||||
|
'text': instance.text,
|
||||||
|
'function_call': instance.functionCall?.toJson(),
|
||||||
|
'function_result': instance.functionResult?.toJson(),
|
||||||
|
};
|
||||||
|
|
||||||
_SnThinkingSequence _$SnThinkingSequenceFromJson(Map<String, dynamic> json) =>
|
_SnThinkingSequence _$SnThinkingSequenceFromJson(Map<String, dynamic> json) =>
|
||||||
_SnThinkingSequence(
|
_SnThinkingSequence(
|
||||||
id: json['id'] as String,
|
id: json['id'] as String,
|
||||||
@@ -80,17 +140,19 @@ Map<String, dynamic> _$SnThinkingSequenceToJson(_SnThinkingSequence instance) =>
|
|||||||
_SnThinkingThought _$SnThinkingThoughtFromJson(Map<String, dynamic> json) =>
|
_SnThinkingThought _$SnThinkingThoughtFromJson(Map<String, dynamic> json) =>
|
||||||
_SnThinkingThought(
|
_SnThinkingThought(
|
||||||
id: json['id'] as String,
|
id: json['id'] as String,
|
||||||
content: json['content'] as String?,
|
parts:
|
||||||
|
(json['parts'] as List<dynamic>?)
|
||||||
|
?.map(
|
||||||
|
(e) =>
|
||||||
|
SnThinkingMessagePart.fromJson(e as Map<String, dynamic>),
|
||||||
|
)
|
||||||
|
.toList() ??
|
||||||
|
const [],
|
||||||
files:
|
files:
|
||||||
(json['files'] as List<dynamic>?)
|
(json['files'] as List<dynamic>?)
|
||||||
?.map((e) => SnCloudFile.fromJson(e as Map<String, dynamic>))
|
?.map((e) => SnCloudFile.fromJson(e as Map<String, dynamic>))
|
||||||
.toList() ??
|
.toList() ??
|
||||||
const [],
|
const [],
|
||||||
chunks:
|
|
||||||
(json['chunks'] as List<dynamic>?)
|
|
||||||
?.map((e) => SnThinkingChunk.fromJson(e as Map<String, dynamic>))
|
|
||||||
.toList() ??
|
|
||||||
const [],
|
|
||||||
role: const ThinkingThoughtRoleConverter().fromJson(
|
role: const ThinkingThoughtRoleConverter().fromJson(
|
||||||
(json['role'] as num).toInt(),
|
(json['role'] as num).toInt(),
|
||||||
),
|
),
|
||||||
@@ -114,9 +176,8 @@ _SnThinkingThought _$SnThinkingThoughtFromJson(Map<String, dynamic> json) =>
|
|||||||
Map<String, dynamic> _$SnThinkingThoughtToJson(_SnThinkingThought instance) =>
|
Map<String, dynamic> _$SnThinkingThoughtToJson(_SnThinkingThought instance) =>
|
||||||
<String, dynamic>{
|
<String, dynamic>{
|
||||||
'id': instance.id,
|
'id': instance.id,
|
||||||
'content': instance.content,
|
'parts': instance.parts.map((e) => e.toJson()).toList(),
|
||||||
'files': instance.files.map((e) => e.toJson()).toList(),
|
'files': instance.files.map((e) => e.toJson()).toList(),
|
||||||
'chunks': instance.chunks.map((e) => e.toJson()).toList(),
|
|
||||||
'role': const ThinkingThoughtRoleConverter().toJson(instance.role),
|
'role': const ThinkingThoughtRoleConverter().toJson(instance.role),
|
||||||
'token_count': instance.tokenCount,
|
'token_count': instance.tokenCount,
|
||||||
'model_name': instance.modelName,
|
'model_name': instance.modelName,
|
||||||
@@ -126,3 +187,34 @@ Map<String, dynamic> _$SnThinkingThoughtToJson(_SnThinkingThought instance) =>
|
|||||||
'updated_at': instance.updatedAt.toIso8601String(),
|
'updated_at': instance.updatedAt.toIso8601String(),
|
||||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_ThoughtService _$ThoughtServiceFromJson(Map<String, dynamic> json) =>
|
||||||
|
_ThoughtService(
|
||||||
|
serviceId: json['service_id'] as String,
|
||||||
|
billingMultiplier: (json['billing_multiplier'] as num).toDouble(),
|
||||||
|
perkLevel: (json['perk_level'] as num).toInt(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$ThoughtServiceToJson(_ThoughtService instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'service_id': instance.serviceId,
|
||||||
|
'billing_multiplier': instance.billingMultiplier,
|
||||||
|
'perk_level': instance.perkLevel,
|
||||||
|
};
|
||||||
|
|
||||||
|
_ThoughtServicesResponse _$ThoughtServicesResponseFromJson(
|
||||||
|
Map<String, dynamic> json,
|
||||||
|
) => _ThoughtServicesResponse(
|
||||||
|
defaultService: json['default_service'] as String,
|
||||||
|
services:
|
||||||
|
(json['services'] as List<dynamic>)
|
||||||
|
.map((e) => ThoughtService.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$ThoughtServicesResponseToJson(
|
||||||
|
_ThoughtServicesResponse instance,
|
||||||
|
) => <String, dynamic>{
|
||||||
|
'default_service': instance.defaultService,
|
||||||
|
'services': instance.services.map((e) => e.toJson()).toList(),
|
||||||
|
};
|
||||||
|
|||||||
@@ -176,6 +176,8 @@ sealed class SnWalletFund with _$SnWalletFund {
|
|||||||
required String id,
|
required String id,
|
||||||
required String currency,
|
required String currency,
|
||||||
required double totalAmount,
|
required double totalAmount,
|
||||||
|
required double remainingAmount,
|
||||||
|
required int amountOfSplits,
|
||||||
required int splitType, // 0: even, 1: random
|
required int splitType, // 0: even, 1: random
|
||||||
required int
|
required int
|
||||||
status, // 0: created, 1: partially claimed, 2: fully claimed, 3: expired
|
status, // 0: created, 1: partially claimed, 2: fully claimed, 3: expired
|
||||||
@@ -184,6 +186,7 @@ sealed class SnWalletFund with _$SnWalletFund {
|
|||||||
required SnAccount? creatorAccount,
|
required SnAccount? creatorAccount,
|
||||||
required DateTime expiredAt,
|
required DateTime expiredAt,
|
||||||
required List<SnWalletFundRecipient> recipients,
|
required List<SnWalletFundRecipient> recipients,
|
||||||
|
required bool isOpen,
|
||||||
required DateTime createdAt,
|
required DateTime createdAt,
|
||||||
required DateTime updatedAt,
|
required DateTime updatedAt,
|
||||||
required DateTime? deletedAt,
|
required DateTime? deletedAt,
|
||||||
|
|||||||
@@ -2553,9 +2553,9 @@ $SnWalletSubscriptionCopyWith<$Res>? get subscription {
|
|||||||
/// @nodoc
|
/// @nodoc
|
||||||
mixin _$SnWalletFund {
|
mixin _$SnWalletFund {
|
||||||
|
|
||||||
String get id; String get currency; double get totalAmount; int get splitType;// 0: even, 1: random
|
String get id; String get currency; double get totalAmount; double get remainingAmount; int get amountOfSplits; int get splitType;// 0: even, 1: random
|
||||||
int get status;// 0: created, 1: partially claimed, 2: fully claimed, 3: expired
|
int get status;// 0: created, 1: partially claimed, 2: fully claimed, 3: expired
|
||||||
String? get message; String get creatorAccountId; SnAccount? get creatorAccount; DateTime get expiredAt; List<SnWalletFundRecipient> get recipients; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
|
String? get message; String get creatorAccountId; SnAccount? get creatorAccount; DateTime get expiredAt; List<SnWalletFundRecipient> get recipients; bool get isOpen; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
|
||||||
/// Create a copy of SnWalletFund
|
/// Create a copy of SnWalletFund
|
||||||
/// 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)
|
||||||
@@ -2568,16 +2568,16 @@ $SnWalletFundCopyWith<SnWalletFund> get copyWith => _$SnWalletFundCopyWithImpl<S
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnWalletFund&&(identical(other.id, id) || other.id == id)&&(identical(other.currency, currency) || other.currency == currency)&&(identical(other.totalAmount, totalAmount) || other.totalAmount == totalAmount)&&(identical(other.splitType, splitType) || other.splitType == splitType)&&(identical(other.status, status) || other.status == status)&&(identical(other.message, message) || other.message == message)&&(identical(other.creatorAccountId, creatorAccountId) || other.creatorAccountId == creatorAccountId)&&(identical(other.creatorAccount, creatorAccount) || other.creatorAccount == creatorAccount)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&const DeepCollectionEquality().equals(other.recipients, recipients)&&(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 SnWalletFund&&(identical(other.id, id) || other.id == id)&&(identical(other.currency, currency) || other.currency == currency)&&(identical(other.totalAmount, totalAmount) || other.totalAmount == totalAmount)&&(identical(other.remainingAmount, remainingAmount) || other.remainingAmount == remainingAmount)&&(identical(other.amountOfSplits, amountOfSplits) || other.amountOfSplits == amountOfSplits)&&(identical(other.splitType, splitType) || other.splitType == splitType)&&(identical(other.status, status) || other.status == status)&&(identical(other.message, message) || other.message == message)&&(identical(other.creatorAccountId, creatorAccountId) || other.creatorAccountId == creatorAccountId)&&(identical(other.creatorAccount, creatorAccount) || other.creatorAccount == creatorAccount)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&const DeepCollectionEquality().equals(other.recipients, recipients)&&(identical(other.isOpen, isOpen) || other.isOpen == isOpen)&&(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.hash(runtimeType,id,currency,totalAmount,splitType,status,message,creatorAccountId,creatorAccount,expiredAt,const DeepCollectionEquality().hash(recipients),createdAt,updatedAt,deletedAt);
|
int get hashCode => Object.hash(runtimeType,id,currency,totalAmount,remainingAmount,amountOfSplits,splitType,status,message,creatorAccountId,creatorAccount,expiredAt,const DeepCollectionEquality().hash(recipients),isOpen,createdAt,updatedAt,deletedAt);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'SnWalletFund(id: $id, currency: $currency, totalAmount: $totalAmount, splitType: $splitType, status: $status, message: $message, creatorAccountId: $creatorAccountId, creatorAccount: $creatorAccount, expiredAt: $expiredAt, recipients: $recipients, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
return 'SnWalletFund(id: $id, currency: $currency, totalAmount: $totalAmount, remainingAmount: $remainingAmount, amountOfSplits: $amountOfSplits, splitType: $splitType, status: $status, message: $message, creatorAccountId: $creatorAccountId, creatorAccount: $creatorAccount, expiredAt: $expiredAt, recipients: $recipients, isOpen: $isOpen, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -2588,7 +2588,7 @@ abstract mixin class $SnWalletFundCopyWith<$Res> {
|
|||||||
factory $SnWalletFundCopyWith(SnWalletFund value, $Res Function(SnWalletFund) _then) = _$SnWalletFundCopyWithImpl;
|
factory $SnWalletFundCopyWith(SnWalletFund value, $Res Function(SnWalletFund) _then) = _$SnWalletFundCopyWithImpl;
|
||||||
@useResult
|
@useResult
|
||||||
$Res call({
|
$Res call({
|
||||||
String id, String currency, double totalAmount, int splitType, int status, String? message, String creatorAccountId, SnAccount? creatorAccount, DateTime expiredAt, List<SnWalletFundRecipient> recipients, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
String id, String currency, double totalAmount, double remainingAmount, int amountOfSplits, int splitType, int status, String? message, String creatorAccountId, SnAccount? creatorAccount, DateTime expiredAt, List<SnWalletFundRecipient> recipients, bool isOpen, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -2605,19 +2605,22 @@ class _$SnWalletFundCopyWithImpl<$Res>
|
|||||||
|
|
||||||
/// Create a copy of SnWalletFund
|
/// Create a copy of SnWalletFund
|
||||||
/// 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? currency = null,Object? totalAmount = null,Object? splitType = null,Object? status = null,Object? message = freezed,Object? creatorAccountId = null,Object? creatorAccount = freezed,Object? expiredAt = null,Object? recipients = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? currency = null,Object? totalAmount = null,Object? remainingAmount = null,Object? amountOfSplits = null,Object? splitType = null,Object? status = null,Object? message = freezed,Object? creatorAccountId = null,Object? creatorAccount = freezed,Object? expiredAt = null,Object? recipients = null,Object? isOpen = null,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,currency: null == currency ? _self.currency : currency // ignore: cast_nullable_to_non_nullable
|
as String,currency: null == currency ? _self.currency : currency // ignore: cast_nullable_to_non_nullable
|
||||||
as String,totalAmount: null == totalAmount ? _self.totalAmount : totalAmount // ignore: cast_nullable_to_non_nullable
|
as String,totalAmount: null == totalAmount ? _self.totalAmount : totalAmount // ignore: cast_nullable_to_non_nullable
|
||||||
as double,splitType: null == splitType ? _self.splitType : splitType // ignore: cast_nullable_to_non_nullable
|
as double,remainingAmount: null == remainingAmount ? _self.remainingAmount : remainingAmount // ignore: cast_nullable_to_non_nullable
|
||||||
|
as double,amountOfSplits: null == amountOfSplits ? _self.amountOfSplits : amountOfSplits // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,splitType: null == splitType ? _self.splitType : splitType // ignore: cast_nullable_to_non_nullable
|
||||||
as int,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
|
as int,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
|
||||||
as int,message: freezed == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
|
as int,message: freezed == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
|
||||||
as String?,creatorAccountId: null == creatorAccountId ? _self.creatorAccountId : creatorAccountId // ignore: cast_nullable_to_non_nullable
|
as String?,creatorAccountId: null == creatorAccountId ? _self.creatorAccountId : creatorAccountId // ignore: cast_nullable_to_non_nullable
|
||||||
as String,creatorAccount: freezed == creatorAccount ? _self.creatorAccount : creatorAccount // ignore: cast_nullable_to_non_nullable
|
as String,creatorAccount: freezed == creatorAccount ? _self.creatorAccount : creatorAccount // ignore: cast_nullable_to_non_nullable
|
||||||
as SnAccount?,expiredAt: null == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
|
as SnAccount?,expiredAt: null == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
|
||||||
as DateTime,recipients: null == recipients ? _self.recipients : recipients // ignore: cast_nullable_to_non_nullable
|
as DateTime,recipients: null == recipients ? _self.recipients : recipients // ignore: cast_nullable_to_non_nullable
|
||||||
as List<SnWalletFundRecipient>,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
as List<SnWalletFundRecipient>,isOpen: null == isOpen ? _self.isOpen : isOpen // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,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?,
|
||||||
@@ -2714,10 +2717,10 @@ return $default(_that);case _:
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String currency, double totalAmount, int splitType, int status, String? message, String creatorAccountId, SnAccount? creatorAccount, DateTime expiredAt, List<SnWalletFundRecipient> recipients, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
|
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String currency, double totalAmount, double remainingAmount, int amountOfSplits, int splitType, int status, String? message, String creatorAccountId, SnAccount? creatorAccount, DateTime expiredAt, List<SnWalletFundRecipient> recipients, bool isOpen, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _SnWalletFund() when $default != null:
|
case _SnWalletFund() when $default != null:
|
||||||
return $default(_that.id,_that.currency,_that.totalAmount,_that.splitType,_that.status,_that.message,_that.creatorAccountId,_that.creatorAccount,_that.expiredAt,_that.recipients,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
return $default(_that.id,_that.currency,_that.totalAmount,_that.remainingAmount,_that.amountOfSplits,_that.splitType,_that.status,_that.message,_that.creatorAccountId,_that.creatorAccount,_that.expiredAt,_that.recipients,_that.isOpen,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||||
return orElse();
|
return orElse();
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -2735,10 +2738,10 @@ return $default(_that.id,_that.currency,_that.totalAmount,_that.splitType,_that.
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String currency, double totalAmount, int splitType, int status, String? message, String creatorAccountId, SnAccount? creatorAccount, DateTime expiredAt, List<SnWalletFundRecipient> recipients, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
|
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String currency, double totalAmount, double remainingAmount, int amountOfSplits, int splitType, int status, String? message, String creatorAccountId, SnAccount? creatorAccount, DateTime expiredAt, List<SnWalletFundRecipient> recipients, bool isOpen, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _SnWalletFund():
|
case _SnWalletFund():
|
||||||
return $default(_that.id,_that.currency,_that.totalAmount,_that.splitType,_that.status,_that.message,_that.creatorAccountId,_that.creatorAccount,_that.expiredAt,_that.recipients,_that.createdAt,_that.updatedAt,_that.deletedAt);}
|
return $default(_that.id,_that.currency,_that.totalAmount,_that.remainingAmount,_that.amountOfSplits,_that.splitType,_that.status,_that.message,_that.creatorAccountId,_that.creatorAccount,_that.expiredAt,_that.recipients,_that.isOpen,_that.createdAt,_that.updatedAt,_that.deletedAt);}
|
||||||
}
|
}
|
||||||
/// A variant of `when` that fallback to returning `null`
|
/// A variant of `when` that fallback to returning `null`
|
||||||
///
|
///
|
||||||
@@ -2752,10 +2755,10 @@ return $default(_that.id,_that.currency,_that.totalAmount,_that.splitType,_that.
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String currency, double totalAmount, int splitType, int status, String? message, String creatorAccountId, SnAccount? creatorAccount, DateTime expiredAt, List<SnWalletFundRecipient> recipients, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
|
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String currency, double totalAmount, double remainingAmount, int amountOfSplits, int splitType, int status, String? message, String creatorAccountId, SnAccount? creatorAccount, DateTime expiredAt, List<SnWalletFundRecipient> recipients, bool isOpen, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _SnWalletFund() when $default != null:
|
case _SnWalletFund() when $default != null:
|
||||||
return $default(_that.id,_that.currency,_that.totalAmount,_that.splitType,_that.status,_that.message,_that.creatorAccountId,_that.creatorAccount,_that.expiredAt,_that.recipients,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
return $default(_that.id,_that.currency,_that.totalAmount,_that.remainingAmount,_that.amountOfSplits,_that.splitType,_that.status,_that.message,_that.creatorAccountId,_that.creatorAccount,_that.expiredAt,_that.recipients,_that.isOpen,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -2767,12 +2770,14 @@ return $default(_that.id,_that.currency,_that.totalAmount,_that.splitType,_that.
|
|||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
|
|
||||||
class _SnWalletFund implements SnWalletFund {
|
class _SnWalletFund implements SnWalletFund {
|
||||||
const _SnWalletFund({required this.id, required this.currency, required this.totalAmount, required this.splitType, required this.status, required this.message, required this.creatorAccountId, required this.creatorAccount, required this.expiredAt, required final List<SnWalletFundRecipient> recipients, required this.createdAt, required this.updatedAt, required this.deletedAt}): _recipients = recipients;
|
const _SnWalletFund({required this.id, required this.currency, required this.totalAmount, required this.remainingAmount, required this.amountOfSplits, required this.splitType, required this.status, required this.message, required this.creatorAccountId, required this.creatorAccount, required this.expiredAt, required final List<SnWalletFundRecipient> recipients, required this.isOpen, required this.createdAt, required this.updatedAt, required this.deletedAt}): _recipients = recipients;
|
||||||
factory _SnWalletFund.fromJson(Map<String, dynamic> json) => _$SnWalletFundFromJson(json);
|
factory _SnWalletFund.fromJson(Map<String, dynamic> json) => _$SnWalletFundFromJson(json);
|
||||||
|
|
||||||
@override final String id;
|
@override final String id;
|
||||||
@override final String currency;
|
@override final String currency;
|
||||||
@override final double totalAmount;
|
@override final double totalAmount;
|
||||||
|
@override final double remainingAmount;
|
||||||
|
@override final int amountOfSplits;
|
||||||
@override final int splitType;
|
@override final int splitType;
|
||||||
// 0: even, 1: random
|
// 0: even, 1: random
|
||||||
@override final int status;
|
@override final int status;
|
||||||
@@ -2788,6 +2793,7 @@ class _SnWalletFund implements SnWalletFund {
|
|||||||
return EqualUnmodifiableListView(_recipients);
|
return EqualUnmodifiableListView(_recipients);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override final bool isOpen;
|
||||||
@override final DateTime createdAt;
|
@override final DateTime createdAt;
|
||||||
@override final DateTime updatedAt;
|
@override final DateTime updatedAt;
|
||||||
@override final DateTime? deletedAt;
|
@override final DateTime? deletedAt;
|
||||||
@@ -2805,16 +2811,16 @@ Map<String, dynamic> toJson() {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnWalletFund&&(identical(other.id, id) || other.id == id)&&(identical(other.currency, currency) || other.currency == currency)&&(identical(other.totalAmount, totalAmount) || other.totalAmount == totalAmount)&&(identical(other.splitType, splitType) || other.splitType == splitType)&&(identical(other.status, status) || other.status == status)&&(identical(other.message, message) || other.message == message)&&(identical(other.creatorAccountId, creatorAccountId) || other.creatorAccountId == creatorAccountId)&&(identical(other.creatorAccount, creatorAccount) || other.creatorAccount == creatorAccount)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&const DeepCollectionEquality().equals(other._recipients, _recipients)&&(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 _SnWalletFund&&(identical(other.id, id) || other.id == id)&&(identical(other.currency, currency) || other.currency == currency)&&(identical(other.totalAmount, totalAmount) || other.totalAmount == totalAmount)&&(identical(other.remainingAmount, remainingAmount) || other.remainingAmount == remainingAmount)&&(identical(other.amountOfSplits, amountOfSplits) || other.amountOfSplits == amountOfSplits)&&(identical(other.splitType, splitType) || other.splitType == splitType)&&(identical(other.status, status) || other.status == status)&&(identical(other.message, message) || other.message == message)&&(identical(other.creatorAccountId, creatorAccountId) || other.creatorAccountId == creatorAccountId)&&(identical(other.creatorAccount, creatorAccount) || other.creatorAccount == creatorAccount)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&const DeepCollectionEquality().equals(other._recipients, _recipients)&&(identical(other.isOpen, isOpen) || other.isOpen == isOpen)&&(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.hash(runtimeType,id,currency,totalAmount,splitType,status,message,creatorAccountId,creatorAccount,expiredAt,const DeepCollectionEquality().hash(_recipients),createdAt,updatedAt,deletedAt);
|
int get hashCode => Object.hash(runtimeType,id,currency,totalAmount,remainingAmount,amountOfSplits,splitType,status,message,creatorAccountId,creatorAccount,expiredAt,const DeepCollectionEquality().hash(_recipients),isOpen,createdAt,updatedAt,deletedAt);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'SnWalletFund(id: $id, currency: $currency, totalAmount: $totalAmount, splitType: $splitType, status: $status, message: $message, creatorAccountId: $creatorAccountId, creatorAccount: $creatorAccount, expiredAt: $expiredAt, recipients: $recipients, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
return 'SnWalletFund(id: $id, currency: $currency, totalAmount: $totalAmount, remainingAmount: $remainingAmount, amountOfSplits: $amountOfSplits, splitType: $splitType, status: $status, message: $message, creatorAccountId: $creatorAccountId, creatorAccount: $creatorAccount, expiredAt: $expiredAt, recipients: $recipients, isOpen: $isOpen, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -2825,7 +2831,7 @@ abstract mixin class _$SnWalletFundCopyWith<$Res> implements $SnWalletFundCopyWi
|
|||||||
factory _$SnWalletFundCopyWith(_SnWalletFund value, $Res Function(_SnWalletFund) _then) = __$SnWalletFundCopyWithImpl;
|
factory _$SnWalletFundCopyWith(_SnWalletFund value, $Res Function(_SnWalletFund) _then) = __$SnWalletFundCopyWithImpl;
|
||||||
@override @useResult
|
@override @useResult
|
||||||
$Res call({
|
$Res call({
|
||||||
String id, String currency, double totalAmount, int splitType, int status, String? message, String creatorAccountId, SnAccount? creatorAccount, DateTime expiredAt, List<SnWalletFundRecipient> recipients, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
String id, String currency, double totalAmount, double remainingAmount, int amountOfSplits, int splitType, int status, String? message, String creatorAccountId, SnAccount? creatorAccount, DateTime expiredAt, List<SnWalletFundRecipient> recipients, bool isOpen, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -2842,19 +2848,22 @@ class __$SnWalletFundCopyWithImpl<$Res>
|
|||||||
|
|
||||||
/// Create a copy of SnWalletFund
|
/// Create a copy of SnWalletFund
|
||||||
/// 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? currency = null,Object? totalAmount = null,Object? splitType = null,Object? status = null,Object? message = freezed,Object? creatorAccountId = null,Object? creatorAccount = freezed,Object? expiredAt = null,Object? recipients = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? currency = null,Object? totalAmount = null,Object? remainingAmount = null,Object? amountOfSplits = null,Object? splitType = null,Object? status = null,Object? message = freezed,Object? creatorAccountId = null,Object? creatorAccount = freezed,Object? expiredAt = null,Object? recipients = null,Object? isOpen = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||||
return _then(_SnWalletFund(
|
return _then(_SnWalletFund(
|
||||||
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,currency: null == currency ? _self.currency : currency // ignore: cast_nullable_to_non_nullable
|
as String,currency: null == currency ? _self.currency : currency // ignore: cast_nullable_to_non_nullable
|
||||||
as String,totalAmount: null == totalAmount ? _self.totalAmount : totalAmount // ignore: cast_nullable_to_non_nullable
|
as String,totalAmount: null == totalAmount ? _self.totalAmount : totalAmount // ignore: cast_nullable_to_non_nullable
|
||||||
as double,splitType: null == splitType ? _self.splitType : splitType // ignore: cast_nullable_to_non_nullable
|
as double,remainingAmount: null == remainingAmount ? _self.remainingAmount : remainingAmount // ignore: cast_nullable_to_non_nullable
|
||||||
|
as double,amountOfSplits: null == amountOfSplits ? _self.amountOfSplits : amountOfSplits // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,splitType: null == splitType ? _self.splitType : splitType // ignore: cast_nullable_to_non_nullable
|
||||||
as int,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
|
as int,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
|
||||||
as int,message: freezed == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
|
as int,message: freezed == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
|
||||||
as String?,creatorAccountId: null == creatorAccountId ? _self.creatorAccountId : creatorAccountId // ignore: cast_nullable_to_non_nullable
|
as String?,creatorAccountId: null == creatorAccountId ? _self.creatorAccountId : creatorAccountId // ignore: cast_nullable_to_non_nullable
|
||||||
as String,creatorAccount: freezed == creatorAccount ? _self.creatorAccount : creatorAccount // ignore: cast_nullable_to_non_nullable
|
as String,creatorAccount: freezed == creatorAccount ? _self.creatorAccount : creatorAccount // ignore: cast_nullable_to_non_nullable
|
||||||
as SnAccount?,expiredAt: null == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
|
as SnAccount?,expiredAt: null == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
|
||||||
as DateTime,recipients: null == recipients ? _self._recipients : recipients // ignore: cast_nullable_to_non_nullable
|
as DateTime,recipients: null == recipients ? _self._recipients : recipients // ignore: cast_nullable_to_non_nullable
|
||||||
as List<SnWalletFundRecipient>,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
as List<SnWalletFundRecipient>,isOpen: null == isOpen ? _self.isOpen : isOpen // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,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?,
|
||||||
|
|||||||
@@ -336,6 +336,8 @@ _SnWalletFund _$SnWalletFundFromJson(
|
|||||||
id: json['id'] as String,
|
id: json['id'] as String,
|
||||||
currency: json['currency'] as String,
|
currency: json['currency'] as String,
|
||||||
totalAmount: (json['total_amount'] as num).toDouble(),
|
totalAmount: (json['total_amount'] as num).toDouble(),
|
||||||
|
remainingAmount: (json['remaining_amount'] as num).toDouble(),
|
||||||
|
amountOfSplits: (json['amount_of_splits'] as num).toInt(),
|
||||||
splitType: (json['split_type'] as num).toInt(),
|
splitType: (json['split_type'] as num).toInt(),
|
||||||
status: (json['status'] as num).toInt(),
|
status: (json['status'] as num).toInt(),
|
||||||
message: json['message'] as String?,
|
message: json['message'] as String?,
|
||||||
@@ -349,6 +351,7 @@ _SnWalletFund _$SnWalletFundFromJson(
|
|||||||
(json['recipients'] as List<dynamic>)
|
(json['recipients'] as List<dynamic>)
|
||||||
.map((e) => SnWalletFundRecipient.fromJson(e as Map<String, dynamic>))
|
.map((e) => SnWalletFundRecipient.fromJson(e as Map<String, dynamic>))
|
||||||
.toList(),
|
.toList(),
|
||||||
|
isOpen: json['is_open'] as bool,
|
||||||
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:
|
||||||
@@ -362,6 +365,8 @@ Map<String, dynamic> _$SnWalletFundToJson(_SnWalletFund instance) =>
|
|||||||
'id': instance.id,
|
'id': instance.id,
|
||||||
'currency': instance.currency,
|
'currency': instance.currency,
|
||||||
'total_amount': instance.totalAmount,
|
'total_amount': instance.totalAmount,
|
||||||
|
'remaining_amount': instance.remainingAmount,
|
||||||
|
'amount_of_splits': instance.amountOfSplits,
|
||||||
'split_type': instance.splitType,
|
'split_type': instance.splitType,
|
||||||
'status': instance.status,
|
'status': instance.status,
|
||||||
'message': instance.message,
|
'message': instance.message,
|
||||||
@@ -369,6 +374,7 @@ Map<String, dynamic> _$SnWalletFundToJson(_SnWalletFund instance) =>
|
|||||||
'creator_account': instance.creatorAccount?.toJson(),
|
'creator_account': instance.creatorAccount?.toJson(),
|
||||||
'expired_at': instance.expiredAt.toIso8601String(),
|
'expired_at': instance.expiredAt.toIso8601String(),
|
||||||
'recipients': instance.recipients.map((e) => e.toJson()).toList(),
|
'recipients': instance.recipients.map((e) => e.toJson()).toList(),
|
||||||
|
'is_open': instance.isOpen,
|
||||||
'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(),
|
||||||
|
|||||||
@@ -212,8 +212,14 @@ class CallNotifier extends _$CallNotifier {
|
|||||||
String? _roomId;
|
String? _roomId;
|
||||||
String? get roomId => _roomId;
|
String? get roomId => _roomId;
|
||||||
|
|
||||||
Future<void> joinRoom(String roomId) async {
|
SnChatRoom? _chatRoom;
|
||||||
if (_roomId == roomId && _room != null) {
|
SnChatRoom? get chatRoom => _chatRoom;
|
||||||
|
|
||||||
|
Future<void> joinRoom(SnChatRoom room) async {
|
||||||
|
var roomId = room.id;
|
||||||
|
if (_roomId == roomId &&
|
||||||
|
_room != null &&
|
||||||
|
_room?.connectionState == lk.ConnectionState.connected) {
|
||||||
talker.info('[Call] Call skipped. Already has data');
|
talker.info('[Call] Call skipped. Already has data');
|
||||||
return;
|
return;
|
||||||
} else if (_room != null) {
|
} else if (_room != null) {
|
||||||
@@ -223,6 +229,7 @@ class CallNotifier extends _$CallNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
_roomId = roomId;
|
_roomId = roomId;
|
||||||
|
_chatRoom = room;
|
||||||
if (_room != null) {
|
if (_room != null) {
|
||||||
await _room!.disconnect();
|
await _room!.disconnect();
|
||||||
await _room!.dispose();
|
await _room!.dispose();
|
||||||
@@ -355,6 +362,7 @@ class CallNotifier extends _$CallNotifier {
|
|||||||
sourceId: source.id,
|
sourceId: source.id,
|
||||||
maxFrameRate: 30.0,
|
maxFrameRate: 30.0,
|
||||||
captureScreenAudio: true,
|
captureScreenAudio: true,
|
||||||
|
useiOSBroadcastExtension: true,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
await _localParticipant!.publishVideoTrack(track);
|
await _localParticipant!.publishVideoTrack(track);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ part of 'call.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$callNotifierHash() => r'a8ca3f625c0db3ad9992033ae70864ce15efc281';
|
String _$callNotifierHash() => r'ef4e3e9c9d411cf9dce1ceb456a3b866b2c87db3';
|
||||||
|
|
||||||
/// See also [CallNotifier].
|
/// See also [CallNotifier].
|
||||||
@ProviderFor(CallNotifier)
|
@ProviderFor(CallNotifier)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:math' as math;
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:island/models/chat.dart';
|
import 'package:island/models/chat.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
@@ -6,6 +8,58 @@ import 'package:island/pods/chat/chat_subscribe.dart';
|
|||||||
|
|
||||||
part 'chat_summary.g.dart';
|
part 'chat_summary.g.dart';
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
class ChatUnreadCountNotifier extends _$ChatUnreadCountNotifier {
|
||||||
|
StreamSubscription<WebSocketPacket>? _subscription;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<int> build() async {
|
||||||
|
// Subscribe to websocket events when this provider is built
|
||||||
|
_subscribeToWebSocket();
|
||||||
|
|
||||||
|
// Dispose the subscription when this provider is disposed
|
||||||
|
ref.onDispose(() {
|
||||||
|
_subscription?.cancel();
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final client = ref.read(apiClientProvider);
|
||||||
|
final response = await client.get('/sphere/chat/unread');
|
||||||
|
return (response.data as num).toInt();
|
||||||
|
} catch (_) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _subscribeToWebSocket() {
|
||||||
|
final webSocketService = ref.read(websocketProvider);
|
||||||
|
_subscription = webSocketService.dataStream.listen((packet) {
|
||||||
|
if (packet.type == 'messages.new' && packet.data != null) {
|
||||||
|
final message = SnChatMessage.fromJson(packet.data!);
|
||||||
|
final currentSubscribed = ref.read(currentSubscribedChatIdProvider);
|
||||||
|
// Only increment if the message is not from the currently subscribed chat
|
||||||
|
if (message.chatRoomId != currentSubscribed) {
|
||||||
|
_incrementCounter();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _incrementCounter() async {
|
||||||
|
final current = await future;
|
||||||
|
state = AsyncData(current + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> decrement(int count) async {
|
||||||
|
final current = await future;
|
||||||
|
state = AsyncData(math.max(current - count, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear() async {
|
||||||
|
state = AsyncData(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
class ChatSummary extends _$ChatSummary {
|
class ChatSummary extends _$ChatSummary {
|
||||||
@override
|
@override
|
||||||
@@ -41,6 +95,14 @@ class ChatSummary extends _$ChatSummary {
|
|||||||
state.whenData((summaries) {
|
state.whenData((summaries) {
|
||||||
final summary = summaries[chatId];
|
final summary = summaries[chatId];
|
||||||
if (summary != null) {
|
if (summary != null) {
|
||||||
|
// Decrement global unread count
|
||||||
|
final unreadToDecrement = summary.unreadCount;
|
||||||
|
if (unreadToDecrement > 0) {
|
||||||
|
ref
|
||||||
|
.read(chatUnreadCountNotifierProvider.notifier)
|
||||||
|
.decrement(unreadToDecrement);
|
||||||
|
}
|
||||||
|
|
||||||
state = AsyncData({
|
state = AsyncData({
|
||||||
...summaries,
|
...summaries,
|
||||||
chatId: SnChatSummary(
|
chatId: SnChatSummary(
|
||||||
|
|||||||
@@ -6,6 +6,24 @@ part of 'chat_summary.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
|
String _$chatUnreadCountNotifierHash() =>
|
||||||
|
r'b8d93589dc37f772d4c3a07d9afd81c37026e57d';
|
||||||
|
|
||||||
|
/// See also [ChatUnreadCountNotifier].
|
||||||
|
@ProviderFor(ChatUnreadCountNotifier)
|
||||||
|
final chatUnreadCountNotifierProvider =
|
||||||
|
AutoDisposeAsyncNotifierProvider<ChatUnreadCountNotifier, int>.internal(
|
||||||
|
ChatUnreadCountNotifier.new,
|
||||||
|
name: r'chatUnreadCountNotifierProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$chatUnreadCountNotifierHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef _$ChatUnreadCountNotifier = AutoDisposeAsyncNotifier<int>;
|
||||||
String _$chatSummaryHash() => r'33815a3bd81d20902b7063e8194fe336930df9b4';
|
String _$chatSummaryHash() => r'33815a3bd81d20902b7063e8194fe336930df9b4';
|
||||||
|
|
||||||
/// See also [ChatSummary].
|
/// See also [ChatSummary].
|
||||||
|
|||||||
@@ -3,10 +3,14 @@ import "package:dio/dio.dart";
|
|||||||
import "package:drift/drift.dart" show Variable;
|
import "package:drift/drift.dart" show Variable;
|
||||||
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:hooks_riverpod/hooks_riverpod.dart";
|
||||||
import "package:island/database/drift_db.dart";
|
import "package:island/database/drift_db.dart";
|
||||||
import "package:island/database/message.dart";
|
import "package:island/database/message.dart";
|
||||||
|
import "package:island/models/account.dart";
|
||||||
import "package:island/models/chat.dart";
|
import "package:island/models/chat.dart";
|
||||||
import "package:island/models/file.dart";
|
import "package:island/models/file.dart";
|
||||||
|
import "package:island/models/poll.dart";
|
||||||
|
import "package:island/models/wallet.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";
|
||||||
@@ -17,6 +21,7 @@ import "package:riverpod_annotation/riverpod_annotation.dart";
|
|||||||
import "package:uuid/uuid.dart";
|
import "package:uuid/uuid.dart";
|
||||||
import "package:island/screens/chat/chat.dart";
|
import "package:island/screens/chat/chat.dart";
|
||||||
import "package:island/pods/chat/chat_rooms.dart";
|
import "package:island/pods/chat/chat_rooms.dart";
|
||||||
|
import "package:island/screens/account/profile.dart";
|
||||||
|
|
||||||
part 'messages_notifier.g.dart';
|
part 'messages_notifier.g.dart';
|
||||||
|
|
||||||
@@ -28,7 +33,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
late final SnChatMember _identity;
|
late final SnChatMember _identity;
|
||||||
|
|
||||||
final Map<String, LocalChatMessage> _pendingMessages = {};
|
final Map<String, LocalChatMessage> _pendingMessages = {};
|
||||||
final Map<String, Map<int, double>> _fileUploadProgress = {};
|
final Map<String, Map<int, double?>> _fileUploadProgress = {};
|
||||||
int? _totalCount;
|
int? _totalCount;
|
||||||
String? _searchQuery;
|
String? _searchQuery;
|
||||||
bool? _withLinks;
|
bool? _withLinks;
|
||||||
@@ -40,8 +45,11 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
bool _isSyncing = false;
|
bool _isSyncing = false;
|
||||||
bool _isJumping = false;
|
bool _isJumping = false;
|
||||||
bool _isUpdatingState = false;
|
bool _isUpdatingState = false;
|
||||||
|
bool _allRemoteMessagesFetched = false;
|
||||||
DateTime? _lastPauseTime;
|
DateTime? _lastPauseTime;
|
||||||
|
|
||||||
|
late final Future<SnAccount?> Function(String) _fetchAccount;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<List<LocalChatMessage>> build(String roomId) async {
|
FutureOr<List<LocalChatMessage>> build(String roomId) async {
|
||||||
_roomId = roomId;
|
_roomId = roomId;
|
||||||
@@ -50,6 +58,15 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
final room = await ref.watch(chatroomProvider(roomId).future);
|
final room = await ref.watch(chatroomProvider(roomId).future);
|
||||||
final identity = await ref.watch(chatroomIdentityProvider(roomId).future);
|
final identity = await ref.watch(chatroomIdentityProvider(roomId).future);
|
||||||
|
|
||||||
|
// Initialize fetch account method for corrupted data recovery
|
||||||
|
_fetchAccount = (String accountId) async {
|
||||||
|
try {
|
||||||
|
return await ref.watch(accountProvider(accountId).future);
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (room == null) {
|
if (room == null) {
|
||||||
throw Exception('Room not found');
|
throw Exception('Room not found');
|
||||||
}
|
}
|
||||||
@@ -130,6 +147,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
_roomId,
|
_roomId,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
withAttachments: withAttachments,
|
withAttachments: withAttachments,
|
||||||
|
fetchAccount: _fetchAccount,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
final chatMessagesFromDb = await _database.getMessagesForRoom(
|
final chatMessagesFromDb = await _database.getMessagesForRoom(
|
||||||
@@ -137,8 +155,16 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
offset: offset,
|
offset: offset,
|
||||||
limit: take,
|
limit: take,
|
||||||
);
|
);
|
||||||
dbMessages =
|
dbMessages = await Future.wait(
|
||||||
chatMessagesFromDb.map(_database.companionToMessage).toList();
|
chatMessagesFromDb
|
||||||
|
.map(
|
||||||
|
(msg) => _database.companionToMessage(
|
||||||
|
msg,
|
||||||
|
fetchAccount: _fetchAccount,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<LocalChatMessage> filteredMessages = dbMessages;
|
List<LocalChatMessage> filteredMessages = dbMessages;
|
||||||
@@ -199,8 +225,14 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
offset: offset,
|
offset: offset,
|
||||||
limit: take,
|
limit: take,
|
||||||
);
|
);
|
||||||
final dbMessages =
|
final dbMessages = await Future.wait(
|
||||||
chatMessagesFromDb.map(_database.companionToMessage).toList();
|
chatMessagesFromDb
|
||||||
|
.map(
|
||||||
|
(msg) =>
|
||||||
|
_database.companionToMessage(msg, fetchAccount: _fetchAccount),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
|
||||||
// Always ensure unique messages to prevent duplicate keys
|
// Always ensure unique messages to prevent duplicate keys
|
||||||
final uniqueMessages = <LocalChatMessage>[];
|
final uniqueMessages = <LocalChatMessage>[];
|
||||||
@@ -247,6 +279,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (offset >= _totalCount!) {
|
if (offset >= _totalCount!) {
|
||||||
|
_allRemoteMessagesFetched = true;
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,7 +301,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
for (final message in messages) {
|
for (final message in messages) {
|
||||||
await _database.saveMessage(_database.messageToCompanion(message));
|
await _database.saveMessageWithSender(message);
|
||||||
if (message.nonce != null) {
|
if (message.nonce != null) {
|
||||||
_pendingMessages.removeWhere(
|
_pendingMessages.removeWhere(
|
||||||
(_, pendingMsg) => pendingMsg.nonce == message.nonce,
|
(_, pendingMsg) => pendingMsg.nonce == message.nonce,
|
||||||
@@ -276,6 +309,11 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if we've fetched all remote messages
|
||||||
|
if (offset + messages.length >= _totalCount!) {
|
||||||
|
_allRemoteMessagesFetched = true;
|
||||||
|
}
|
||||||
|
|
||||||
return messages;
|
return messages;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,6 +323,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_isSyncing = true;
|
_isSyncing = true;
|
||||||
|
_allRemoteMessagesFetched = false;
|
||||||
|
|
||||||
talker.log('Starting message sync');
|
talker.log('Starting message sync');
|
||||||
Future.microtask(() => ref.read(isSyncingProvider.notifier).state = true);
|
Future.microtask(() => ref.read(isSyncingProvider.notifier).state = true);
|
||||||
@@ -297,7 +336,10 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
final lastMessage =
|
final lastMessage =
|
||||||
dbMessages.isEmpty
|
dbMessages.isEmpty
|
||||||
? null
|
? null
|
||||||
: _database.companionToMessage(dbMessages.first);
|
: await _database.companionToMessage(
|
||||||
|
dbMessages.first,
|
||||||
|
fetchAccount: _fetchAccount,
|
||||||
|
);
|
||||||
|
|
||||||
if (lastMessage == null) {
|
if (lastMessage == null) {
|
||||||
talker.log('No local messages, fetching from network');
|
talker.log('No local messages, fetching from network');
|
||||||
@@ -309,19 +351,48 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final resp = await _apiClient.post(
|
// Sync with pagination support using timestamp-based cursor
|
||||||
'/sphere/chat/${_room.id}/sync',
|
int? totalMessages;
|
||||||
data: {
|
int syncedCount = 0;
|
||||||
'last_sync_timestamp':
|
int lastSyncTimestamp =
|
||||||
lastMessage.toRemoteMessage().updatedAt.millisecondsSinceEpoch,
|
lastMessage.toRemoteMessage().updatedAt.millisecondsSinceEpoch;
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
final response = MessageSyncResponse.fromJson(resp.data);
|
do {
|
||||||
talker.log('Sync response: ${response.messages.length} changes');
|
final resp = await _apiClient.post(
|
||||||
for (final message in response.messages) {
|
'/sphere/chat/${_room.id}/sync',
|
||||||
await receiveMessage(message);
|
data: {'last_sync_timestamp': lastSyncTimestamp},
|
||||||
}
|
);
|
||||||
|
|
||||||
|
// Read total count from header on first request
|
||||||
|
if (totalMessages == null) {
|
||||||
|
totalMessages = int.parse(
|
||||||
|
resp.headers['x-total']?.firstOrNull ?? '0',
|
||||||
|
);
|
||||||
|
talker.log('Total messages to sync: $totalMessages');
|
||||||
|
}
|
||||||
|
|
||||||
|
final response = MessageSyncResponse.fromJson(resp.data);
|
||||||
|
final messagesCount = response.messages.length;
|
||||||
|
talker.log(
|
||||||
|
'Sync page: synced=$syncedCount/$totalMessages, count=$messagesCount',
|
||||||
|
);
|
||||||
|
|
||||||
|
for (final message in response.messages) {
|
||||||
|
await receiveMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
syncedCount += messagesCount;
|
||||||
|
|
||||||
|
// Update cursor to the last message's createdAt for next page
|
||||||
|
if (response.messages.isNotEmpty) {
|
||||||
|
lastSyncTimestamp =
|
||||||
|
response.messages.last.createdAt.millisecondsSinceEpoch;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue if there are more messages to fetch
|
||||||
|
} while (syncedCount < totalMessages);
|
||||||
|
|
||||||
|
talker.log('Sync complete: synced $syncedCount messages');
|
||||||
} catch (err, stackTrace) {
|
} catch (err, stackTrace) {
|
||||||
talker.log(
|
talker.log(
|
||||||
'Error syncing messages',
|
'Error syncing messages',
|
||||||
@@ -360,14 +431,35 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
withAttachments: _withAttachments,
|
withAttachments: _withAttachments,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (localMessages.isNotEmpty) {
|
// If we have local messages AND we've fetched all remote messages, return local
|
||||||
|
if (localMessages.isNotEmpty && _allRemoteMessagesFetched) {
|
||||||
return localMessages;
|
return localMessages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we haven't fetched all remote messages, check remote even if we have local
|
||||||
|
// OR if we have no local messages at all
|
||||||
if (_searchQuery == null || _searchQuery!.isEmpty) {
|
if (_searchQuery == null || _searchQuery!.isEmpty) {
|
||||||
return await _fetchAndCacheMessages(offset: offset, take: take);
|
final remoteMessages = await _fetchAndCacheMessages(
|
||||||
|
offset: offset,
|
||||||
|
take: take,
|
||||||
|
);
|
||||||
|
|
||||||
|
// If we got remote messages, re-fetch from cache to get merged result
|
||||||
|
if (remoteMessages.isNotEmpty) {
|
||||||
|
return await _getCachedMessages(
|
||||||
|
offset: offset,
|
||||||
|
take: take,
|
||||||
|
searchQuery: _searchQuery,
|
||||||
|
withLinks: _withLinks,
|
||||||
|
withAttachments: _withAttachments,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// No remote messages, return local (if any)
|
||||||
|
return localMessages;
|
||||||
} else {
|
} else {
|
||||||
return []; // If searching, and no local messages, don't fetch from network
|
// For search queries, return local only
|
||||||
|
return localMessages;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
final localMessages = await _getCachedMessages(
|
final localMessages = await _getCachedMessages(
|
||||||
@@ -387,6 +479,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
|
|
||||||
Future<void> loadInitial() async {
|
Future<void> loadInitial() async {
|
||||||
talker.log('Loading initial messages');
|
talker.log('Loading initial messages');
|
||||||
|
_allRemoteMessagesFetched = false;
|
||||||
if (_searchQuery == null || _searchQuery!.isEmpty) {
|
if (_searchQuery == null || _searchQuery!.isEmpty) {
|
||||||
syncMessages();
|
syncMessages();
|
||||||
}
|
}
|
||||||
@@ -408,6 +501,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
if (!_hasMore || state is AsyncLoading) return;
|
if (!_hasMore || state is AsyncLoading) return;
|
||||||
talker.log('Loading more messages');
|
talker.log('Loading more messages');
|
||||||
|
|
||||||
|
Future.microtask(() => ref.read(isSyncingProvider.notifier).state = true);
|
||||||
try {
|
try {
|
||||||
final currentMessages = state.value ?? [];
|
final currentMessages = state.value ?? [];
|
||||||
final offset = currentMessages.length;
|
final offset = currentMessages.length;
|
||||||
@@ -429,16 +523,23 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
stackTrace: stackTrace,
|
stackTrace: stackTrace,
|
||||||
);
|
);
|
||||||
showErrorAlert(err);
|
showErrorAlert(err);
|
||||||
|
} finally {
|
||||||
|
Future.microtask(
|
||||||
|
() => ref.read(isSyncingProvider.notifier).state = false,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> sendMessage(
|
Future<void> sendMessage(
|
||||||
|
WidgetRef ref,
|
||||||
String content,
|
String content,
|
||||||
List<UniversalFile> attachments, {
|
List<UniversalFile> attachments, {
|
||||||
|
SnPoll? poll,
|
||||||
|
SnWalletFund? fund,
|
||||||
SnChatMessage? editingTo,
|
SnChatMessage? editingTo,
|
||||||
SnChatMessage? forwardingTo,
|
SnChatMessage? forwardingTo,
|
||||||
SnChatMessage? replyingTo,
|
SnChatMessage? replyingTo,
|
||||||
Function(String, Map<int, double>)? onProgress,
|
Function(String, Map<int, double?>)? onProgress,
|
||||||
}) 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');
|
||||||
@@ -461,7 +562,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
|
|
||||||
_pendingMessages[localMessage.id] = localMessage;
|
_pendingMessages[localMessage.id] = localMessage;
|
||||||
_fileUploadProgress[localMessage.id] = {};
|
_fileUploadProgress[localMessage.id] = {};
|
||||||
await _database.saveMessage(_database.messageToCompanion(localMessage));
|
await _database.saveMessageWithSender(localMessage);
|
||||||
|
|
||||||
final currentMessages = state.value ?? [];
|
final currentMessages = state.value ?? [];
|
||||||
state = AsyncValue.data([localMessage, ...currentMessages]);
|
state = AsyncValue.data([localMessage, ...currentMessages]);
|
||||||
@@ -471,10 +572,10 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
for (var idx = 0; idx < attachments.length; idx++) {
|
for (var idx = 0; idx < attachments.length; idx++) {
|
||||||
final cloudFile =
|
final cloudFile =
|
||||||
await FileUploader.createCloudFile(
|
await FileUploader.createCloudFile(
|
||||||
|
ref: ref,
|
||||||
fileData: attachments[idx],
|
fileData: attachments[idx],
|
||||||
client: ref.read(apiClientProvider),
|
|
||||||
onProgress: (progress, _) {
|
onProgress: (progress, _) {
|
||||||
_fileUploadProgress[localMessage.id]?[idx] = progress;
|
_fileUploadProgress[localMessage.id]?[idx] = progress ?? 0.0;
|
||||||
onProgress?.call(
|
onProgress?.call(
|
||||||
localMessage.id,
|
localMessage.id,
|
||||||
_fileUploadProgress[localMessage.id] ?? {},
|
_fileUploadProgress[localMessage.id] ?? {},
|
||||||
@@ -496,6 +597,8 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
'attachments_id': cloudAttachments.map((e) => e.id).toList(),
|
'attachments_id': cloudAttachments.map((e) => e.id).toList(),
|
||||||
'replied_message_id': replyingTo?.id,
|
'replied_message_id': replyingTo?.id,
|
||||||
'forwarded_message_id': forwardingTo?.id,
|
'forwarded_message_id': forwardingTo?.id,
|
||||||
|
'poll_id': poll?.id,
|
||||||
|
'fund_id': fund?.id,
|
||||||
'meta': {},
|
'meta': {},
|
||||||
'nonce': nonce,
|
'nonce': nonce,
|
||||||
},
|
},
|
||||||
@@ -510,7 +613,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
|
|
||||||
_pendingMessages.remove(localMessage.id);
|
_pendingMessages.remove(localMessage.id);
|
||||||
await _database.deleteMessage(localMessage.id);
|
await _database.deleteMessage(localMessage.id);
|
||||||
await _database.saveMessage(_database.messageToCompanion(updatedMessage));
|
await _database.saveMessageWithSender(updatedMessage);
|
||||||
|
|
||||||
final currentMessages = state.value ?? [];
|
final currentMessages = state.value ?? [];
|
||||||
if (editingTo != null) {
|
if (editingTo != null) {
|
||||||
@@ -592,7 +695,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
|
|
||||||
_pendingMessages.remove(pendingMessageId);
|
_pendingMessages.remove(pendingMessageId);
|
||||||
await _database.deleteMessage(pendingMessageId);
|
await _database.deleteMessage(pendingMessageId);
|
||||||
await _database.saveMessage(_database.messageToCompanion(updatedMessage));
|
await _database.saveMessageWithSender(updatedMessage);
|
||||||
|
|
||||||
final newMessages =
|
final newMessages =
|
||||||
(state.value ?? []).map((m) {
|
(state.value ?? []).map((m) {
|
||||||
@@ -649,7 +752,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await _database.saveMessage(_database.messageToCompanion(localMessage));
|
await _database.saveMessageWithSender(localMessage);
|
||||||
|
|
||||||
final currentMessages = state.value ?? [];
|
final currentMessages = state.value ?? [];
|
||||||
final existingIndex = currentMessages.indexWhere(
|
final existingIndex = currentMessages.indexWhere(
|
||||||
@@ -746,7 +849,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
messageToUpdate.status,
|
messageToUpdate.status,
|
||||||
);
|
);
|
||||||
|
|
||||||
await _database.saveMessage(_database.messageToCompanion(deletedMessage));
|
await _database.saveMessageWithSender(deletedMessage);
|
||||||
|
|
||||||
if (messageIndex != -1) {
|
if (messageIndex != -1) {
|
||||||
final newList = [...currentMessages];
|
final newList = [...currentMessages];
|
||||||
@@ -870,6 +973,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
_searchQuery = null;
|
_searchQuery = null;
|
||||||
_withLinks = null;
|
_withLinks = null;
|
||||||
_withAttachments = null;
|
_withAttachments = null;
|
||||||
|
_allRemoteMessagesFetched = false;
|
||||||
loadInitial();
|
loadInitial();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -880,7 +984,10 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
await (_database.select(_database.chatMessages)
|
await (_database.select(_database.chatMessages)
|
||||||
..where((tbl) => tbl.id.equals(messageId))).getSingleOrNull();
|
..where((tbl) => tbl.id.equals(messageId))).getSingleOrNull();
|
||||||
if (localMessage != null) {
|
if (localMessage != null) {
|
||||||
return _database.companionToMessage(localMessage);
|
return _database.companionToMessage(
|
||||||
|
localMessage,
|
||||||
|
fetchAccount: _fetchAccount,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final response = await _apiClient.get(
|
final response = await _apiClient.get(
|
||||||
@@ -892,7 +999,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
MessageStatus.sent,
|
MessageStatus.sent,
|
||||||
);
|
);
|
||||||
|
|
||||||
await _database.saveMessage(_database.messageToCompanion(message));
|
await _database.saveMessageWithSender(message);
|
||||||
return message;
|
return message;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e is DioException) return null;
|
if (e is DioException) return null;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ part of 'messages_notifier.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$messagesNotifierHash() => r'6adefd9152cdd686c2a863964993f24c42d405b5';
|
String _$messagesNotifierHash() => r'27ce32c54e317a04e1d554ed4a70a24e4503fdd1';
|
||||||
|
|
||||||
/// Copied from Dart SDK
|
/// Copied from Dart SDK
|
||||||
class _SystemHash {
|
class _SystemHash {
|
||||||
|
|||||||
198
lib/pods/file_list.dart
Normal file
198
lib/pods/file_list.dart
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:island/models/file.dart';
|
||||||
|
import 'package:island/models/file_list_item.dart';
|
||||||
|
import 'package:island/pods/network.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
|
||||||
|
|
||||||
|
part 'file_list.g.dart';
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
class CloudFileListNotifier extends _$CloudFileListNotifier
|
||||||
|
with CursorPagingNotifierMixin<FileListItem> {
|
||||||
|
String _currentPath = '/';
|
||||||
|
String? _poolId;
|
||||||
|
String? _query;
|
||||||
|
String? _order;
|
||||||
|
bool _orderDesc = false;
|
||||||
|
|
||||||
|
void setPath(String path) {
|
||||||
|
_currentPath = path;
|
||||||
|
ref.invalidateSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setPool(String? poolId) {
|
||||||
|
_poolId = poolId;
|
||||||
|
ref.invalidateSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setQuery(String? query) {
|
||||||
|
_query = query;
|
||||||
|
ref.invalidateSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setOrder(String? order) {
|
||||||
|
_order = order;
|
||||||
|
ref.invalidateSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setOrderDesc(bool orderDesc) {
|
||||||
|
_orderDesc = orderDesc;
|
||||||
|
ref.invalidateSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<CursorPagingData<FileListItem>> build() => fetch(cursor: null);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<CursorPagingData<FileListItem>> fetch({
|
||||||
|
required String? cursor,
|
||||||
|
}) async {
|
||||||
|
final client = ref.read(apiClientProvider);
|
||||||
|
|
||||||
|
final queryParameters = <String, String>{'path': _currentPath};
|
||||||
|
|
||||||
|
if (_poolId != null) {
|
||||||
|
queryParameters['pool'] = _poolId!;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_query != null) {
|
||||||
|
queryParameters['query'] = _query!;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_order != null) {
|
||||||
|
queryParameters['order'] = _order!;
|
||||||
|
}
|
||||||
|
|
||||||
|
queryParameters['orderDesc'] = _orderDesc.toString();
|
||||||
|
|
||||||
|
final response = await client.get(
|
||||||
|
'/drive/index/browse',
|
||||||
|
queryParameters: queryParameters,
|
||||||
|
);
|
||||||
|
|
||||||
|
final List<String> folders =
|
||||||
|
(response.data['folders'] as List).map((e) => e as String).toList();
|
||||||
|
final List<SnCloudFileIndex> files =
|
||||||
|
(response.data['files'] as List)
|
||||||
|
.map((e) => SnCloudFileIndex.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final List<FileListItem> items = [
|
||||||
|
...folders.map((folderName) => FileListItem.folder(folderName)),
|
||||||
|
...files.map((file) => FileListItem.file(file)),
|
||||||
|
];
|
||||||
|
|
||||||
|
// The new API returns all files in the path, no pagination
|
||||||
|
return CursorPagingData(items: items, hasMore: false, nextCursor: null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
Future<Map<String, dynamic>?> billingUsage(Ref ref) async {
|
||||||
|
final client = ref.read(apiClientProvider);
|
||||||
|
final response = await client.get('/drive/billing/usage');
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
class UnindexedFileListNotifier extends _$UnindexedFileListNotifier
|
||||||
|
with CursorPagingNotifierMixin<FileListItem> {
|
||||||
|
String? _poolId;
|
||||||
|
bool _recycled = false;
|
||||||
|
String? _query;
|
||||||
|
String? _order;
|
||||||
|
bool _orderDesc = false;
|
||||||
|
|
||||||
|
void setPool(String? poolId) {
|
||||||
|
_poolId = poolId;
|
||||||
|
ref.invalidateSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setRecycled(bool recycled) {
|
||||||
|
_recycled = recycled;
|
||||||
|
ref.invalidateSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setQuery(String? query) {
|
||||||
|
_query = query;
|
||||||
|
ref.invalidateSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setOrder(String? order) {
|
||||||
|
_order = order;
|
||||||
|
ref.invalidateSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setOrderDesc(bool orderDesc) {
|
||||||
|
_orderDesc = orderDesc;
|
||||||
|
ref.invalidateSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<CursorPagingData<FileListItem>> build() => fetch(cursor: null);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<CursorPagingData<FileListItem>> fetch({
|
||||||
|
required String? cursor,
|
||||||
|
}) async {
|
||||||
|
final client = ref.read(apiClientProvider);
|
||||||
|
|
||||||
|
final offset = cursor != null ? int.tryParse(cursor) ?? 0 : 0;
|
||||||
|
const take = 50; // Default page size
|
||||||
|
|
||||||
|
final queryParameters = <String, String>{
|
||||||
|
'take': take.toString(),
|
||||||
|
'offset': offset.toString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (_poolId != null) {
|
||||||
|
queryParameters['pool'] = _poolId!;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_recycled) {
|
||||||
|
queryParameters['recycled'] = _recycled.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_query != null) {
|
||||||
|
queryParameters['query'] = _query!;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_order != null) {
|
||||||
|
queryParameters['order'] = _order!;
|
||||||
|
}
|
||||||
|
|
||||||
|
queryParameters['orderDesc'] = _orderDesc.toString();
|
||||||
|
|
||||||
|
final response = await client.get(
|
||||||
|
'/drive/index/unindexed',
|
||||||
|
queryParameters: queryParameters,
|
||||||
|
);
|
||||||
|
|
||||||
|
final total = int.tryParse(response.headers.value('x-total') ?? '0') ?? 0;
|
||||||
|
|
||||||
|
final List<SnCloudFile> files =
|
||||||
|
(response.data as List)
|
||||||
|
.map((e) => SnCloudFile.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final List<FileListItem> items =
|
||||||
|
files.map((file) => FileListItem.unindexedFile(file)).toList();
|
||||||
|
|
||||||
|
final hasMore = offset + take < total;
|
||||||
|
final nextCursor = hasMore ? (offset + take).toString() : null;
|
||||||
|
|
||||||
|
return CursorPagingData(
|
||||||
|
items: items,
|
||||||
|
hasMore: hasMore,
|
||||||
|
nextCursor: nextCursor,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
Future<Map<String, dynamic>?> billingQuota(Ref ref) async {
|
||||||
|
final client = ref.read(apiClientProvider);
|
||||||
|
final response = await client.get('/drive/billing/quota');
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
@@ -45,13 +45,13 @@ final billingQuotaProvider =
|
|||||||
// ignore: unused_element
|
// ignore: unused_element
|
||||||
typedef BillingQuotaRef = AutoDisposeFutureProviderRef<Map<String, dynamic>?>;
|
typedef BillingQuotaRef = AutoDisposeFutureProviderRef<Map<String, dynamic>?>;
|
||||||
String _$cloudFileListNotifierHash() =>
|
String _$cloudFileListNotifierHash() =>
|
||||||
r'22c45a8ea23147a3835ba870ad2f0bb833f853ea';
|
r'533dfa86f920b60cf7491fb4aeb95ece19e428af';
|
||||||
|
|
||||||
/// See also [CloudFileListNotifier].
|
/// See also [CloudFileListNotifier].
|
||||||
@ProviderFor(CloudFileListNotifier)
|
@ProviderFor(CloudFileListNotifier)
|
||||||
final cloudFileListNotifierProvider = AutoDisposeAsyncNotifierProvider<
|
final cloudFileListNotifierProvider = AutoDisposeAsyncNotifierProvider<
|
||||||
CloudFileListNotifier,
|
CloudFileListNotifier,
|
||||||
CursorPagingData<SnCloudFile>
|
CursorPagingData<FileListItem>
|
||||||
>.internal(
|
>.internal(
|
||||||
CloudFileListNotifier.new,
|
CloudFileListNotifier.new,
|
||||||
name: r'cloudFileListNotifierProvider',
|
name: r'cloudFileListNotifierProvider',
|
||||||
@@ -64,6 +64,27 @@ final cloudFileListNotifierProvider = AutoDisposeAsyncNotifierProvider<
|
|||||||
);
|
);
|
||||||
|
|
||||||
typedef _$CloudFileListNotifier =
|
typedef _$CloudFileListNotifier =
|
||||||
AutoDisposeAsyncNotifier<CursorPagingData<SnCloudFile>>;
|
AutoDisposeAsyncNotifier<CursorPagingData<FileListItem>>;
|
||||||
|
String _$unindexedFileListNotifierHash() =>
|
||||||
|
r'afa487d7b956b71b21ca1b073a01364a34ede1d5';
|
||||||
|
|
||||||
|
/// See also [UnindexedFileListNotifier].
|
||||||
|
@ProviderFor(UnindexedFileListNotifier)
|
||||||
|
final unindexedFileListNotifierProvider = AutoDisposeAsyncNotifierProvider<
|
||||||
|
UnindexedFileListNotifier,
|
||||||
|
CursorPagingData<FileListItem>
|
||||||
|
>.internal(
|
||||||
|
UnindexedFileListNotifier.new,
|
||||||
|
name: r'unindexedFileListNotifierProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$unindexedFileListNotifierHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef _$UnindexedFileListNotifier =
|
||||||
|
AutoDisposeAsyncNotifier<CursorPagingData<FileListItem>>;
|
||||||
// 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
|
||||||
16
lib/pods/file_references.dart
Normal file
16
lib/pods/file_references.dart
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
import 'package:island/models/reference.dart';
|
||||||
|
import 'package:island/pods/network.dart';
|
||||||
|
|
||||||
|
part 'file_references.g.dart';
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
Future<List<Reference>> fileReferences(Ref ref, String fileId) async {
|
||||||
|
final client = ref.read(apiClientProvider);
|
||||||
|
final response = await client.get('/drive/files/$fileId/references');
|
||||||
|
final list = response.data as List<dynamic>;
|
||||||
|
return list
|
||||||
|
.map((json) => Reference.fromJson(json as Map<String, dynamic>))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
153
lib/pods/file_references.g.dart
Normal file
153
lib/pods/file_references.g.dart
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'file_references.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
String _$fileReferencesHash() => r'd66c678c221f61978bdb242b98e6dbe31d0c204b';
|
||||||
|
|
||||||
|
/// Copied from Dart SDK
|
||||||
|
class _SystemHash {
|
||||||
|
_SystemHash._();
|
||||||
|
|
||||||
|
static int combine(int hash, int value) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + value);
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||||
|
return hash ^ (hash >> 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int finish(int hash) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = hash ^ (hash >> 11);
|
||||||
|
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [fileReferences].
|
||||||
|
@ProviderFor(fileReferences)
|
||||||
|
const fileReferencesProvider = FileReferencesFamily();
|
||||||
|
|
||||||
|
/// See also [fileReferences].
|
||||||
|
class FileReferencesFamily extends Family<AsyncValue<List<Reference>>> {
|
||||||
|
/// See also [fileReferences].
|
||||||
|
const FileReferencesFamily();
|
||||||
|
|
||||||
|
/// See also [fileReferences].
|
||||||
|
FileReferencesProvider call(String fileId) {
|
||||||
|
return FileReferencesProvider(fileId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
FileReferencesProvider getProviderOverride(
|
||||||
|
covariant FileReferencesProvider provider,
|
||||||
|
) {
|
||||||
|
return call(provider.fileId);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||||
|
_allTransitiveDependencies;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get name => r'fileReferencesProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [fileReferences].
|
||||||
|
class FileReferencesProvider
|
||||||
|
extends AutoDisposeFutureProvider<List<Reference>> {
|
||||||
|
/// See also [fileReferences].
|
||||||
|
FileReferencesProvider(String fileId)
|
||||||
|
: this._internal(
|
||||||
|
(ref) => fileReferences(ref as FileReferencesRef, fileId),
|
||||||
|
from: fileReferencesProvider,
|
||||||
|
name: r'fileReferencesProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$fileReferencesHash,
|
||||||
|
dependencies: FileReferencesFamily._dependencies,
|
||||||
|
allTransitiveDependencies:
|
||||||
|
FileReferencesFamily._allTransitiveDependencies,
|
||||||
|
fileId: fileId,
|
||||||
|
);
|
||||||
|
|
||||||
|
FileReferencesProvider._internal(
|
||||||
|
super._createNotifier, {
|
||||||
|
required super.name,
|
||||||
|
required super.dependencies,
|
||||||
|
required super.allTransitiveDependencies,
|
||||||
|
required super.debugGetCreateSourceHash,
|
||||||
|
required super.from,
|
||||||
|
required this.fileId,
|
||||||
|
}) : super.internal();
|
||||||
|
|
||||||
|
final String fileId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Override overrideWith(
|
||||||
|
FutureOr<List<Reference>> Function(FileReferencesRef provider) create,
|
||||||
|
) {
|
||||||
|
return ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
override: FileReferencesProvider._internal(
|
||||||
|
(ref) => create(ref as FileReferencesRef),
|
||||||
|
from: from,
|
||||||
|
name: null,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
debugGetCreateSourceHash: null,
|
||||||
|
fileId: fileId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AutoDisposeFutureProviderElement<List<Reference>> createElement() {
|
||||||
|
return _FileReferencesProviderElement(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is FileReferencesProvider && other.fileId == fileId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, fileId.hashCode);
|
||||||
|
|
||||||
|
return _SystemHash.finish(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
mixin FileReferencesRef on AutoDisposeFutureProviderRef<List<Reference>> {
|
||||||
|
/// The parameter `fileId` of this provider.
|
||||||
|
String get fileId;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FileReferencesProviderElement
|
||||||
|
extends AutoDisposeFutureProviderElement<List<Reference>>
|
||||||
|
with FileReferencesRef {
|
||||||
|
_FileReferencesProviderElement(super.provider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get fileId => (origin as FileReferencesProvider).fileId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
159
lib/pods/site_files.dart
Normal file
159
lib/pods/site_files.dart
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:http_parser/http_parser.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:island/models/site_file.dart';
|
||||||
|
import 'package:island/pods/network.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
|
part 'site_files.g.dart';
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
Future<List<SnSiteFileEntry>> siteFiles(
|
||||||
|
Ref ref, {
|
||||||
|
required String siteId,
|
||||||
|
String? path,
|
||||||
|
}) async {
|
||||||
|
final apiClient = ref.watch(apiClientProvider);
|
||||||
|
final queryParams = path != null ? {'path': path} : null;
|
||||||
|
final resp = await apiClient.get(
|
||||||
|
'/zone/sites/$siteId/files',
|
||||||
|
queryParameters: queryParams,
|
||||||
|
);
|
||||||
|
final data = resp.data as List<dynamic>;
|
||||||
|
return data.map((json) => SnSiteFileEntry.fromJson(json)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
Future<SnFileContent> siteFileContent(
|
||||||
|
Ref ref, {
|
||||||
|
required String siteId,
|
||||||
|
required String relativePath,
|
||||||
|
}) async {
|
||||||
|
final apiClient = ref.watch(apiClientProvider);
|
||||||
|
final resp = await apiClient.get(
|
||||||
|
'/zone/sites/$siteId/files/content/$relativePath',
|
||||||
|
);
|
||||||
|
final content =
|
||||||
|
resp.data is String
|
||||||
|
? resp.data
|
||||||
|
: SnFileContent.fromJson(resp.data).content;
|
||||||
|
return SnFileContent(content: content);
|
||||||
|
}
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
Future<String> siteFileContentRaw(
|
||||||
|
Ref ref, {
|
||||||
|
required String siteId,
|
||||||
|
required String relativePath,
|
||||||
|
}) async {
|
||||||
|
final apiClient = ref.watch(apiClientProvider);
|
||||||
|
final resp = await apiClient.get(
|
||||||
|
'/zone/sites/$siteId/files/content/$relativePath',
|
||||||
|
);
|
||||||
|
return resp.data is String ? resp.data : resp.data['content'] as String;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SiteFilesNotifier
|
||||||
|
extends
|
||||||
|
AutoDisposeFamilyAsyncNotifier<
|
||||||
|
List<SnSiteFileEntry>,
|
||||||
|
({String siteId, String? path})
|
||||||
|
> {
|
||||||
|
@override
|
||||||
|
Future<List<SnSiteFileEntry>> build(
|
||||||
|
({String siteId, String? path}) arg,
|
||||||
|
) async {
|
||||||
|
return fetchFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<SnSiteFileEntry>> fetchFiles() async {
|
||||||
|
try {
|
||||||
|
final apiClient = ref.read(apiClientProvider);
|
||||||
|
final queryParams = arg.path != null ? {'path': arg.path} : null;
|
||||||
|
final resp = await apiClient.get(
|
||||||
|
'/zone/sites/${arg.siteId}/files',
|
||||||
|
queryParameters: queryParams,
|
||||||
|
);
|
||||||
|
final data = resp.data as List<dynamic>;
|
||||||
|
return data.map((json) => SnSiteFileEntry.fromJson(json)).toList();
|
||||||
|
} catch (e) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> uploadFile(File file, String filePath) async {
|
||||||
|
state = const AsyncValue.loading();
|
||||||
|
try {
|
||||||
|
final apiClient = ref.read(apiClientProvider);
|
||||||
|
|
||||||
|
// Create multipart form data
|
||||||
|
final formData = FormData.fromMap({
|
||||||
|
'filePath': filePath,
|
||||||
|
'file': await MultipartFile.fromFile(
|
||||||
|
file.path,
|
||||||
|
filename: file.path.split('/').last,
|
||||||
|
contentType: MediaType('application', 'octet-stream'),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
await apiClient.post(
|
||||||
|
'/zone/sites/${arg.siteId}/files/upload',
|
||||||
|
data: formData,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Refresh the files list
|
||||||
|
ref.invalidate(siteFilesProvider(siteId: arg.siteId, path: arg.path));
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
state = AsyncValue.error(error, stackTrace);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateFileContent(String relativePath, String newContent) async {
|
||||||
|
state = const AsyncValue.loading();
|
||||||
|
try {
|
||||||
|
final apiClient = ref.read(apiClientProvider);
|
||||||
|
await apiClient.put(
|
||||||
|
'/zone/sites/${arg.siteId}/files/edit/$relativePath',
|
||||||
|
data: {'new_content': newContent},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Refresh the files list
|
||||||
|
ref.invalidate(siteFilesProvider(siteId: arg.siteId, path: arg.path));
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
state = AsyncValue.error(error, stackTrace);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteFile(String relativePath) async {
|
||||||
|
state = const AsyncValue.loading();
|
||||||
|
try {
|
||||||
|
final apiClient = ref.read(apiClientProvider);
|
||||||
|
await apiClient.delete(
|
||||||
|
'/zone/sites/${arg.siteId}/files/delete/$relativePath',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Refresh the files list
|
||||||
|
ref.invalidate(siteFilesProvider(siteId: arg.siteId, path: arg.path));
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
state = AsyncValue.error(error, stackTrace);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> createDirectory(String directoryPath) async {
|
||||||
|
// For directories, we upload a dummy file first then delete it or create through upload
|
||||||
|
// Actually, according to API docs, directories are created when uploading files to them
|
||||||
|
// So we'll just invalidate to refresh the list
|
||||||
|
ref.invalidate(siteFilesProvider(siteId: arg.siteId, path: arg.path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final siteFilesNotifierProvider = AsyncNotifierProvider.autoDispose.family<
|
||||||
|
SiteFilesNotifier,
|
||||||
|
List<SnSiteFileEntry>,
|
||||||
|
({String siteId, String? path})
|
||||||
|
>(SiteFilesNotifier.new);
|
||||||
451
lib/pods/site_files.g.dart
Normal file
451
lib/pods/site_files.g.dart
Normal file
@@ -0,0 +1,451 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'site_files.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
String _$siteFilesHash() => r'd4029e6c160edcd454eb39ef1c19427b7f95a8d8';
|
||||||
|
|
||||||
|
/// Copied from Dart SDK
|
||||||
|
class _SystemHash {
|
||||||
|
_SystemHash._();
|
||||||
|
|
||||||
|
static int combine(int hash, int value) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + value);
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||||
|
return hash ^ (hash >> 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int finish(int hash) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = hash ^ (hash >> 11);
|
||||||
|
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [siteFiles].
|
||||||
|
@ProviderFor(siteFiles)
|
||||||
|
const siteFilesProvider = SiteFilesFamily();
|
||||||
|
|
||||||
|
/// See also [siteFiles].
|
||||||
|
class SiteFilesFamily extends Family<AsyncValue<List<SnSiteFileEntry>>> {
|
||||||
|
/// See also [siteFiles].
|
||||||
|
const SiteFilesFamily();
|
||||||
|
|
||||||
|
/// See also [siteFiles].
|
||||||
|
SiteFilesProvider call({required String siteId, String? path}) {
|
||||||
|
return SiteFilesProvider(siteId: siteId, path: path);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
SiteFilesProvider getProviderOverride(covariant SiteFilesProvider provider) {
|
||||||
|
return call(siteId: provider.siteId, path: provider.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||||
|
_allTransitiveDependencies;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get name => r'siteFilesProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [siteFiles].
|
||||||
|
class SiteFilesProvider
|
||||||
|
extends AutoDisposeFutureProvider<List<SnSiteFileEntry>> {
|
||||||
|
/// See also [siteFiles].
|
||||||
|
SiteFilesProvider({required String siteId, String? path})
|
||||||
|
: this._internal(
|
||||||
|
(ref) => siteFiles(ref as SiteFilesRef, siteId: siteId, path: path),
|
||||||
|
from: siteFilesProvider,
|
||||||
|
name: r'siteFilesProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$siteFilesHash,
|
||||||
|
dependencies: SiteFilesFamily._dependencies,
|
||||||
|
allTransitiveDependencies: SiteFilesFamily._allTransitiveDependencies,
|
||||||
|
siteId: siteId,
|
||||||
|
path: path,
|
||||||
|
);
|
||||||
|
|
||||||
|
SiteFilesProvider._internal(
|
||||||
|
super._createNotifier, {
|
||||||
|
required super.name,
|
||||||
|
required super.dependencies,
|
||||||
|
required super.allTransitiveDependencies,
|
||||||
|
required super.debugGetCreateSourceHash,
|
||||||
|
required super.from,
|
||||||
|
required this.siteId,
|
||||||
|
required this.path,
|
||||||
|
}) : super.internal();
|
||||||
|
|
||||||
|
final String siteId;
|
||||||
|
final String? path;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Override overrideWith(
|
||||||
|
FutureOr<List<SnSiteFileEntry>> Function(SiteFilesRef provider) create,
|
||||||
|
) {
|
||||||
|
return ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
override: SiteFilesProvider._internal(
|
||||||
|
(ref) => create(ref as SiteFilesRef),
|
||||||
|
from: from,
|
||||||
|
name: null,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
debugGetCreateSourceHash: null,
|
||||||
|
siteId: siteId,
|
||||||
|
path: path,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AutoDisposeFutureProviderElement<List<SnSiteFileEntry>> createElement() {
|
||||||
|
return _SiteFilesProviderElement(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is SiteFilesProvider &&
|
||||||
|
other.siteId == siteId &&
|
||||||
|
other.path == path;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, siteId.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, path.hashCode);
|
||||||
|
|
||||||
|
return _SystemHash.finish(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
mixin SiteFilesRef on AutoDisposeFutureProviderRef<List<SnSiteFileEntry>> {
|
||||||
|
/// The parameter `siteId` of this provider.
|
||||||
|
String get siteId;
|
||||||
|
|
||||||
|
/// The parameter `path` of this provider.
|
||||||
|
String? get path;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SiteFilesProviderElement
|
||||||
|
extends AutoDisposeFutureProviderElement<List<SnSiteFileEntry>>
|
||||||
|
with SiteFilesRef {
|
||||||
|
_SiteFilesProviderElement(super.provider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get siteId => (origin as SiteFilesProvider).siteId;
|
||||||
|
@override
|
||||||
|
String? get path => (origin as SiteFilesProvider).path;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$siteFileContentHash() => r'b594ad4f8c54555e742ece94ee001092cb2f83d1';
|
||||||
|
|
||||||
|
/// See also [siteFileContent].
|
||||||
|
@ProviderFor(siteFileContent)
|
||||||
|
const siteFileContentProvider = SiteFileContentFamily();
|
||||||
|
|
||||||
|
/// See also [siteFileContent].
|
||||||
|
class SiteFileContentFamily extends Family<AsyncValue<SnFileContent>> {
|
||||||
|
/// See also [siteFileContent].
|
||||||
|
const SiteFileContentFamily();
|
||||||
|
|
||||||
|
/// See also [siteFileContent].
|
||||||
|
SiteFileContentProvider call({
|
||||||
|
required String siteId,
|
||||||
|
required String relativePath,
|
||||||
|
}) {
|
||||||
|
return SiteFileContentProvider(siteId: siteId, relativePath: relativePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
SiteFileContentProvider getProviderOverride(
|
||||||
|
covariant SiteFileContentProvider provider,
|
||||||
|
) {
|
||||||
|
return call(siteId: provider.siteId, relativePath: provider.relativePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||||
|
_allTransitiveDependencies;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get name => r'siteFileContentProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [siteFileContent].
|
||||||
|
class SiteFileContentProvider extends AutoDisposeFutureProvider<SnFileContent> {
|
||||||
|
/// See also [siteFileContent].
|
||||||
|
SiteFileContentProvider({
|
||||||
|
required String siteId,
|
||||||
|
required String relativePath,
|
||||||
|
}) : this._internal(
|
||||||
|
(ref) => siteFileContent(
|
||||||
|
ref as SiteFileContentRef,
|
||||||
|
siteId: siteId,
|
||||||
|
relativePath: relativePath,
|
||||||
|
),
|
||||||
|
from: siteFileContentProvider,
|
||||||
|
name: r'siteFileContentProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$siteFileContentHash,
|
||||||
|
dependencies: SiteFileContentFamily._dependencies,
|
||||||
|
allTransitiveDependencies:
|
||||||
|
SiteFileContentFamily._allTransitiveDependencies,
|
||||||
|
siteId: siteId,
|
||||||
|
relativePath: relativePath,
|
||||||
|
);
|
||||||
|
|
||||||
|
SiteFileContentProvider._internal(
|
||||||
|
super._createNotifier, {
|
||||||
|
required super.name,
|
||||||
|
required super.dependencies,
|
||||||
|
required super.allTransitiveDependencies,
|
||||||
|
required super.debugGetCreateSourceHash,
|
||||||
|
required super.from,
|
||||||
|
required this.siteId,
|
||||||
|
required this.relativePath,
|
||||||
|
}) : super.internal();
|
||||||
|
|
||||||
|
final String siteId;
|
||||||
|
final String relativePath;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Override overrideWith(
|
||||||
|
FutureOr<SnFileContent> Function(SiteFileContentRef provider) create,
|
||||||
|
) {
|
||||||
|
return ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
override: SiteFileContentProvider._internal(
|
||||||
|
(ref) => create(ref as SiteFileContentRef),
|
||||||
|
from: from,
|
||||||
|
name: null,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
debugGetCreateSourceHash: null,
|
||||||
|
siteId: siteId,
|
||||||
|
relativePath: relativePath,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AutoDisposeFutureProviderElement<SnFileContent> createElement() {
|
||||||
|
return _SiteFileContentProviderElement(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is SiteFileContentProvider &&
|
||||||
|
other.siteId == siteId &&
|
||||||
|
other.relativePath == relativePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, siteId.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, relativePath.hashCode);
|
||||||
|
|
||||||
|
return _SystemHash.finish(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
mixin SiteFileContentRef on AutoDisposeFutureProviderRef<SnFileContent> {
|
||||||
|
/// The parameter `siteId` of this provider.
|
||||||
|
String get siteId;
|
||||||
|
|
||||||
|
/// The parameter `relativePath` of this provider.
|
||||||
|
String get relativePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SiteFileContentProviderElement
|
||||||
|
extends AutoDisposeFutureProviderElement<SnFileContent>
|
||||||
|
with SiteFileContentRef {
|
||||||
|
_SiteFileContentProviderElement(super.provider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get siteId => (origin as SiteFileContentProvider).siteId;
|
||||||
|
@override
|
||||||
|
String get relativePath => (origin as SiteFileContentProvider).relativePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$siteFileContentRawHash() =>
|
||||||
|
r'd0331c30698a9f4b90fe9b79273ff5914fa46616';
|
||||||
|
|
||||||
|
/// See also [siteFileContentRaw].
|
||||||
|
@ProviderFor(siteFileContentRaw)
|
||||||
|
const siteFileContentRawProvider = SiteFileContentRawFamily();
|
||||||
|
|
||||||
|
/// See also [siteFileContentRaw].
|
||||||
|
class SiteFileContentRawFamily extends Family<AsyncValue<String>> {
|
||||||
|
/// See also [siteFileContentRaw].
|
||||||
|
const SiteFileContentRawFamily();
|
||||||
|
|
||||||
|
/// See also [siteFileContentRaw].
|
||||||
|
SiteFileContentRawProvider call({
|
||||||
|
required String siteId,
|
||||||
|
required String relativePath,
|
||||||
|
}) {
|
||||||
|
return SiteFileContentRawProvider(
|
||||||
|
siteId: siteId,
|
||||||
|
relativePath: relativePath,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
SiteFileContentRawProvider getProviderOverride(
|
||||||
|
covariant SiteFileContentRawProvider provider,
|
||||||
|
) {
|
||||||
|
return call(siteId: provider.siteId, relativePath: provider.relativePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||||
|
_allTransitiveDependencies;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get name => r'siteFileContentRawProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [siteFileContentRaw].
|
||||||
|
class SiteFileContentRawProvider extends AutoDisposeFutureProvider<String> {
|
||||||
|
/// See also [siteFileContentRaw].
|
||||||
|
SiteFileContentRawProvider({
|
||||||
|
required String siteId,
|
||||||
|
required String relativePath,
|
||||||
|
}) : this._internal(
|
||||||
|
(ref) => siteFileContentRaw(
|
||||||
|
ref as SiteFileContentRawRef,
|
||||||
|
siteId: siteId,
|
||||||
|
relativePath: relativePath,
|
||||||
|
),
|
||||||
|
from: siteFileContentRawProvider,
|
||||||
|
name: r'siteFileContentRawProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$siteFileContentRawHash,
|
||||||
|
dependencies: SiteFileContentRawFamily._dependencies,
|
||||||
|
allTransitiveDependencies:
|
||||||
|
SiteFileContentRawFamily._allTransitiveDependencies,
|
||||||
|
siteId: siteId,
|
||||||
|
relativePath: relativePath,
|
||||||
|
);
|
||||||
|
|
||||||
|
SiteFileContentRawProvider._internal(
|
||||||
|
super._createNotifier, {
|
||||||
|
required super.name,
|
||||||
|
required super.dependencies,
|
||||||
|
required super.allTransitiveDependencies,
|
||||||
|
required super.debugGetCreateSourceHash,
|
||||||
|
required super.from,
|
||||||
|
required this.siteId,
|
||||||
|
required this.relativePath,
|
||||||
|
}) : super.internal();
|
||||||
|
|
||||||
|
final String siteId;
|
||||||
|
final String relativePath;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Override overrideWith(
|
||||||
|
FutureOr<String> Function(SiteFileContentRawRef provider) create,
|
||||||
|
) {
|
||||||
|
return ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
override: SiteFileContentRawProvider._internal(
|
||||||
|
(ref) => create(ref as SiteFileContentRawRef),
|
||||||
|
from: from,
|
||||||
|
name: null,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
debugGetCreateSourceHash: null,
|
||||||
|
siteId: siteId,
|
||||||
|
relativePath: relativePath,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AutoDisposeFutureProviderElement<String> createElement() {
|
||||||
|
return _SiteFileContentRawProviderElement(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is SiteFileContentRawProvider &&
|
||||||
|
other.siteId == siteId &&
|
||||||
|
other.relativePath == relativePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, siteId.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, relativePath.hashCode);
|
||||||
|
|
||||||
|
return _SystemHash.finish(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
mixin SiteFileContentRawRef on AutoDisposeFutureProviderRef<String> {
|
||||||
|
/// The parameter `siteId` of this provider.
|
||||||
|
String get siteId;
|
||||||
|
|
||||||
|
/// The parameter `relativePath` of this provider.
|
||||||
|
String get relativePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SiteFileContentRawProviderElement
|
||||||
|
extends AutoDisposeFutureProviderElement<String>
|
||||||
|
with SiteFileContentRawRef {
|
||||||
|
_SiteFileContentRawProviderElement(super.provider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get siteId => (origin as SiteFileContentRawProvider).siteId;
|
||||||
|
@override
|
||||||
|
String get relativePath =>
|
||||||
|
(origin as SiteFileContentRawProvider).relativePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
116
lib/pods/site_pages.dart
Normal file
116
lib/pods/site_pages.dart
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:island/models/publication_site.dart';
|
||||||
|
import 'package:island/pods/network.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
|
part 'site_pages.g.dart';
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
Future<List<SnPublicationPage>> sitePages(
|
||||||
|
Ref ref,
|
||||||
|
String pubName,
|
||||||
|
String siteSlug,
|
||||||
|
) async {
|
||||||
|
final apiClient = ref.watch(apiClientProvider);
|
||||||
|
final resp = await apiClient.get('/zone/sites/$pubName/$siteSlug/pages');
|
||||||
|
final data = resp.data as List<dynamic>;
|
||||||
|
return data.map((json) => SnPublicationPage.fromJson(json)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
Future<SnPublicationPage> sitePage(Ref ref, String pageId) async {
|
||||||
|
final apiClient = ref.watch(apiClientProvider);
|
||||||
|
final resp = await apiClient.get('/zone/sites/pages/$pageId');
|
||||||
|
return SnPublicationPage.fromJson(resp.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
class SitePagesNotifier
|
||||||
|
extends
|
||||||
|
AutoDisposeFamilyAsyncNotifier<
|
||||||
|
List<SnPublicationPage>,
|
||||||
|
({String pubName, String siteSlug})
|
||||||
|
> {
|
||||||
|
@override
|
||||||
|
Future<List<SnPublicationPage>> build(
|
||||||
|
({String pubName, String siteSlug}) arg,
|
||||||
|
) async {
|
||||||
|
return fetchPages();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<SnPublicationPage>> fetchPages() async {
|
||||||
|
try {
|
||||||
|
final apiClient = ref.read(apiClientProvider);
|
||||||
|
final resp = await apiClient.get(
|
||||||
|
'/zone/sites/${arg.pubName}/${arg.siteSlug}/pages',
|
||||||
|
);
|
||||||
|
final data = resp.data as List<dynamic>;
|
||||||
|
return data.map((json) => SnPublicationPage.fromJson(json)).toList();
|
||||||
|
} catch (e) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<SnPublicationPage?> createPage(Map<String, dynamic> pageData) async {
|
||||||
|
state = const AsyncValue.loading();
|
||||||
|
try {
|
||||||
|
final apiClient = ref.read(apiClientProvider);
|
||||||
|
final resp = await apiClient.post(
|
||||||
|
'/zone/sites/${arg.pubName}/${arg.siteSlug}/pages',
|
||||||
|
data: pageData,
|
||||||
|
);
|
||||||
|
final newPage = SnPublicationPage.fromJson(resp.data);
|
||||||
|
|
||||||
|
// Refresh the pages list
|
||||||
|
ref.invalidate(sitePagesProvider(arg.pubName, arg.siteSlug));
|
||||||
|
|
||||||
|
return newPage;
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
state = AsyncValue.error(error, stackTrace);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<SnPublicationPage?> updatePage(
|
||||||
|
String pageId,
|
||||||
|
Map<String, dynamic> pageData,
|
||||||
|
) async {
|
||||||
|
state = const AsyncValue.loading();
|
||||||
|
try {
|
||||||
|
final apiClient = ref.read(apiClientProvider);
|
||||||
|
final resp = await apiClient.patch(
|
||||||
|
'/zone/sites/pages/$pageId',
|
||||||
|
data: pageData,
|
||||||
|
);
|
||||||
|
final updatedPage = SnPublicationPage.fromJson(resp.data);
|
||||||
|
|
||||||
|
// Refresh the pages list
|
||||||
|
ref.invalidate(sitePagesProvider(arg.pubName, arg.siteSlug));
|
||||||
|
|
||||||
|
return updatedPage;
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
state = AsyncValue.error(error, stackTrace);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deletePage(String pageId) async {
|
||||||
|
state = const AsyncValue.loading();
|
||||||
|
try {
|
||||||
|
final apiClient = ref.read(apiClientProvider);
|
||||||
|
await apiClient.delete('/zone/sites/pages/$pageId');
|
||||||
|
|
||||||
|
// Refresh the pages list
|
||||||
|
ref.invalidate(sitePagesProvider(arg.pubName, arg.siteSlug));
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
state = AsyncValue.error(error, stackTrace);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final sitePagesNotifierProvider = AsyncNotifierProvider.autoDispose.family<
|
||||||
|
SitePagesNotifier,
|
||||||
|
List<SnPublicationPage>,
|
||||||
|
({String pubName, String siteSlug})
|
||||||
|
>(SitePagesNotifier.new);
|
||||||
280
lib/pods/site_pages.g.dart
Normal file
280
lib/pods/site_pages.g.dart
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'site_pages.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
String _$sitePagesHash() => r'5e084e9694ad665e9b238c6a747c6c6e99c5eb03';
|
||||||
|
|
||||||
|
/// Copied from Dart SDK
|
||||||
|
class _SystemHash {
|
||||||
|
_SystemHash._();
|
||||||
|
|
||||||
|
static int combine(int hash, int value) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + value);
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||||
|
return hash ^ (hash >> 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int finish(int hash) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = hash ^ (hash >> 11);
|
||||||
|
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [sitePages].
|
||||||
|
@ProviderFor(sitePages)
|
||||||
|
const sitePagesProvider = SitePagesFamily();
|
||||||
|
|
||||||
|
/// See also [sitePages].
|
||||||
|
class SitePagesFamily extends Family<AsyncValue<List<SnPublicationPage>>> {
|
||||||
|
/// See also [sitePages].
|
||||||
|
const SitePagesFamily();
|
||||||
|
|
||||||
|
/// See also [sitePages].
|
||||||
|
SitePagesProvider call(String pubName, String siteSlug) {
|
||||||
|
return SitePagesProvider(pubName, siteSlug);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
SitePagesProvider getProviderOverride(covariant SitePagesProvider provider) {
|
||||||
|
return call(provider.pubName, provider.siteSlug);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||||
|
_allTransitiveDependencies;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get name => r'sitePagesProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [sitePages].
|
||||||
|
class SitePagesProvider
|
||||||
|
extends AutoDisposeFutureProvider<List<SnPublicationPage>> {
|
||||||
|
/// See also [sitePages].
|
||||||
|
SitePagesProvider(String pubName, String siteSlug)
|
||||||
|
: this._internal(
|
||||||
|
(ref) => sitePages(ref as SitePagesRef, pubName, siteSlug),
|
||||||
|
from: sitePagesProvider,
|
||||||
|
name: r'sitePagesProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$sitePagesHash,
|
||||||
|
dependencies: SitePagesFamily._dependencies,
|
||||||
|
allTransitiveDependencies: SitePagesFamily._allTransitiveDependencies,
|
||||||
|
pubName: pubName,
|
||||||
|
siteSlug: siteSlug,
|
||||||
|
);
|
||||||
|
|
||||||
|
SitePagesProvider._internal(
|
||||||
|
super._createNotifier, {
|
||||||
|
required super.name,
|
||||||
|
required super.dependencies,
|
||||||
|
required super.allTransitiveDependencies,
|
||||||
|
required super.debugGetCreateSourceHash,
|
||||||
|
required super.from,
|
||||||
|
required this.pubName,
|
||||||
|
required this.siteSlug,
|
||||||
|
}) : super.internal();
|
||||||
|
|
||||||
|
final String pubName;
|
||||||
|
final String siteSlug;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Override overrideWith(
|
||||||
|
FutureOr<List<SnPublicationPage>> Function(SitePagesRef provider) create,
|
||||||
|
) {
|
||||||
|
return ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
override: SitePagesProvider._internal(
|
||||||
|
(ref) => create(ref as SitePagesRef),
|
||||||
|
from: from,
|
||||||
|
name: null,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
debugGetCreateSourceHash: null,
|
||||||
|
pubName: pubName,
|
||||||
|
siteSlug: siteSlug,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AutoDisposeFutureProviderElement<List<SnPublicationPage>> createElement() {
|
||||||
|
return _SitePagesProviderElement(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is SitePagesProvider &&
|
||||||
|
other.pubName == pubName &&
|
||||||
|
other.siteSlug == siteSlug;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, pubName.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, siteSlug.hashCode);
|
||||||
|
|
||||||
|
return _SystemHash.finish(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
mixin SitePagesRef on AutoDisposeFutureProviderRef<List<SnPublicationPage>> {
|
||||||
|
/// The parameter `pubName` of this provider.
|
||||||
|
String get pubName;
|
||||||
|
|
||||||
|
/// The parameter `siteSlug` of this provider.
|
||||||
|
String get siteSlug;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SitePagesProviderElement
|
||||||
|
extends AutoDisposeFutureProviderElement<List<SnPublicationPage>>
|
||||||
|
with SitePagesRef {
|
||||||
|
_SitePagesProviderElement(super.provider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pubName => (origin as SitePagesProvider).pubName;
|
||||||
|
@override
|
||||||
|
String get siteSlug => (origin as SitePagesProvider).siteSlug;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$sitePageHash() => r'542f70c5b103fe34d7cf7eb0821d52f017022efc';
|
||||||
|
|
||||||
|
/// See also [sitePage].
|
||||||
|
@ProviderFor(sitePage)
|
||||||
|
const sitePageProvider = SitePageFamily();
|
||||||
|
|
||||||
|
/// See also [sitePage].
|
||||||
|
class SitePageFamily extends Family<AsyncValue<SnPublicationPage>> {
|
||||||
|
/// See also [sitePage].
|
||||||
|
const SitePageFamily();
|
||||||
|
|
||||||
|
/// See also [sitePage].
|
||||||
|
SitePageProvider call(String pageId) {
|
||||||
|
return SitePageProvider(pageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
SitePageProvider getProviderOverride(covariant SitePageProvider provider) {
|
||||||
|
return call(provider.pageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||||
|
_allTransitiveDependencies;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get name => r'sitePageProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [sitePage].
|
||||||
|
class SitePageProvider extends AutoDisposeFutureProvider<SnPublicationPage> {
|
||||||
|
/// See also [sitePage].
|
||||||
|
SitePageProvider(String pageId)
|
||||||
|
: this._internal(
|
||||||
|
(ref) => sitePage(ref as SitePageRef, pageId),
|
||||||
|
from: sitePageProvider,
|
||||||
|
name: r'sitePageProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$sitePageHash,
|
||||||
|
dependencies: SitePageFamily._dependencies,
|
||||||
|
allTransitiveDependencies: SitePageFamily._allTransitiveDependencies,
|
||||||
|
pageId: pageId,
|
||||||
|
);
|
||||||
|
|
||||||
|
SitePageProvider._internal(
|
||||||
|
super._createNotifier, {
|
||||||
|
required super.name,
|
||||||
|
required super.dependencies,
|
||||||
|
required super.allTransitiveDependencies,
|
||||||
|
required super.debugGetCreateSourceHash,
|
||||||
|
required super.from,
|
||||||
|
required this.pageId,
|
||||||
|
}) : super.internal();
|
||||||
|
|
||||||
|
final String pageId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Override overrideWith(
|
||||||
|
FutureOr<SnPublicationPage> Function(SitePageRef provider) create,
|
||||||
|
) {
|
||||||
|
return ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
override: SitePageProvider._internal(
|
||||||
|
(ref) => create(ref as SitePageRef),
|
||||||
|
from: from,
|
||||||
|
name: null,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
debugGetCreateSourceHash: null,
|
||||||
|
pageId: pageId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AutoDisposeFutureProviderElement<SnPublicationPage> createElement() {
|
||||||
|
return _SitePageProviderElement(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is SitePageProvider && other.pageId == pageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, pageId.hashCode);
|
||||||
|
|
||||||
|
return _SystemHash.finish(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
mixin SitePageRef on AutoDisposeFutureProviderRef<SnPublicationPage> {
|
||||||
|
/// The parameter `pageId` of this provider.
|
||||||
|
String get pageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SitePageProviderElement
|
||||||
|
extends AutoDisposeFutureProviderElement<SnPublicationPage>
|
||||||
|
with SitePageRef {
|
||||||
|
_SitePageProviderElement(super.provider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pageId => (origin as SitePageProvider).pageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
88
lib/pods/sites.dart
Normal file
88
lib/pods/sites.dart
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:island/models/publication_site.dart';
|
||||||
|
import 'package:island/pods/network.dart';
|
||||||
|
|
||||||
|
class SiteNotifier
|
||||||
|
extends
|
||||||
|
AutoDisposeFamilyAsyncNotifier<
|
||||||
|
SnPublicationSite,
|
||||||
|
({String pubName, String? siteId})
|
||||||
|
> {
|
||||||
|
@override
|
||||||
|
FutureOr<SnPublicationSite> build(
|
||||||
|
({String pubName, String? siteId}) arg,
|
||||||
|
) async {
|
||||||
|
if (arg.siteId == null || arg.siteId!.isEmpty) {
|
||||||
|
return SnPublicationSite(
|
||||||
|
id: '',
|
||||||
|
slug: '',
|
||||||
|
name: '',
|
||||||
|
publisherId: arg.pubName,
|
||||||
|
accountId: '',
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
pages: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final client = ref.read(apiClientProvider);
|
||||||
|
final response = await client.get('/sphere/sites/${arg.siteId}');
|
||||||
|
return SnPublicationSite.fromJson(response.data);
|
||||||
|
} catch (e) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> saveSite(SnPublicationSite site) async {
|
||||||
|
state = const AsyncValue.loading();
|
||||||
|
try {
|
||||||
|
final client = ref.read(apiClientProvider);
|
||||||
|
final url = '/sphere/sites';
|
||||||
|
|
||||||
|
final response =
|
||||||
|
site.id.isEmpty
|
||||||
|
? await client.post(url, data: site.toJson())
|
||||||
|
: await client.patch('$url/${site.id}', data: site.toJson());
|
||||||
|
|
||||||
|
state = AsyncValue.data(SnPublicationSite.fromJson(response.data));
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
state = AsyncValue.error(error, stackTrace);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteSite() async {
|
||||||
|
final siteId = arg.siteId;
|
||||||
|
if (siteId == null || siteId.isEmpty) return;
|
||||||
|
|
||||||
|
state = const AsyncValue.loading();
|
||||||
|
try {
|
||||||
|
final client = ref.read(apiClientProvider);
|
||||||
|
await client.delete('/sphere/sites/$siteId');
|
||||||
|
state = AsyncValue.data(
|
||||||
|
SnPublicationSite(
|
||||||
|
id: '',
|
||||||
|
slug: '',
|
||||||
|
name: '',
|
||||||
|
publisherId: arg.pubName,
|
||||||
|
accountId: '',
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
pages: [],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
state = AsyncValue.error(error, stackTrace);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final siteNotifierProvider = AsyncNotifierProvider.autoDispose.family<
|
||||||
|
SiteNotifier,
|
||||||
|
SnPublicationSite,
|
||||||
|
({String pubName, String? siteId})
|
||||||
|
>(SiteNotifier.new);
|
||||||
537
lib/pods/upload_tasks.dart
Normal file
537
lib/pods/upload_tasks.dart
Normal file
@@ -0,0 +1,537 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
import 'package:cross_file/cross_file.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:island/models/file.dart';
|
||||||
|
import 'package:island/models/drive_task.dart';
|
||||||
|
import 'package:island/pods/network.dart';
|
||||||
|
import 'package:island/pods/websocket.dart';
|
||||||
|
import 'package:island/services/file_uploader.dart';
|
||||||
|
import 'package:island/talker.dart';
|
||||||
|
|
||||||
|
final uploadTasksProvider =
|
||||||
|
StateNotifierProvider<UploadTasksNotifier, List<DriveTask>>(
|
||||||
|
(ref) => UploadTasksNotifier(ref),
|
||||||
|
);
|
||||||
|
|
||||||
|
class UploadTasksNotifier extends StateNotifier<List<DriveTask>> {
|
||||||
|
final Ref ref;
|
||||||
|
StreamSubscription? _websocketSubscription;
|
||||||
|
final Map<String, Map<String, dynamic>> _pendingUploads = {};
|
||||||
|
|
||||||
|
UploadTasksNotifier(this.ref) : super([]) {
|
||||||
|
_listenToWebSocket();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _listenToWebSocket() {
|
||||||
|
final WebSocketService websocketService = ref.read(websocketProvider);
|
||||||
|
_websocketSubscription = websocketService.dataStream.listen(
|
||||||
|
_handleWebSocketPacket,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleWebSocketPacket(dynamic packet) {
|
||||||
|
if (packet.type.startsWith('task.')) {
|
||||||
|
final data = packet.data;
|
||||||
|
if (data == null) return;
|
||||||
|
|
||||||
|
// Debug logging
|
||||||
|
talker.info(
|
||||||
|
'[UploadTasks] Received WebSocket packet: ${packet.type}, data: $data',
|
||||||
|
);
|
||||||
|
|
||||||
|
final taskId = data['task_id'] as String?;
|
||||||
|
if (taskId == null) return;
|
||||||
|
|
||||||
|
switch (packet.type) {
|
||||||
|
case 'task.created':
|
||||||
|
_handleTaskCreated(taskId, data);
|
||||||
|
break;
|
||||||
|
case 'task.progress':
|
||||||
|
_handleProgressUpdate(taskId, data);
|
||||||
|
break;
|
||||||
|
case 'task.completed':
|
||||||
|
_handleUploadCompleted(taskId, data);
|
||||||
|
break;
|
||||||
|
case 'task.failed':
|
||||||
|
_handleUploadFailed(taskId, data);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleTaskCreated(String taskId, Map<String, dynamic> data) {
|
||||||
|
talker.info('[UploadTasks] Handling task.created for taskId: $taskId');
|
||||||
|
|
||||||
|
// Check if task already exists (might have been created locally)
|
||||||
|
final existingTask =
|
||||||
|
state.where((task) => task.taskId == taskId).firstOrNull;
|
||||||
|
if (existingTask != null) {
|
||||||
|
talker.info('[UploadTasks] Task already exists, updating status');
|
||||||
|
// Task already exists, just update its status to confirm server creation
|
||||||
|
state =
|
||||||
|
state.map((task) {
|
||||||
|
if (task.taskId == taskId) {
|
||||||
|
return task.copyWith(
|
||||||
|
status: DriveTaskStatus.pending,
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return task;
|
||||||
|
}).toList();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we have stored metadata for this task
|
||||||
|
final metadata = _pendingUploads[taskId];
|
||||||
|
talker.info('[UploadTasks] Metadata for taskId $taskId: $metadata');
|
||||||
|
|
||||||
|
if (metadata != null) {
|
||||||
|
talker.info('[UploadTasks] Creating task with full metadata');
|
||||||
|
// Create task with full metadata
|
||||||
|
final uploadTask = DriveTask(
|
||||||
|
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||||
|
taskId: taskId,
|
||||||
|
fileName: metadata['file_name'] as String,
|
||||||
|
contentType: metadata['mime_type'] as String,
|
||||||
|
fileSize: metadata['file_size'] as int,
|
||||||
|
uploadedBytes: 0,
|
||||||
|
totalChunks: metadata['total_chunks'] as int,
|
||||||
|
uploadedChunks: 0,
|
||||||
|
status: DriveTaskStatus.pending,
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
type: 'FileUpload',
|
||||||
|
poolId: metadata['pool_id'] as String?,
|
||||||
|
bundleId: metadata['bundleId'] as String?,
|
||||||
|
encryptPassword: metadata['encrypt_password'] as String?,
|
||||||
|
expiredAt: metadata['expired_at'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
state = [...state, uploadTask];
|
||||||
|
talker.info(
|
||||||
|
'[UploadTasks] Task created successfully. Total tasks: ${state.length}',
|
||||||
|
);
|
||||||
|
// Clean up stored metadata
|
||||||
|
_pendingUploads.remove(taskId);
|
||||||
|
} else {
|
||||||
|
talker.info('[UploadTasks] No metadata found, creating minimal task');
|
||||||
|
// Create minimal task if no metadata is stored
|
||||||
|
final params = data['parameters'];
|
||||||
|
final uploadTask = DriveTask(
|
||||||
|
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||||
|
taskId: taskId,
|
||||||
|
fileName: params['file_name'] as String? ?? 'Unknown file',
|
||||||
|
contentType: params['content_type'],
|
||||||
|
fileSize: params['file_size'],
|
||||||
|
uploadedBytes:
|
||||||
|
(params['chunk_size'] as int) * (params['chunks_uploaded'] as int),
|
||||||
|
totalChunks: params['chunks_count'],
|
||||||
|
uploadedChunks: params['chunks_uploaded'],
|
||||||
|
status: DriveTaskStatus.pending,
|
||||||
|
createdAt: DateTime.tryParse(data['created_at']) ?? DateTime.now(),
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
type: data['type'],
|
||||||
|
);
|
||||||
|
|
||||||
|
state = [...state, uploadTask];
|
||||||
|
talker.info(
|
||||||
|
'[UploadTasks] Minimal task created. Total tasks: ${state.length}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleProgressUpdate(String taskId, Map<String, dynamic> data) {
|
||||||
|
final progress = data['progress'] as num? ?? 0.0;
|
||||||
|
|
||||||
|
state =
|
||||||
|
state.map((task) {
|
||||||
|
if (task.taskId == taskId) {
|
||||||
|
final uploadedBytes = (progress / 100.0 * task.fileSize).toInt();
|
||||||
|
return task.copyWith(
|
||||||
|
statusMessage: data['status'],
|
||||||
|
uploadedBytes: uploadedBytes,
|
||||||
|
status: DriveTaskStatus.inProgress,
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return task;
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleUploadCompleted(String taskId, Map<String, dynamic> data) {
|
||||||
|
final results = data['results'] as Map<String, dynamic>?;
|
||||||
|
|
||||||
|
state =
|
||||||
|
state.map((task) {
|
||||||
|
if (task.taskId == taskId) {
|
||||||
|
return task.copyWith(
|
||||||
|
status: DriveTaskStatus.completed,
|
||||||
|
uploadedChunks: task.totalChunks,
|
||||||
|
uploadedBytes: task.fileSize,
|
||||||
|
// Update file information from Results if available
|
||||||
|
fileName: results?['file_name'] as String? ?? task.fileName,
|
||||||
|
fileSize: results?['file_size'] as int? ?? task.fileSize,
|
||||||
|
contentType: results?['mime_type'] as String? ?? task.contentType,
|
||||||
|
result:
|
||||||
|
results?['file_info'] != null
|
||||||
|
? SnCloudFile.fromJson(results!['file_info'])
|
||||||
|
: null,
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return task;
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleUploadFailed(String taskId, Map<String, dynamic> data) {
|
||||||
|
final errorMessage = data['error_message'] as String? ?? 'Upload failed';
|
||||||
|
|
||||||
|
state =
|
||||||
|
state.map((task) {
|
||||||
|
if (task.taskId == taskId) {
|
||||||
|
return task.copyWith(
|
||||||
|
status: DriveTaskStatus.failed,
|
||||||
|
errorMessage: errorMessage,
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return task;
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
void addUploadTask(DriveTask task) {
|
||||||
|
state = [...state, task];
|
||||||
|
}
|
||||||
|
|
||||||
|
void storeUploadMetadata(
|
||||||
|
String taskId, {
|
||||||
|
required String fileName,
|
||||||
|
required String contentType,
|
||||||
|
required int fileSize,
|
||||||
|
required int totalChunks,
|
||||||
|
String? poolId,
|
||||||
|
String? bundleId,
|
||||||
|
String? encryptPassword,
|
||||||
|
String? expiredAt,
|
||||||
|
}) {
|
||||||
|
_pendingUploads[taskId] = {
|
||||||
|
'file_name': fileName,
|
||||||
|
'mime_type': contentType,
|
||||||
|
'file_size': fileSize,
|
||||||
|
'total_chunks': totalChunks,
|
||||||
|
'pool_id': poolId,
|
||||||
|
'bundleId': bundleId,
|
||||||
|
'encrypt_password': encryptPassword,
|
||||||
|
'expired_at': expiredAt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateTaskStatus(
|
||||||
|
String taskId,
|
||||||
|
DriveTaskStatus status, {
|
||||||
|
String? errorMessage,
|
||||||
|
}) {
|
||||||
|
state =
|
||||||
|
state.map((task) {
|
||||||
|
if (task.taskId == taskId) {
|
||||||
|
return task.copyWith(
|
||||||
|
status: status,
|
||||||
|
errorMessage: errorMessage,
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return task;
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateTransmissionProgress(String taskId, double progress) {
|
||||||
|
state =
|
||||||
|
state.map((task) {
|
||||||
|
if (task.taskId == taskId) {
|
||||||
|
return task.copyWith(
|
||||||
|
transmissionProgress: progress,
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return task;
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateDownloadProgress(
|
||||||
|
String taskId,
|
||||||
|
int downloadedBytes,
|
||||||
|
int totalBytes,
|
||||||
|
) {
|
||||||
|
state =
|
||||||
|
state.map((task) {
|
||||||
|
if (task.taskId == taskId) {
|
||||||
|
return task.copyWith(
|
||||||
|
fileSize: totalBytes,
|
||||||
|
uploadedBytes: downloadedBytes,
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return task;
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeTask(String taskId) {
|
||||||
|
state = state.where((task) => task.taskId != taskId).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearCompletedTasks() {
|
||||||
|
state =
|
||||||
|
state
|
||||||
|
.where(
|
||||||
|
(task) =>
|
||||||
|
task.status != DriveTaskStatus.completed &&
|
||||||
|
task.status != DriveTaskStatus.failed &&
|
||||||
|
task.status != DriveTaskStatus.cancelled &&
|
||||||
|
task.status != DriveTaskStatus.expired,
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearAllTasks() {
|
||||||
|
state = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
DriveTask? getTask(String taskId) {
|
||||||
|
return state.where((task) => task.taskId == taskId).firstOrNull;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<DriveTask> getActiveTasks() {
|
||||||
|
return state
|
||||||
|
.where(
|
||||||
|
(task) =>
|
||||||
|
task.status == DriveTaskStatus.pending ||
|
||||||
|
task.status == DriveTaskStatus.inProgress ||
|
||||||
|
task.status == DriveTaskStatus.paused ||
|
||||||
|
task.status == DriveTaskStatus.completed,
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
String addLocalDownloadTask(SnCloudFile item) {
|
||||||
|
final taskId =
|
||||||
|
'download-${item.id}-${DateTime.now().millisecondsSinceEpoch}';
|
||||||
|
final task = DriveTask(
|
||||||
|
id: taskId,
|
||||||
|
taskId: taskId,
|
||||||
|
fileName: item.name,
|
||||||
|
contentType: item.mimeType ?? '',
|
||||||
|
fileSize: 0,
|
||||||
|
uploadedBytes: 0,
|
||||||
|
totalChunks: 1,
|
||||||
|
uploadedChunks: 0,
|
||||||
|
status: DriveTaskStatus.inProgress,
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
type: 'FileDownload',
|
||||||
|
);
|
||||||
|
state = [...state, task];
|
||||||
|
return taskId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_websocketSubscription?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provider for the enhanced FileUploader that integrates with upload tasks
|
||||||
|
final enhancedFileUploaderProvider = Provider<EnhancedFileUploader>((ref) {
|
||||||
|
final dio = ref.watch(apiClientProvider);
|
||||||
|
return EnhancedFileUploader(dio, ref);
|
||||||
|
});
|
||||||
|
|
||||||
|
class EnhancedFileUploader extends FileUploader {
|
||||||
|
final Ref ref;
|
||||||
|
|
||||||
|
EnhancedFileUploader(super.client, this.ref);
|
||||||
|
|
||||||
|
/// Reads the next chunk from a stream subscription.
|
||||||
|
Future<Uint8List> _readNextChunkFromStream(
|
||||||
|
StreamSubscription<List<int>> subscription,
|
||||||
|
int size,
|
||||||
|
) async {
|
||||||
|
final completer = Completer<Uint8List>();
|
||||||
|
final buffer = <int>[];
|
||||||
|
int remaining = size;
|
||||||
|
|
||||||
|
void onData(List<int> data) {
|
||||||
|
buffer.addAll(data);
|
||||||
|
remaining -= data.length;
|
||||||
|
if (remaining <= 0) {
|
||||||
|
subscription.pause();
|
||||||
|
completer.complete(Uint8List.fromList(buffer.sublist(0, size)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onDone() {
|
||||||
|
if (!completer.isCompleted) {
|
||||||
|
completer.complete(Uint8List.fromList(buffer));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
subscription.onData(onData);
|
||||||
|
subscription.onDone(onDone);
|
||||||
|
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<SnCloudFile> uploadFile({
|
||||||
|
required dynamic fileData,
|
||||||
|
required String fileName,
|
||||||
|
required String contentType,
|
||||||
|
String? poolId,
|
||||||
|
String? bundleId,
|
||||||
|
String? encryptPassword,
|
||||||
|
String? expiredAt,
|
||||||
|
int? customChunkSize,
|
||||||
|
String? path,
|
||||||
|
Function(double? progress, Duration estimate)? onProgress,
|
||||||
|
}) async {
|
||||||
|
// Step 1: Create upload task
|
||||||
|
onProgress?.call(null, Duration.zero);
|
||||||
|
final createResponse = await createUploadTask(
|
||||||
|
fileData: fileData,
|
||||||
|
fileName: fileName,
|
||||||
|
contentType: contentType,
|
||||||
|
poolId: poolId,
|
||||||
|
bundleId: bundleId,
|
||||||
|
encryptPassword: encryptPassword,
|
||||||
|
expiredAt: expiredAt,
|
||||||
|
chunkSize: customChunkSize,
|
||||||
|
path: path,
|
||||||
|
);
|
||||||
|
|
||||||
|
int totalSize;
|
||||||
|
if (fileData is XFile) {
|
||||||
|
totalSize = await fileData.length();
|
||||||
|
} else if (fileData is Uint8List) {
|
||||||
|
totalSize = fileData.length;
|
||||||
|
} else {
|
||||||
|
throw ArgumentError('Invalid fileData type');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (createResponse['file_exists'] == true) {
|
||||||
|
// File already exists, create a local task to show it was found
|
||||||
|
final existingFile = SnCloudFile.fromJson(createResponse['file']);
|
||||||
|
|
||||||
|
// Create a task that shows as completed immediately
|
||||||
|
// Use a generated taskId since the server might not provide one for existing files
|
||||||
|
final taskId =
|
||||||
|
createResponse['task_id'] as String? ??
|
||||||
|
'existing-${DateTime.now().millisecondsSinceEpoch}';
|
||||||
|
|
||||||
|
final uploadTask = DriveTask(
|
||||||
|
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||||
|
taskId: taskId,
|
||||||
|
fileName: fileName,
|
||||||
|
contentType: contentType,
|
||||||
|
fileSize: totalSize,
|
||||||
|
uploadedBytes: totalSize,
|
||||||
|
totalChunks: 1, // For existing files, we consider it as 1 chunk
|
||||||
|
uploadedChunks: 1,
|
||||||
|
status: DriveTaskStatus.completed,
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
type: 'FileUpload',
|
||||||
|
poolId: poolId,
|
||||||
|
bundleId: bundleId,
|
||||||
|
encryptPassword: encryptPassword,
|
||||||
|
expiredAt: expiredAt,
|
||||||
|
);
|
||||||
|
|
||||||
|
ref.read(uploadTasksProvider.notifier).addUploadTask(uploadTask);
|
||||||
|
|
||||||
|
return existingFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
final taskId = createResponse['task_id'] as String;
|
||||||
|
final chunkSize = createResponse['chunk_size'] as int;
|
||||||
|
final chunksCount = createResponse['chunks_count'] as int;
|
||||||
|
|
||||||
|
// Store upload metadata for when task.created event arrives
|
||||||
|
talker.info('[UploadTasks] Storing metadata for taskId: $taskId');
|
||||||
|
ref
|
||||||
|
.read(uploadTasksProvider.notifier)
|
||||||
|
.storeUploadMetadata(
|
||||||
|
taskId,
|
||||||
|
fileName: fileName,
|
||||||
|
contentType: contentType,
|
||||||
|
fileSize: totalSize,
|
||||||
|
totalChunks: chunksCount,
|
||||||
|
poolId: poolId,
|
||||||
|
bundleId: bundleId,
|
||||||
|
encryptPassword: encryptPassword,
|
||||||
|
expiredAt: expiredAt,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Step 2: Upload chunks
|
||||||
|
int bytesUploaded = 0;
|
||||||
|
if (fileData is XFile) {
|
||||||
|
// Use stream for XFile
|
||||||
|
final subscription = fileData.openRead().listen(null);
|
||||||
|
subscription.pause();
|
||||||
|
for (int i = 0; i < chunksCount; i++) {
|
||||||
|
subscription.resume();
|
||||||
|
final chunkData = await _readNextChunkFromStream(
|
||||||
|
subscription,
|
||||||
|
chunkSize,
|
||||||
|
);
|
||||||
|
await uploadChunk(
|
||||||
|
taskId: taskId,
|
||||||
|
chunkIndex: i,
|
||||||
|
chunkData: chunkData,
|
||||||
|
onSendProgress: (sent, total) {
|
||||||
|
final overallProgress = (bytesUploaded + sent) / totalSize;
|
||||||
|
onProgress?.call(overallProgress, Duration.zero);
|
||||||
|
// Update transmission progress in UI
|
||||||
|
ref
|
||||||
|
.read(uploadTasksProvider.notifier)
|
||||||
|
.updateTransmissionProgress(taskId, overallProgress);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
bytesUploaded += chunkData.length;
|
||||||
|
}
|
||||||
|
subscription.cancel();
|
||||||
|
} else if (fileData is Uint8List) {
|
||||||
|
// Use old way for Uint8List
|
||||||
|
final chunks = <Uint8List>[];
|
||||||
|
for (int i = 0; i < fileData.length; i += chunkSize) {
|
||||||
|
final end =
|
||||||
|
i + chunkSize > fileData.length ? fileData.length : i + chunkSize;
|
||||||
|
chunks.add(Uint8List.fromList(fileData.sublist(i, end)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload each chunk
|
||||||
|
for (int i = 0; i < chunks.length; i++) {
|
||||||
|
await uploadChunk(
|
||||||
|
taskId: taskId,
|
||||||
|
chunkIndex: i,
|
||||||
|
chunkData: chunks[i],
|
||||||
|
onSendProgress: (sent, total) {
|
||||||
|
final overallProgress = (bytesUploaded + sent) / totalSize;
|
||||||
|
onProgress?.call(overallProgress, Duration.zero);
|
||||||
|
// Update transmission progress in UI
|
||||||
|
ref
|
||||||
|
.read(uploadTasksProvider.notifier)
|
||||||
|
.updateTransmissionProgress(taskId, overallProgress);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
bytesUploaded += chunks[i].length;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw ArgumentError('Invalid fileData type');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Complete upload
|
||||||
|
onProgress?.call(null, Duration.zero);
|
||||||
|
return await completeUpload(taskId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,8 @@ import 'package:dio/dio.dart';
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:firebase_analytics/firebase_analytics.dart';
|
import 'package:firebase_analytics/firebase_analytics.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_platform_alert/flutter_platform_alert.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:island/widgets/alert.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/account.dart';
|
import 'package:island/models/account.dart';
|
||||||
@@ -36,41 +37,65 @@ class UserInfoNotifier extends StateNotifier<AsyncValue<SnAccount?>> {
|
|||||||
} catch (error, stackTrace) {
|
} catch (error, stackTrace) {
|
||||||
if (!kIsWeb) {
|
if (!kIsWeb) {
|
||||||
if (error is DioException) {
|
if (error is DioException) {
|
||||||
FlutterPlatformAlert.showCustomAlert(
|
showOverlayDialog<bool>(
|
||||||
windowTitle: 'failedToLoadUserInfo'.tr(),
|
builder:
|
||||||
text: [
|
(context, close) => AlertDialog(
|
||||||
(error.response?.statusCode == 401
|
title: Text('failedToLoadUserInfo'.tr()),
|
||||||
? 'failedToLoadUserInfoUnauthorized'
|
content: Text(
|
||||||
: 'failedToLoadUserInfoNetwork')
|
[
|
||||||
.tr()
|
(error.response?.statusCode == 401
|
||||||
.trim(),
|
? 'failedToLoadUserInfoUnauthorized'
|
||||||
'',
|
: 'failedToLoadUserInfoNetwork')
|
||||||
'${error.response?.statusCode ?? 'Network Error'}',
|
.tr()
|
||||||
if (error.response?.headers != null) error.response?.headers,
|
.trim(),
|
||||||
if (error.response?.data != null)
|
'',
|
||||||
jsonEncode(error.response?.data),
|
'${error.response?.statusCode ?? 'Network Error'}',
|
||||||
].join('\n'),
|
if (error.response?.headers != null)
|
||||||
iconStyle: IconStyle.error,
|
error.response?.headers,
|
||||||
neutralButtonTitle: 'retry'.tr(),
|
if (error.response?.data != null)
|
||||||
negativeButtonTitle: 'okay'.tr(),
|
jsonEncode(error.response?.data),
|
||||||
|
].join('\n'),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => close(false),
|
||||||
|
child: Text('okay'.tr()),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => close(true),
|
||||||
|
child: Text('retry'.tr()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
).then((value) {
|
).then((value) {
|
||||||
if (value == CustomButton.neutralButton) {
|
if (value == true) {
|
||||||
fetchUser();
|
fetchUser();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
FlutterPlatformAlert.showCustomAlert(
|
showOverlayDialog<bool>(
|
||||||
windowTitle: 'failedToLoadUserInfo'.tr(),
|
builder:
|
||||||
text:
|
(context, close) => AlertDialog(
|
||||||
[
|
title: Text('failedToLoadUserInfo'.tr()),
|
||||||
'failedToLoadUserInfoNetwork'.tr(),
|
content: Text(
|
||||||
error.toString(),
|
[
|
||||||
].join('\n\n').trim(),
|
'failedToLoadUserInfoNetwork'.tr(),
|
||||||
iconStyle: IconStyle.error,
|
error.toString(),
|
||||||
neutralButtonTitle: 'retry'.tr(),
|
].join('\n\n').trim(),
|
||||||
negativeButtonTitle: 'okay'.tr(),
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => close(false),
|
||||||
|
child: Text('okay'.tr()),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => close(true),
|
||||||
|
child: Text('retry'.tr()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
).then((value) {
|
).then((value) {
|
||||||
if (value == CustomButton.neutralButton) {
|
if (value == true) {
|
||||||
fetchUser();
|
fetchUser();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
119
lib/route.dart
119
lib/route.dart
@@ -12,7 +12,9 @@ import 'package:island/screens/developers/hub.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/discovery/articles.dart';
|
import 'package:island/screens/discovery/articles.dart';
|
||||||
|
import 'package:island/models/file.dart';
|
||||||
import 'package:island/screens/files/file_list.dart';
|
import 'package:island/screens/files/file_list.dart';
|
||||||
|
import 'package:island/screens/files/file_detail.dart';
|
||||||
import 'package:island/screens/posts/post_categories_list.dart';
|
import 'package:island/screens/posts/post_categories_list.dart';
|
||||||
import 'package:island/screens/posts/post_category_detail.dart';
|
import 'package:island/screens/posts/post_category_detail.dart';
|
||||||
import 'package:island/screens/posts/post_search.dart';
|
import 'package:island/screens/posts/post_search.dart';
|
||||||
@@ -28,10 +30,8 @@ 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/search_messages.dart';
|
import 'package:island/screens/chat/search_messages.dart';
|
||||||
import 'package:island/screens/thought/think.dart';
|
import 'package:island/screens/thought/think.dart';
|
||||||
import 'package:island/screens/creators/hub.dart';
|
import 'package:island/screens/creators/hub.dart';
|
||||||
@@ -42,9 +42,9 @@ 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_form.dart';
|
import 'package:island/screens/creators/sites/site_detail.dart';
|
||||||
|
import 'package:island/screens/creators/sites/site_list.dart';
|
||||||
import 'package:island/screens/creators/webfeed/webfeed_list.dart';
|
import 'package:island/screens/creators/webfeed/webfeed_list.dart';
|
||||||
import 'package:island/screens/poll/poll_editor.dart';
|
|
||||||
import 'package:island/screens/posts/compose.dart';
|
import 'package:island/screens/posts/compose.dart';
|
||||||
import 'package:island/screens/posts/compose_article.dart';
|
import 'package:island/screens/posts/compose_article.dart';
|
||||||
import 'package:island/screens/posts/post_detail.dart';
|
import 'package:island/screens/posts/post_detail.dart';
|
||||||
@@ -118,19 +118,6 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
return ArticleEditScreen(id: id);
|
return ArticleEditScreen(id: id);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
GoRoute(
|
|
||||||
name: 'chatCall',
|
|
||||||
path: '/chat/:id/call',
|
|
||||||
builder: (context, state) {
|
|
||||||
final id = state.pathParameters['id']!;
|
|
||||||
return CallScreen(roomId: id);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
name: 'thought',
|
|
||||||
path: '/thought',
|
|
||||||
builder: (context, state) => const ThoughtScreen(),
|
|
||||||
),
|
|
||||||
GoRoute(
|
GoRoute(
|
||||||
name: 'logs',
|
name: 'logs',
|
||||||
path: '/logs',
|
path: '/logs',
|
||||||
@@ -176,6 +163,22 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
builder: (context, state) => const AboutScreen(),
|
builder: (context, state) => const AboutScreen(),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
GoRoute(
|
||||||
|
name: 'fileDetail',
|
||||||
|
path: '/files/:id',
|
||||||
|
builder: (context, state) {
|
||||||
|
// For now, we'll need to pass the file object through extra
|
||||||
|
// This will be updated when we modify the file list navigation
|
||||||
|
final file = state.extra as SnCloudFile?;
|
||||||
|
if (file != null) {
|
||||||
|
return FileDetailScreen(item: file);
|
||||||
|
}
|
||||||
|
// Fallback - this shouldn't happen in normal flow
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
// Main tabs with TabsScreen shell
|
// Main tabs with TabsScreen shell
|
||||||
ShellRoute(
|
ShellRoute(
|
||||||
navigatorKey: _tabsShellKey,
|
navigatorKey: _tabsShellKey,
|
||||||
@@ -269,11 +272,6 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
path: '/chat',
|
path: '/chat',
|
||||||
builder: (context, state) => const ChatListScreen(),
|
builder: (context, state) => const ChatListScreen(),
|
||||||
),
|
),
|
||||||
GoRoute(
|
|
||||||
name: 'chatNew',
|
|
||||||
path: '/chat/new',
|
|
||||||
builder: (context, state) => const NewChatScreen(),
|
|
||||||
),
|
|
||||||
GoRoute(
|
GoRoute(
|
||||||
name: 'chatRoom',
|
name: 'chatRoom',
|
||||||
path: '/chat/:id',
|
path: '/chat/:id',
|
||||||
@@ -282,14 +280,6 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
return ChatRoomScreen(id: id);
|
return ChatRoomScreen(id: id);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
GoRoute(
|
|
||||||
name: 'chatEdit',
|
|
||||||
path: '/chat/:id/edit',
|
|
||||||
builder: (context, state) {
|
|
||||||
final id = state.pathParameters['id']!;
|
|
||||||
return EditChatScreen(id: id);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
GoRoute(
|
GoRoute(
|
||||||
name: 'chatDetail',
|
name: 'chatDetail',
|
||||||
path: '/chat/:id/detail',
|
path: '/chat/:id/detail',
|
||||||
@@ -396,11 +386,6 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
path: '/account/wallet',
|
path: '/account/wallet',
|
||||||
builder: (context, state) => const WalletScreen(),
|
builder: (context, state) => const WalletScreen(),
|
||||||
),
|
),
|
||||||
GoRoute(
|
|
||||||
name: 'files',
|
|
||||||
path: '/account/files',
|
|
||||||
builder: (context, state) => const FileListScreen(),
|
|
||||||
),
|
|
||||||
GoRoute(
|
GoRoute(
|
||||||
name: 'relationships',
|
name: 'relationships',
|
||||||
path: '/account/relationships',
|
path: '/account/relationships',
|
||||||
@@ -445,6 +430,21 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
return AccountProfileScreen(name: name);
|
return AccountProfileScreen(name: name);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// Files tab
|
||||||
|
GoRoute(
|
||||||
|
name: 'files',
|
||||||
|
path: '/files',
|
||||||
|
builder: (context, state) => const FileListScreen(),
|
||||||
|
),
|
||||||
|
|
||||||
|
// SN-chan tab
|
||||||
|
GoRoute(
|
||||||
|
name: 'thought',
|
||||||
|
path: '/thought',
|
||||||
|
builder: (context, state) => const ThoughtScreen(),
|
||||||
|
),
|
||||||
|
|
||||||
// Creator hub tab
|
// Creator hub tab
|
||||||
GoRoute(
|
GoRoute(
|
||||||
name: 'creatorHub',
|
name: 'creatorHub',
|
||||||
@@ -477,28 +477,30 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
return CreatorPollListScreen(pubName: name);
|
return CreatorPollListScreen(pubName: name);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
// Poll routes
|
// Site list route
|
||||||
GoRoute(
|
GoRoute(
|
||||||
name: 'creatorPollNew',
|
name: 'creatorSites',
|
||||||
path: ':name/polls/new',
|
path: ':name/sites',
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final name = state.pathParameters['name']!;
|
final name = state.pathParameters['name']!;
|
||||||
// initialPollId left null for create; initialPublisher prefilled
|
return CreatorSiteListScreen(pubName: name);
|
||||||
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,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
name: 'creatorSiteDetail',
|
||||||
|
path: ':siteSlug',
|
||||||
|
builder: (context, state) {
|
||||||
|
final name = state.pathParameters['name']!;
|
||||||
|
final siteSlug = state.pathParameters['siteSlug']!;
|
||||||
|
return PublicationSiteDetailScreen(
|
||||||
|
siteSlug: siteSlug,
|
||||||
|
pubName: name,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
GoRoute(
|
GoRoute(
|
||||||
name: 'creatorStickers',
|
name: 'creatorStickers',
|
||||||
path: ':name/stickers',
|
path: ':name/stickers',
|
||||||
@@ -507,19 +509,6 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
return StickersScreen(pubName: 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);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|||||||
@@ -384,9 +384,7 @@ class _AboutScreenState extends ConsumerState<AboutScreen> {
|
|||||||
icon: const Icon(Symbols.content_copy, size: 16),
|
icon: const Icon(Symbols.content_copy, size: 16),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Clipboard.setData(ClipboardData(text: value));
|
Clipboard.setData(ClipboardData(text: value));
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
showSnackBar('copiedToClipboard'.tr());
|
||||||
SnackBar(content: Text('copiedToClipboard'.tr())),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
constraints: const BoxConstraints(),
|
constraints: const BoxConstraints(),
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ class AccountScreen extends HookConsumerWidget {
|
|||||||
pathParameters: {'name': user.value!.name},
|
pathParameters: {'name': user.value!.name},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
).padding(bottom: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -375,6 +375,17 @@ class AccountScreen extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
if (!isWideScreen(context))
|
||||||
|
ListTile(
|
||||||
|
minTileHeight: 48,
|
||||||
|
leading: const Icon(Symbols.files),
|
||||||
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
|
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
title: Text('files').tr(),
|
||||||
|
onTap: () {
|
||||||
|
context.goNamed('files');
|
||||||
|
},
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
minTileHeight: 48,
|
minTileHeight: 48,
|
||||||
leading: const Icon(Symbols.wallet),
|
leading: const Icon(Symbols.wallet),
|
||||||
@@ -385,16 +396,6 @@ class AccountScreen extends HookConsumerWidget {
|
|||||||
context.pushNamed('wallet');
|
context.pushNamed('wallet');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
|
||||||
minTileHeight: 48,
|
|
||||||
leading: const Icon(Symbols.files),
|
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
|
||||||
title: Text('files').tr(),
|
|
||||||
onTap: () {
|
|
||||||
context.pushNamed('files');
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ListTile(
|
ListTile(
|
||||||
minTileHeight: 48,
|
minTileHeight: 48,
|
||||||
leading: const Icon(Symbols.people),
|
leading: const Icon(Symbols.people),
|
||||||
|
|||||||
@@ -84,9 +84,7 @@ class AccountSettingsScreen extends HookConsumerWidget {
|
|||||||
'accountPasswordChange'.tr(),
|
'accountPasswordChange'.tr(),
|
||||||
);
|
);
|
||||||
if (!confirm || !context.mounted) return;
|
if (!confirm || !context.mounted) return;
|
||||||
final captchaTk = await Navigator.of(
|
final captchaTk = await CaptchaScreen.show(context);
|
||||||
context,
|
|
||||||
).push(MaterialPageRoute(builder: (context) => CaptchaScreen()));
|
|
||||||
if (captchaTk == null) return;
|
if (captchaTk == null) return;
|
||||||
try {
|
try {
|
||||||
if (context.mounted) showLoadingModal(context);
|
if (context.mounted) showLoadingModal(context);
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ class UpdateProfileScreen extends HookConsumerWidget {
|
|||||||
try {
|
try {
|
||||||
final cloudFile =
|
final cloudFile =
|
||||||
await FileUploader.createCloudFile(
|
await FileUploader.createCloudFile(
|
||||||
client: ref.read(apiClientProvider),
|
ref: ref,
|
||||||
fileData: UniversalFile(
|
fileData: UniversalFile(
|
||||||
data: result,
|
data: result,
|
||||||
type: UniversalFileType.image,
|
type: UniversalFileType.image,
|
||||||
|
|||||||
@@ -2,9 +2,19 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:island/screens/auth/captcha.config.dart';
|
import 'package:island/screens/auth/captcha.config.dart';
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
import 'package:island/widgets/content/sheet.dart';
|
||||||
|
|
||||||
class CaptchaScreen extends ConsumerWidget {
|
class CaptchaScreen extends ConsumerWidget {
|
||||||
|
static Future<String?> show(BuildContext context) {
|
||||||
|
return Navigator.push<String>(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => const CaptchaScreen(),
|
||||||
|
fullscreenDialog: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const CaptchaScreen({super.key});
|
const CaptchaScreen({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -13,9 +23,9 @@ class CaptchaScreen extends ConsumerWidget {
|
|||||||
|
|
||||||
if (!captchaUrl.hasValue) return Center(child: CircularProgressIndicator());
|
if (!captchaUrl.hasValue) return Center(child: CircularProgressIndicator());
|
||||||
|
|
||||||
return AppScaffold(
|
return SheetScaffold(
|
||||||
appBar: AppBar(title: Text("Anti-Robot")),
|
titleText: "Anti-Robot",
|
||||||
body: InAppWebView(
|
child: InAppWebView(
|
||||||
initialUrlRequest: URLRequest(
|
initialUrlRequest: URLRequest(
|
||||||
url: WebUri('${captchaUrl.value}?redirect_uri=solian://captcha'),
|
url: WebUri('${captchaUrl.value}?redirect_uri=solian://captcha'),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,14 +1,22 @@
|
|||||||
// ignore_for_file: invalid_runtime_check_with_js_interop_types
|
|
||||||
|
|
||||||
import 'dart:ui_web' as ui;
|
import 'dart:ui_web' as ui;
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/pods/config.dart';
|
import 'package:island/pods/config.dart';
|
||||||
import 'package:island/screens/auth/captcha.config.dart';
|
import 'package:island/screens/auth/captcha.config.dart';
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
import 'package:island/widgets/content/sheet.dart';
|
||||||
import 'package:web/web.dart' as web;
|
import 'package:web/web.dart' as web;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class CaptchaScreen extends ConsumerStatefulWidget {
|
class CaptchaScreen extends ConsumerStatefulWidget {
|
||||||
|
static Future<String?> show(BuildContext context) {
|
||||||
|
return Navigator.push<String>(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => const CaptchaScreen(),
|
||||||
|
fullscreenDialog: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const CaptchaScreen({super.key});
|
const CaptchaScreen({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -20,7 +28,9 @@ class _CaptchaScreenState extends ConsumerState<CaptchaScreen> {
|
|||||||
|
|
||||||
void _setupWebListener(String serverUrl) async {
|
void _setupWebListener(String serverUrl) async {
|
||||||
web.window.onMessage.listen((event) {
|
web.window.onMessage.listen((event) {
|
||||||
|
// ignore: invalid_runtime_check_with_js_interop_types
|
||||||
if (event.data != null && event.data is String) {
|
if (event.data != null && event.data is String) {
|
||||||
|
// ignore: invalid_runtime_check_with_js_interop_types
|
||||||
final message = event.data as String;
|
final message = event.data as String;
|
||||||
if (message.startsWith("captcha_tk=")) {
|
if (message.startsWith("captcha_tk=")) {
|
||||||
String token = message.replaceFirst("captcha_tk=", "");
|
String token = message.replaceFirst("captcha_tk=", "");
|
||||||
@@ -61,9 +71,9 @@ class _CaptchaScreenState extends ConsumerState<CaptchaScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AppScaffold(
|
return SheetScaffold(
|
||||||
appBar: AppBar(title: Text("Anti-Robot")),
|
titleText: "Anti-Robot",
|
||||||
body:
|
child:
|
||||||
_isInitialized
|
_isInitialized
|
||||||
? HtmlElementView(viewType: 'captcha-iframe')
|
? HtmlElementView(viewType: 'captcha-iframe')
|
||||||
: Center(child: CircularProgressIndicator()),
|
: Center(child: CircularProgressIndicator()),
|
||||||
|
|||||||
@@ -1,317 +1,22 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:email_validator/email_validator.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:gap/gap.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/pods/network.dart';
|
|
||||||
import 'package:island/screens/account/me/profile_update.dart';
|
|
||||||
import 'package:island/widgets/alert.dart';
|
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
|
||||||
|
|
||||||
import 'captcha.dart';
|
import 'create_account_content.dart';
|
||||||
|
|
||||||
class CreateAccountScreen extends HookConsumerWidget {
|
class CreateAccountScreen extends HookConsumerWidget {
|
||||||
const CreateAccountScreen({super.key});
|
const CreateAccountScreen({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final formKey = useMemoized(GlobalKey<FormState>.new, const []);
|
|
||||||
|
|
||||||
final emailController = useTextEditingController();
|
|
||||||
final usernameController = useTextEditingController();
|
|
||||||
final nicknameController = useTextEditingController();
|
|
||||||
final passwordController = useTextEditingController();
|
|
||||||
|
|
||||||
void showPostCreateModal() {
|
|
||||||
showModalBottomSheet(
|
|
||||||
isScrollControlled: true,
|
|
||||||
context: context,
|
|
||||||
builder: (context) => _PostCreateModal(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void performAction() async {
|
|
||||||
if (!formKey.currentState!.validate()) return;
|
|
||||||
|
|
||||||
final captchaTk = await Navigator.of(
|
|
||||||
context,
|
|
||||||
).push(MaterialPageRoute(builder: (context) => CaptchaScreen()));
|
|
||||||
if (captchaTk == null) return;
|
|
||||||
|
|
||||||
if (!context.mounted) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
showLoadingModal(context);
|
|
||||||
final client = ref.watch(apiClientProvider);
|
|
||||||
await client.post(
|
|
||||||
'/pass/accounts',
|
|
||||||
data: {
|
|
||||||
'name': usernameController.text,
|
|
||||||
'nick': nicknameController.text,
|
|
||||||
'email': emailController.text,
|
|
||||||
'password': passwordController.text,
|
|
||||||
'language':
|
|
||||||
kServerSupportedLanguages[EasyLocalization.of(
|
|
||||||
context,
|
|
||||||
)!.currentLocale.toString()] ??
|
|
||||||
'en-us',
|
|
||||||
'captcha_token': captchaTk,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (!context.mounted) return;
|
|
||||||
hideLoadingModal(context);
|
|
||||||
showPostCreateModal();
|
|
||||||
} catch (err) {
|
|
||||||
if (context.mounted) hideLoadingModal(context);
|
|
||||||
showErrorAlert(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
isNoBackground: false,
|
isNoBackground: false,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: const PageBackButton(),
|
leading: const PageBackButton(),
|
||||||
title: Text('createAccount').tr(),
|
title: Text('createAccount').tr(),
|
||||||
),
|
),
|
||||||
body:
|
body: CreateAccountContent(),
|
||||||
StyledWidget(
|
|
||||||
Container(
|
|
||||||
constraints: const BoxConstraints(maxWidth: 380),
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: CircleAvatar(
|
|
||||||
radius: 26,
|
|
||||||
child: const Icon(Symbols.person_add, size: 28),
|
|
||||||
).padding(bottom: 8),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'createAccount',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 28,
|
|
||||||
fontWeight: FontWeight.w900,
|
|
||||||
),
|
|
||||||
).tr().padding(left: 4, bottom: 16),
|
|
||||||
Form(
|
|
||||||
key: formKey,
|
|
||||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
TextFormField(
|
|
||||||
controller: usernameController,
|
|
||||||
validator: (value) {
|
|
||||||
if (value == null || value.isEmpty) {
|
|
||||||
return 'fieldCannotBeEmpty'.tr();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
autocorrect: false,
|
|
||||||
enableSuggestions: false,
|
|
||||||
autofillHints: const [AutofillHints.username],
|
|
||||||
decoration: InputDecoration(
|
|
||||||
isDense: true,
|
|
||||||
border: const UnderlineInputBorder(),
|
|
||||||
labelText: 'username'.tr(),
|
|
||||||
helperText: 'usernameCannotChangeHint'.tr(),
|
|
||||||
),
|
|
||||||
onTapOutside:
|
|
||||||
(_) =>
|
|
||||||
FocusManager.instance.primaryFocus
|
|
||||||
?.unfocus(),
|
|
||||||
),
|
|
||||||
const Gap(12),
|
|
||||||
TextFormField(
|
|
||||||
controller: nicknameController,
|
|
||||||
validator: (value) {
|
|
||||||
if (value == null || value.isEmpty) {
|
|
||||||
return 'fieldCannotBeEmpty'.tr();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
autocorrect: false,
|
|
||||||
autofillHints: const [AutofillHints.nickname],
|
|
||||||
decoration: InputDecoration(
|
|
||||||
isDense: true,
|
|
||||||
border: const UnderlineInputBorder(),
|
|
||||||
labelText: 'nickname'.tr(),
|
|
||||||
),
|
|
||||||
onTapOutside:
|
|
||||||
(_) =>
|
|
||||||
FocusManager.instance.primaryFocus
|
|
||||||
?.unfocus(),
|
|
||||||
),
|
|
||||||
const Gap(12),
|
|
||||||
TextFormField(
|
|
||||||
controller: emailController,
|
|
||||||
validator: (value) {
|
|
||||||
if (value == null || value.isEmpty) {
|
|
||||||
return 'fieldCannotBeEmpty'.tr();
|
|
||||||
}
|
|
||||||
if (!EmailValidator.validate(value)) {
|
|
||||||
return 'fieldEmailAddressMustBeValid'.tr();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
autocorrect: false,
|
|
||||||
enableSuggestions: false,
|
|
||||||
autofillHints: const [AutofillHints.email],
|
|
||||||
decoration: InputDecoration(
|
|
||||||
isDense: true,
|
|
||||||
border: const UnderlineInputBorder(),
|
|
||||||
labelText: 'email'.tr(),
|
|
||||||
),
|
|
||||||
onTapOutside:
|
|
||||||
(_) =>
|
|
||||||
FocusManager.instance.primaryFocus
|
|
||||||
?.unfocus(),
|
|
||||||
),
|
|
||||||
const Gap(12),
|
|
||||||
TextFormField(
|
|
||||||
controller: passwordController,
|
|
||||||
validator: (value) {
|
|
||||||
if (value == null || value.isEmpty) {
|
|
||||||
return 'fieldCannotBeEmpty'.tr();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
obscureText: true,
|
|
||||||
autocorrect: false,
|
|
||||||
enableSuggestions: false,
|
|
||||||
autofillHints: const [AutofillHints.password],
|
|
||||||
decoration: InputDecoration(
|
|
||||||
isDense: true,
|
|
||||||
border: const UnderlineInputBorder(),
|
|
||||||
labelText: 'password'.tr(),
|
|
||||||
),
|
|
||||||
onTapOutside:
|
|
||||||
(_) =>
|
|
||||||
FocusManager.instance.primaryFocus
|
|
||||||
?.unfocus(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).padding(horizontal: 7),
|
|
||||||
),
|
|
||||||
const Gap(16),
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.centerRight,
|
|
||||||
child: StyledWidget(
|
|
||||||
Container(
|
|
||||||
constraints: const BoxConstraints(maxWidth: 290),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'termAcceptNextWithAgree'.tr(),
|
|
||||||
textAlign: TextAlign.end,
|
|
||||||
style: Theme.of(
|
|
||||||
context,
|
|
||||||
).textTheme.bodySmall!.copyWith(
|
|
||||||
color: Theme.of(context).colorScheme.onSurface
|
|
||||||
.withAlpha((255 * 0.75).round()),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Material(
|
|
||||||
color: Colors.transparent,
|
|
||||||
child: InkWell(
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text('termAcceptLink').tr(),
|
|
||||||
const Gap(4),
|
|
||||||
const Icon(Symbols.launch, size: 14),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
launchUrlString(
|
|
||||||
'https://solsynth.dev/terms',
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
).padding(horizontal: 16),
|
|
||||||
),
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.centerRight,
|
|
||||||
child: TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
performAction();
|
|
||||||
},
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text("next").tr(),
|
|
||||||
const Icon(Symbols.chevron_right),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
).padding(all: 24).center(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _PostCreateModal extends HookConsumerWidget {
|
|
||||||
const _PostCreateModal();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
return Center(
|
|
||||||
child: ConstrainedBox(
|
|
||||||
constraints: const BoxConstraints(maxWidth: 280),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Text('🎉').fontSize(32),
|
|
||||||
Text(
|
|
||||||
'postCreateAccountTitle'.tr(),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
).fontSize(17),
|
|
||||||
const Gap(18),
|
|
||||||
Text('postCreateAccountNext').tr().fontSize(19).bold(),
|
|
||||||
const Gap(4),
|
|
||||||
Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
spacing: 6,
|
|
||||||
children: [
|
|
||||||
Text('\u2022'),
|
|
||||||
Expanded(child: Text('postCreateAccountNext1').tr()),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
spacing: 6,
|
|
||||||
children: [
|
|
||||||
Text('\u2022'),
|
|
||||||
Expanded(child: Text('postCreateAccountNext2').tr()),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const Gap(6),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.pop(context);
|
|
||||||
context.pushReplacementNamed('login');
|
|
||||||
},
|
|
||||||
child: Text('login'.tr()),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
486
lib/screens/auth/create_account_content.dart
Normal file
486
lib/screens/auth/create_account_content.dart
Normal file
@@ -0,0 +1,486 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:email_validator/email_validator.dart';
|
||||||
|
import 'package:flutter/foundation.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:gap/gap.dart';
|
||||||
|
import 'package:island/pods/config.dart';
|
||||||
|
import 'package:island/pods/network.dart';
|
||||||
|
import 'package:island/pods/userinfo.dart';
|
||||||
|
import 'package:island/pods/websocket.dart';
|
||||||
|
import 'package:island/screens/account/me/profile_update.dart';
|
||||||
|
import 'package:island/services/event_bus.dart';
|
||||||
|
import 'package:island/services/notify.dart';
|
||||||
|
import 'package:island/services/udid.dart';
|
||||||
|
import 'package:island/widgets/alert.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
|
|
||||||
|
import 'captcha.dart';
|
||||||
|
|
||||||
|
Widget getProviderIcon(String provider, {double size = 24, Color? color}) {
|
||||||
|
final providerLower = provider.toLowerCase();
|
||||||
|
|
||||||
|
// Check if we have an SVG for this provider
|
||||||
|
switch (providerLower) {
|
||||||
|
case 'apple':
|
||||||
|
case 'microsoft':
|
||||||
|
case 'google':
|
||||||
|
case 'github':
|
||||||
|
case 'discord':
|
||||||
|
case 'afdian':
|
||||||
|
case 'steam':
|
||||||
|
return SvgPicture.asset(
|
||||||
|
'assets/images/oidc/$providerLower.svg',
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
colorFilter:
|
||||||
|
color != null ? ColorFilter.mode(color, BlendMode.srcIn) : null,
|
||||||
|
);
|
||||||
|
case 'spotify':
|
||||||
|
return Image.asset(
|
||||||
|
'assets/images/oidc/spotify.png',
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
color: color,
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return Icon(Symbols.link, size: size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CreateAccountContent extends HookConsumerWidget {
|
||||||
|
const CreateAccountContent({super.key});
|
||||||
|
|
||||||
|
Map<String, dynamic> decodeJwt(String token) {
|
||||||
|
final parts = token.split('.');
|
||||||
|
if (parts.length != 3) throw FormatException('Invalid JWT');
|
||||||
|
final payload = parts[1];
|
||||||
|
final normalized = base64Url.normalize(payload);
|
||||||
|
final decoded = utf8.decode(base64Url.decode(normalized));
|
||||||
|
return json.decode(decoded);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final formKey = useMemoized(GlobalKey<FormState>.new, const []);
|
||||||
|
|
||||||
|
final emailController = useTextEditingController();
|
||||||
|
final usernameController = useTextEditingController();
|
||||||
|
final nicknameController = useTextEditingController();
|
||||||
|
final passwordController = useTextEditingController();
|
||||||
|
final waitingForOidc = useState(false);
|
||||||
|
final onboardingToken = useState<String?>(null);
|
||||||
|
|
||||||
|
void showPostCreateModal() {
|
||||||
|
showModalBottomSheet(
|
||||||
|
isScrollControlled: true,
|
||||||
|
context: context,
|
||||||
|
builder: (context) => _PostCreateModal(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void performAction() async {
|
||||||
|
if (!formKey.currentState!.validate()) return;
|
||||||
|
|
||||||
|
String endpoint = '/pass/accounts';
|
||||||
|
Map<String, dynamic> data = {};
|
||||||
|
|
||||||
|
if (onboardingToken.value != null) {
|
||||||
|
// OIDC onboarding
|
||||||
|
endpoint = '/pass/account/onboard';
|
||||||
|
data['onboarding_token'] = onboardingToken.value;
|
||||||
|
data['name'] = usernameController.text;
|
||||||
|
data['nick'] = nicknameController.text;
|
||||||
|
// Password is required in form, but might be optional
|
||||||
|
} else {
|
||||||
|
// Manual account creation
|
||||||
|
final captchaTk = await CaptchaScreen.show(context);
|
||||||
|
if (captchaTk == null) return;
|
||||||
|
if (!context.mounted) return;
|
||||||
|
data['captcha_token'] = captchaTk;
|
||||||
|
data['name'] = usernameController.text;
|
||||||
|
data['nick'] = nicknameController.text;
|
||||||
|
data['email'] = emailController.text;
|
||||||
|
data['password'] = passwordController.text;
|
||||||
|
data['language'] =
|
||||||
|
kServerSupportedLanguages[EasyLocalization.of(
|
||||||
|
context,
|
||||||
|
)!.currentLocale.toString()] ??
|
||||||
|
'en-us';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!context.mounted) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
showLoadingModal(context);
|
||||||
|
final client = ref.watch(apiClientProvider);
|
||||||
|
final resp = await client.post(endpoint, data: data);
|
||||||
|
if (endpoint == '/pass/account/onboard') {
|
||||||
|
// Onboard response has tokens, set them
|
||||||
|
final token = resp.data['token'];
|
||||||
|
setToken(ref.watch(sharedPreferencesProvider), token);
|
||||||
|
ref.invalidate(tokenProvider);
|
||||||
|
final userNotifier = ref.read(userInfoProvider.notifier);
|
||||||
|
await userNotifier.fetchUser();
|
||||||
|
final apiClient = ref.read(apiClientProvider);
|
||||||
|
subscribePushNotification(apiClient);
|
||||||
|
final wsNotifier = ref.read(websocketStateProvider.notifier);
|
||||||
|
wsNotifier.connect();
|
||||||
|
if (context.mounted) Navigator.pop(context, true);
|
||||||
|
} else {
|
||||||
|
if (!context.mounted) return;
|
||||||
|
hideLoadingModal(context);
|
||||||
|
onboardingToken.value = null; // reset
|
||||||
|
showPostCreateModal();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (context.mounted) hideLoadingModal(context);
|
||||||
|
showErrorAlert(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
final subscription = eventBus.on<OidcAuthCallbackEvent>().listen((
|
||||||
|
event,
|
||||||
|
) async {
|
||||||
|
if (!waitingForOidc.value || !context.mounted) return;
|
||||||
|
waitingForOidc.value = false;
|
||||||
|
final client = ref.watch(apiClientProvider);
|
||||||
|
try {
|
||||||
|
// Exchange code for tokens
|
||||||
|
final resp = await client.post(
|
||||||
|
'/pass/auth/token',
|
||||||
|
data: {
|
||||||
|
'grant_type': 'authorization_code',
|
||||||
|
'code': event.challengeId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
final data = resp.data;
|
||||||
|
if (data.containsKey('onboarding_token')) {
|
||||||
|
// New user onboarding
|
||||||
|
final token = data['onboarding_token'] as String;
|
||||||
|
final decoded = decodeJwt(token);
|
||||||
|
final name = decoded['name'] as String?;
|
||||||
|
final email = decoded['email'] as String?;
|
||||||
|
final provider = decoded['provider'] as String?;
|
||||||
|
// Pre-fill form
|
||||||
|
usernameController.text = '';
|
||||||
|
nicknameController.text = name ?? '';
|
||||||
|
emailController.text = email ?? '';
|
||||||
|
passwordController.clear(); // User needs to set password
|
||||||
|
onboardingToken.value = token;
|
||||||
|
// Optionally show a message
|
||||||
|
showSnackBar('Pre-filled from ${provider ?? 'provider'}');
|
||||||
|
} else {
|
||||||
|
// Existing user, switch to login
|
||||||
|
showSnackBar('Account already exists. Redirecting to login.');
|
||||||
|
if (context.mounted) context.goNamed('login');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
showErrorAlert(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return subscription.cancel;
|
||||||
|
}, [waitingForOidc.value, context.mounted]);
|
||||||
|
|
||||||
|
Future<void> withOidc(String provider) async {
|
||||||
|
waitingForOidc.value = true;
|
||||||
|
final serverUrl = ref.watch(serverUrlProvider);
|
||||||
|
final deviceId = await getUdid();
|
||||||
|
final url =
|
||||||
|
Uri.parse('$serverUrl/pass/auth/login/${provider.toLowerCase()}')
|
||||||
|
.replace(
|
||||||
|
queryParameters: {
|
||||||
|
'returnUrl': 'solian://auth/callback',
|
||||||
|
'deviceId': deviceId,
|
||||||
|
'flow': 'login',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.toString();
|
||||||
|
final isLaunched = await launchUrlString(
|
||||||
|
url,
|
||||||
|
mode:
|
||||||
|
kIsWeb
|
||||||
|
? LaunchMode.platformDefault
|
||||||
|
: LaunchMode.externalApplication,
|
||||||
|
);
|
||||||
|
if (!isLaunched) {
|
||||||
|
waitingForOidc.value = false;
|
||||||
|
showErrorAlert('failedToLaunchBrowser'.tr());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return StyledWidget(
|
||||||
|
Container(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 380),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: CircleAvatar(
|
||||||
|
radius: 26,
|
||||||
|
child: const Icon(Symbols.person_add, size: 28),
|
||||||
|
).padding(bottom: 8),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'createAccount',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 28,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
),
|
||||||
|
).tr().padding(left: 4, bottom: 16),
|
||||||
|
if (!kIsWeb)
|
||||||
|
Row(
|
||||||
|
spacing: 6,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Text("orCreateWith").tr().fontSize(11).opacity(0.85),
|
||||||
|
const Gap(8),
|
||||||
|
Spacer(),
|
||||||
|
IconButton.filledTonal(
|
||||||
|
onPressed: () => withOidc('github'),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
icon: getProviderIcon(
|
||||||
|
"github",
|
||||||
|
size: 16,
|
||||||
|
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||||
|
),
|
||||||
|
tooltip: 'GitHub',
|
||||||
|
),
|
||||||
|
IconButton.filledTonal(
|
||||||
|
onPressed: () => withOidc('google'),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
icon: getProviderIcon(
|
||||||
|
"google",
|
||||||
|
size: 16,
|
||||||
|
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||||
|
),
|
||||||
|
tooltip: 'Google',
|
||||||
|
),
|
||||||
|
IconButton.filledTonal(
|
||||||
|
onPressed: () => withOidc('apple'),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
icon: getProviderIcon(
|
||||||
|
"apple",
|
||||||
|
size: 16,
|
||||||
|
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||||
|
),
|
||||||
|
tooltip: 'Apple Account',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 8, vertical: 8)
|
||||||
|
else
|
||||||
|
const Gap(12),
|
||||||
|
Form(
|
||||||
|
key: formKey,
|
||||||
|
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
TextFormField(
|
||||||
|
controller: usernameController,
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'fieldCannotBeEmpty'.tr();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
autocorrect: false,
|
||||||
|
enableSuggestions: false,
|
||||||
|
autofillHints: const [AutofillHints.username],
|
||||||
|
decoration: InputDecoration(
|
||||||
|
isDense: true,
|
||||||
|
border: const UnderlineInputBorder(),
|
||||||
|
labelText: 'username'.tr(),
|
||||||
|
helperText: 'usernameCannotChangeHint'.tr(),
|
||||||
|
),
|
||||||
|
onTapOutside:
|
||||||
|
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
),
|
||||||
|
const Gap(12),
|
||||||
|
TextFormField(
|
||||||
|
controller: nicknameController,
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'fieldCannotBeEmpty'.tr();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
autocorrect: false,
|
||||||
|
autofillHints: const [AutofillHints.nickname],
|
||||||
|
decoration: InputDecoration(
|
||||||
|
isDense: true,
|
||||||
|
border: const UnderlineInputBorder(),
|
||||||
|
labelText: 'nickname'.tr(),
|
||||||
|
),
|
||||||
|
onTapOutside:
|
||||||
|
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
),
|
||||||
|
const Gap(12),
|
||||||
|
TextFormField(
|
||||||
|
controller: emailController,
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'fieldCannotBeEmpty'.tr();
|
||||||
|
}
|
||||||
|
if (!EmailValidator.validate(value)) {
|
||||||
|
return 'fieldEmailAddressMustBeValid'.tr();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
autocorrect: false,
|
||||||
|
enableSuggestions: false,
|
||||||
|
autofillHints: const [AutofillHints.email],
|
||||||
|
decoration: InputDecoration(
|
||||||
|
isDense: true,
|
||||||
|
border: const UnderlineInputBorder(),
|
||||||
|
labelText: 'email'.tr(),
|
||||||
|
),
|
||||||
|
onTapOutside:
|
||||||
|
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
),
|
||||||
|
const Gap(12),
|
||||||
|
TextFormField(
|
||||||
|
controller: passwordController,
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'fieldCannotBeEmpty'.tr();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
obscureText: true,
|
||||||
|
autocorrect: false,
|
||||||
|
enableSuggestions: false,
|
||||||
|
autofillHints: const [AutofillHints.password],
|
||||||
|
decoration: InputDecoration(
|
||||||
|
isDense: true,
|
||||||
|
border: const UnderlineInputBorder(),
|
||||||
|
labelText: 'password'.tr(),
|
||||||
|
),
|
||||||
|
onTapOutside:
|
||||||
|
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 7),
|
||||||
|
),
|
||||||
|
const Gap(16),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: StyledWidget(
|
||||||
|
Container(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 290),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'termAcceptNextWithAgree'.tr(),
|
||||||
|
textAlign: TextAlign.end,
|
||||||
|
style: Theme.of(
|
||||||
|
context,
|
||||||
|
).textTheme.bodySmall!.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface
|
||||||
|
.withAlpha((255 * 0.75).round()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text('termAcceptLink').tr(),
|
||||||
|
const Gap(4),
|
||||||
|
const Icon(Symbols.launch, size: 14),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
launchUrlString('https://solsynth.dev/terms');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).padding(horizontal: 16),
|
||||||
|
),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
performAction();
|
||||||
|
},
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text("next").tr(),
|
||||||
|
const Icon(Symbols.chevron_right),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).padding(all: 24).center();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PostCreateModal extends HookConsumerWidget {
|
||||||
|
const _PostCreateModal();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return Center(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 280),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text('🎉').fontSize(32),
|
||||||
|
Text(
|
||||||
|
'postCreateAccountTitle'.tr(),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
).fontSize(17),
|
||||||
|
const Gap(18),
|
||||||
|
Text('postCreateAccountNext').tr().fontSize(19).bold(),
|
||||||
|
const Gap(4),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
spacing: 6,
|
||||||
|
children: [
|
||||||
|
Text('\u2022'),
|
||||||
|
Expanded(child: Text('postCreateAccountNext1').tr()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
spacing: 6,
|
||||||
|
children: [
|
||||||
|
Text('\u2022'),
|
||||||
|
Expanded(child: Text('postCreateAccountNext2').tr()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Gap(6),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
context.pushReplacementNamed('login');
|
||||||
|
},
|
||||||
|
child: Text('login'.tr()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
lib/screens/auth/create_account_modal.dart
Normal file
19
lib/screens/auth/create_account_modal.dart
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:island/widgets/content/sheet.dart';
|
||||||
|
|
||||||
|
import 'create_account_content.dart';
|
||||||
|
|
||||||
|
class CreateAccountModal extends HookConsumerWidget {
|
||||||
|
const CreateAccountModal({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return SheetScaffold(
|
||||||
|
titleText: 'createAccount'.tr(),
|
||||||
|
heightFactor: 0.9,
|
||||||
|
child: CreateAccountContent(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,31 +1,10 @@
|
|||||||
import 'dart:convert';
|
|
||||||
import 'dart:math' as math;
|
|
||||||
import 'package:animations/animations.dart';
|
|
||||||
import 'package:dio/dio.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:flutter_otp_text_field/flutter_otp_text_field.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:gap/gap.dart';
|
|
||||||
import 'package:island/models/auth.dart';
|
|
||||||
import 'package:island/pods/config.dart';
|
|
||||||
import 'package:island/pods/network.dart';
|
|
||||||
import 'package:island/pods/userinfo.dart';
|
|
||||||
import 'package:island/pods/websocket.dart';
|
|
||||||
import 'package:island/screens/account/me/settings_connections.dart';
|
|
||||||
import 'package:island/screens/auth/oidc.dart';
|
|
||||||
import 'package:island/services/notify.dart';
|
|
||||||
import 'package:island/services/udid.dart';
|
|
||||||
import 'package:island/widgets/alert.dart';
|
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:sign_in_with_apple/sign_in_with_apple.dart';
|
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
|
||||||
|
|
||||||
import 'captcha.dart';
|
import 'login_content.dart';
|
||||||
|
|
||||||
final Map<int, (String, String, IconData)> kFactorTypes = {
|
final Map<int, (String, String, IconData)> kFactorTypes = {
|
||||||
0: ('authFactorPassword', 'authFactorPasswordDescription', Symbols.password),
|
0: ('authFactorPassword', 'authFactorPasswordDescription', Symbols.password),
|
||||||
@@ -44,745 +23,13 @@ class LoginScreen extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final isBusy = useState(false);
|
|
||||||
|
|
||||||
final period = useState(0);
|
|
||||||
final currentTicket = useState<SnAuthChallenge?>(null);
|
|
||||||
final factors = useState<List<SnAuthFactor>>([]);
|
|
||||||
final factorPicked = useState<SnAuthFactor?>(null);
|
|
||||||
|
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
isNoBackground: false,
|
isNoBackground: false,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: const PageBackButton(),
|
leading: const PageBackButton(),
|
||||||
title: Text('login').tr(),
|
title: Text('login').tr(),
|
||||||
),
|
),
|
||||||
body: Theme(
|
body: LoginContent(),
|
||||||
data: Theme.of(context).copyWith(canvasColor: Colors.transparent),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
if (isBusy.value)
|
|
||||||
LinearProgressIndicator(
|
|
||||||
minHeight: 4,
|
|
||||||
borderRadius: BorderRadius.zero,
|
|
||||||
trackGap: 0,
|
|
||||||
stopIndicatorRadius: 0,
|
|
||||||
)
|
|
||||||
else if (currentTicket.value != null)
|
|
||||||
LinearProgressIndicator(
|
|
||||||
minHeight: 4,
|
|
||||||
borderRadius: BorderRadius.zero,
|
|
||||||
trackGap: 0,
|
|
||||||
stopIndicatorRadius: 0,
|
|
||||||
value:
|
|
||||||
1 -
|
|
||||||
(currentTicket.value!.stepRemain /
|
|
||||||
currentTicket.value!.stepTotal),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
const Gap(4),
|
|
||||||
Expanded(
|
|
||||||
child:
|
|
||||||
SingleChildScrollView(
|
|
||||||
child: PageTransitionSwitcher(
|
|
||||||
transitionBuilder: (
|
|
||||||
Widget child,
|
|
||||||
Animation<double> primaryAnimation,
|
|
||||||
Animation<double> secondaryAnimation,
|
|
||||||
) {
|
|
||||||
return SharedAxisTransition(
|
|
||||||
animation: primaryAnimation,
|
|
||||||
secondaryAnimation: secondaryAnimation,
|
|
||||||
transitionType: SharedAxisTransitionType.horizontal,
|
|
||||||
child: Container(
|
|
||||||
constraints: BoxConstraints(maxWidth: 380),
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: switch (period.value % 3) {
|
|
||||||
1 => _LoginPickerScreen(
|
|
||||||
key: const ValueKey(1),
|
|
||||||
challenge: currentTicket.value,
|
|
||||||
factors: factors.value,
|
|
||||||
onChallenge:
|
|
||||||
(SnAuthChallenge? p0) => currentTicket.value = p0,
|
|
||||||
onPickFactor:
|
|
||||||
(SnAuthFactor p0) => factorPicked.value = p0,
|
|
||||||
onNext: () => period.value++,
|
|
||||||
onBusy: (value) => isBusy.value = value,
|
|
||||||
),
|
|
||||||
2 => _LoginCheckScreen(
|
|
||||||
key: const ValueKey(2),
|
|
||||||
challenge: currentTicket.value,
|
|
||||||
factor: factorPicked.value,
|
|
||||||
onChallenge:
|
|
||||||
(SnAuthChallenge? p0) => currentTicket.value = p0,
|
|
||||||
onNext: () => period.value = 1,
|
|
||||||
onBusy: (value) => isBusy.value = value,
|
|
||||||
),
|
|
||||||
_ => _LoginLookupScreen(
|
|
||||||
key: const ValueKey(0),
|
|
||||||
ticket: currentTicket.value,
|
|
||||||
onChallenge:
|
|
||||||
(SnAuthChallenge? p0) => currentTicket.value = p0,
|
|
||||||
onFactor:
|
|
||||||
(List<SnAuthFactor>? p0) =>
|
|
||||||
factors.value = p0 ?? [],
|
|
||||||
onNext: () => period.value++,
|
|
||||||
onBusy: (value) => isBusy.value = value,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
).padding(all: 24),
|
|
||||||
).center(),
|
|
||||||
),
|
|
||||||
|
|
||||||
const Gap(4),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _LoginCheckScreen extends HookConsumerWidget {
|
|
||||||
final SnAuthChallenge? challenge;
|
|
||||||
final SnAuthFactor? factor;
|
|
||||||
final Function(SnAuthChallenge?) onChallenge;
|
|
||||||
final VoidCallback onNext;
|
|
||||||
final Function(bool) onBusy;
|
|
||||||
|
|
||||||
const _LoginCheckScreen({
|
|
||||||
super.key,
|
|
||||||
required this.challenge,
|
|
||||||
required this.factor,
|
|
||||||
required this.onChallenge,
|
|
||||||
required this.onNext,
|
|
||||||
required this.onBusy,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final isBusy = useState(false);
|
|
||||||
final passwordController = useTextEditingController();
|
|
||||||
|
|
||||||
useEffect(() {
|
|
||||||
onBusy.call(isBusy.value);
|
|
||||||
return null;
|
|
||||||
}, [isBusy]);
|
|
||||||
|
|
||||||
Future<void> getToken({String? code}) async {
|
|
||||||
// Get token if challenge is completed
|
|
||||||
final client = ref.watch(apiClientProvider);
|
|
||||||
final tokenResp = await client.post(
|
|
||||||
'/pass/auth/token',
|
|
||||||
data: {
|
|
||||||
'grant_type': 'authorization_code',
|
|
||||||
'code': code ?? challenge!.id,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
final token = tokenResp.data['token'];
|
|
||||||
setToken(ref.watch(sharedPreferencesProvider), token);
|
|
||||||
ref.invalidate(tokenProvider);
|
|
||||||
if (!context.mounted) return;
|
|
||||||
|
|
||||||
// Do post login tasks
|
|
||||||
final userNotifier = ref.read(userInfoProvider.notifier);
|
|
||||||
userNotifier.fetchUser().then((_) {
|
|
||||||
final apiClient = ref.read(apiClientProvider);
|
|
||||||
subscribePushNotification(apiClient);
|
|
||||||
final wsNotifier = ref.read(websocketStateProvider.notifier);
|
|
||||||
wsNotifier.connect();
|
|
||||||
if (context.mounted) Navigator.pop(context, true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() {
|
|
||||||
if (challenge != null && challenge?.stepRemain == 0) {
|
|
||||||
Future(() {
|
|
||||||
if (isBusy.value) return;
|
|
||||||
isBusy.value = true;
|
|
||||||
getToken().catchError((err) {
|
|
||||||
showErrorAlert(err);
|
|
||||||
isBusy.value = false;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}, [challenge]);
|
|
||||||
|
|
||||||
if (factor == null) {
|
|
||||||
// Logging in by third parties
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: CircleAvatar(
|
|
||||||
radius: 26,
|
|
||||||
child: const Icon(Symbols.asterisk, size: 28),
|
|
||||||
).padding(bottom: 8),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'loginInProgress'.tr(),
|
|
||||||
style: const TextStyle(fontSize: 28, fontWeight: FontWeight.w900),
|
|
||||||
).padding(left: 4, bottom: 16),
|
|
||||||
const Gap(16),
|
|
||||||
CircularProgressIndicator().alignment(Alignment.centerLeft),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> performCheckTicket() async {
|
|
||||||
final pwd = passwordController.value.text;
|
|
||||||
if (pwd.isEmpty) return;
|
|
||||||
isBusy.value = true;
|
|
||||||
try {
|
|
||||||
// Pass challenge
|
|
||||||
final client = ref.watch(apiClientProvider);
|
|
||||||
final resp = await client.patch(
|
|
||||||
'/pass/auth/challenge/${challenge!.id}',
|
|
||||||
data: {'factor_id': factor!.id, 'password': pwd},
|
|
||||||
);
|
|
||||||
final result = SnAuthChallenge.fromJson(resp.data);
|
|
||||||
onChallenge(result);
|
|
||||||
if (result.stepRemain > 0) {
|
|
||||||
onNext();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await getToken(code: result.id);
|
|
||||||
} catch (err) {
|
|
||||||
showErrorAlert(err);
|
|
||||||
return;
|
|
||||||
} finally {
|
|
||||||
isBusy.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final width = math.min(380, MediaQuery.of(context).size.width);
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: CircleAvatar(
|
|
||||||
radius: 26,
|
|
||||||
child: const Icon(Symbols.asterisk, size: 28),
|
|
||||||
).padding(bottom: 8),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'loginEnterPassword'.tr(),
|
|
||||||
style: const TextStyle(fontSize: 28, fontWeight: FontWeight.w900),
|
|
||||||
).padding(left: 4, bottom: 16),
|
|
||||||
if ([0].contains(factor!.type))
|
|
||||||
TextField(
|
|
||||||
autocorrect: false,
|
|
||||||
enableSuggestions: false,
|
|
||||||
controller: passwordController,
|
|
||||||
obscureText: true,
|
|
||||||
autofillHints: [
|
|
||||||
factor!.type == 0
|
|
||||||
? AutofillHints.password
|
|
||||||
: AutofillHints.oneTimeCode,
|
|
||||||
],
|
|
||||||
decoration: InputDecoration(
|
|
||||||
isDense: true,
|
|
||||||
labelText: 'password'.tr(),
|
|
||||||
),
|
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
|
||||||
onSubmitted: isBusy.value ? null : (_) => performCheckTicket(),
|
|
||||||
).padding(horizontal: 7)
|
|
||||||
else
|
|
||||||
OtpTextField(
|
|
||||||
showCursor: false,
|
|
||||||
numberOfFields: 6,
|
|
||||||
obscureText: false,
|
|
||||||
showFieldAsBox: true,
|
|
||||||
focusedBorderColor: Theme.of(context).colorScheme.primary,
|
|
||||||
fieldWidth: (width / 6) - 10,
|
|
||||||
onSubmit: (value) {
|
|
||||||
passwordController.text = value;
|
|
||||||
performCheckTicket();
|
|
||||||
},
|
|
||||||
textStyle: Theme.of(context).textTheme.titleLarge!,
|
|
||||||
),
|
|
||||||
const Gap(12),
|
|
||||||
ListTile(
|
|
||||||
leading: Icon(
|
|
||||||
kFactorTypes[factor!.type]?.$3 ?? Symbols.question_mark,
|
|
||||||
),
|
|
||||||
title: Text(kFactorTypes[factor!.type]?.$1 ?? 'unknown').tr(),
|
|
||||||
subtitle: Text(kFactorTypes[factor!.type]?.$2 ?? 'unknown').tr(),
|
|
||||||
),
|
|
||||||
const Gap(12),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: isBusy.value ? null : () => performCheckTicket(),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text('next').tr(),
|
|
||||||
const Icon(Symbols.chevron_right),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _LoginPickerScreen extends HookConsumerWidget {
|
|
||||||
final SnAuthChallenge? challenge;
|
|
||||||
final List<SnAuthFactor>? factors;
|
|
||||||
final Function(SnAuthChallenge?) onChallenge;
|
|
||||||
final Function(SnAuthFactor) onPickFactor;
|
|
||||||
final VoidCallback onNext;
|
|
||||||
final Function(bool) onBusy;
|
|
||||||
|
|
||||||
const _LoginPickerScreen({
|
|
||||||
super.key,
|
|
||||||
required this.challenge,
|
|
||||||
required this.factors,
|
|
||||||
required this.onChallenge,
|
|
||||||
required this.onPickFactor,
|
|
||||||
required this.onNext,
|
|
||||||
required this.onBusy,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final isBusy = useState(false);
|
|
||||||
final factorPicked = useState<SnAuthFactor?>(null);
|
|
||||||
|
|
||||||
useEffect(() {
|
|
||||||
onBusy.call(isBusy.value);
|
|
||||||
return null;
|
|
||||||
}, [isBusy]);
|
|
||||||
|
|
||||||
useEffect(() {
|
|
||||||
if (challenge != null && challenge?.stepRemain == 0) {
|
|
||||||
Future(() {
|
|
||||||
onNext();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}, [challenge]);
|
|
||||||
|
|
||||||
final unfocusColor = Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.onSurface.withAlpha((255 * 0.75).round());
|
|
||||||
|
|
||||||
final hintController = useTextEditingController();
|
|
||||||
|
|
||||||
void performGetFactorCode() async {
|
|
||||||
if (factorPicked.value == null) return;
|
|
||||||
|
|
||||||
isBusy.value = true;
|
|
||||||
final client = ref.watch(apiClientProvider);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await client.post(
|
|
||||||
'/pass/auth/challenge/${challenge!.id}/factors/${factorPicked.value!.id}',
|
|
||||||
data:
|
|
||||||
hintController.text.isNotEmpty
|
|
||||||
? jsonEncode(hintController.text)
|
|
||||||
: null,
|
|
||||||
);
|
|
||||||
onPickFactor(factors!.where((x) => x == factorPicked.value).first);
|
|
||||||
onNext();
|
|
||||||
} catch (err) {
|
|
||||||
if (err is DioException && err.response?.statusCode == 400) {
|
|
||||||
onPickFactor(factors!.where((x) => x == factorPicked.value).first);
|
|
||||||
onNext();
|
|
||||||
if (context.mounted) {
|
|
||||||
showSnackBar(err.response!.data.toString());
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
showErrorAlert(err);
|
|
||||||
return;
|
|
||||||
} finally {
|
|
||||||
isBusy.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
key: const ValueKey<int>(1),
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: CircleAvatar(
|
|
||||||
radius: 26,
|
|
||||||
child: const Icon(Symbols.lock, size: 28),
|
|
||||||
).padding(bottom: 8),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'loginPickFactor',
|
|
||||||
style: const TextStyle(fontSize: 28, fontWeight: FontWeight.w900),
|
|
||||||
).tr().padding(left: 4),
|
|
||||||
const Gap(8),
|
|
||||||
Card(
|
|
||||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
|
||||||
child: Column(
|
|
||||||
children:
|
|
||||||
factors
|
|
||||||
?.map(
|
|
||||||
(x) => CheckboxListTile(
|
|
||||||
shape: const RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
|
||||||
),
|
|
||||||
secondary: Icon(
|
|
||||||
kFactorTypes[x.type]?.$3 ?? Symbols.question_mark,
|
|
||||||
),
|
|
||||||
title: Text(kFactorTypes[x.type]?.$1 ?? 'unknown').tr(),
|
|
||||||
enabled: !challenge!.blacklistFactors.contains(x.id),
|
|
||||||
value: factorPicked.value == x,
|
|
||||||
onChanged: (value) {
|
|
||||||
if (value == true) {
|
|
||||||
factorPicked.value = x;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList() ??
|
|
||||||
List.empty(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if ([1].contains(factorPicked.value?.type))
|
|
||||||
TextField(
|
|
||||||
controller: hintController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
isDense: true,
|
|
||||||
border: const OutlineInputBorder(),
|
|
||||||
labelText: 'authFactorHint'.tr(),
|
|
||||||
helperText: 'authFactorHintHelper'.tr(),
|
|
||||||
),
|
|
||||||
).padding(top: 12, bottom: 4, horizontal: 4),
|
|
||||||
const Gap(8),
|
|
||||||
Text(
|
|
||||||
'loginMultiFactor'.plural(challenge!.stepRemain),
|
|
||||||
style: TextStyle(color: unfocusColor, fontSize: 13),
|
|
||||||
).padding(horizontal: 16),
|
|
||||||
const Gap(12),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: isBusy.value ? null : () => performGetFactorCode(),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text('next'.tr()),
|
|
||||||
const Icon(Symbols.chevron_right),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _LoginLookupScreen extends HookConsumerWidget {
|
|
||||||
final SnAuthChallenge? ticket;
|
|
||||||
final Function(SnAuthChallenge?) onChallenge;
|
|
||||||
final Function(List<SnAuthFactor>?) onFactor;
|
|
||||||
final VoidCallback onNext;
|
|
||||||
final Function(bool) onBusy;
|
|
||||||
|
|
||||||
const _LoginLookupScreen({
|
|
||||||
super.key,
|
|
||||||
required this.ticket,
|
|
||||||
required this.onChallenge,
|
|
||||||
required this.onFactor,
|
|
||||||
required this.onNext,
|
|
||||||
required this.onBusy,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final isBusy = useState(false);
|
|
||||||
final usernameController = useTextEditingController();
|
|
||||||
|
|
||||||
useEffect(() {
|
|
||||||
onBusy.call(isBusy.value);
|
|
||||||
return null;
|
|
||||||
}, [isBusy]);
|
|
||||||
|
|
||||||
Future<void> requestResetPassword() async {
|
|
||||||
final uname = usernameController.value.text;
|
|
||||||
if (uname.isEmpty) {
|
|
||||||
showErrorAlert('loginResetPasswordHint'.tr());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final captchaTk = await Navigator.of(
|
|
||||||
context,
|
|
||||||
).push(MaterialPageRoute(builder: (context) => CaptchaScreen()));
|
|
||||||
if (captchaTk == null) return;
|
|
||||||
isBusy.value = true;
|
|
||||||
try {
|
|
||||||
final client = ref.watch(apiClientProvider);
|
|
||||||
await client.post(
|
|
||||||
'/pass/accounts/recovery/password',
|
|
||||||
data: {'account': uname, 'captcha_token': captchaTk},
|
|
||||||
);
|
|
||||||
showInfoAlert('loginResetPasswordSent'.tr(), 'done'.tr());
|
|
||||||
} catch (err) {
|
|
||||||
showErrorAlert(err);
|
|
||||||
} finally {
|
|
||||||
isBusy.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> performNewTicket() async {
|
|
||||||
final uname = usernameController.value.text;
|
|
||||||
if (uname.isEmpty) return;
|
|
||||||
isBusy.value = true;
|
|
||||||
try {
|
|
||||||
final client = ref.watch(apiClientProvider);
|
|
||||||
final resp = await client.post(
|
|
||||||
'/pass/auth/challenge',
|
|
||||||
data: {
|
|
||||||
'account': uname,
|
|
||||||
'device_id': await getUdid(),
|
|
||||||
'device_name': await getDeviceName(),
|
|
||||||
'platform':
|
|
||||||
kIsWeb
|
|
||||||
? 1
|
|
||||||
: switch (defaultTargetPlatform) {
|
|
||||||
TargetPlatform.iOS => 2,
|
|
||||||
TargetPlatform.android => 3,
|
|
||||||
TargetPlatform.macOS => 4,
|
|
||||||
TargetPlatform.windows => 5,
|
|
||||||
TargetPlatform.linux => 6,
|
|
||||||
_ => 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
final result = SnAuthChallenge.fromJson(resp.data);
|
|
||||||
onChallenge(result);
|
|
||||||
final factorResp = await client.get(
|
|
||||||
'/pass/auth/challenge/${result.id}/factors',
|
|
||||||
);
|
|
||||||
onFactor(
|
|
||||||
List<SnAuthFactor>.from(
|
|
||||||
factorResp.data.map((ele) => SnAuthFactor.fromJson(ele)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
onNext();
|
|
||||||
} catch (err) {
|
|
||||||
showErrorAlert(err);
|
|
||||||
return;
|
|
||||||
} finally {
|
|
||||||
isBusy.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> withApple() async {
|
|
||||||
final client = ref.watch(apiClientProvider);
|
|
||||||
try {
|
|
||||||
final credential = await SignInWithApple.getAppleIDCredential(
|
|
||||||
scopes: [AppleIDAuthorizationScopes.email],
|
|
||||||
webAuthenticationOptions: WebAuthenticationOptions(
|
|
||||||
clientId: 'dev.solsynth.solarpass',
|
|
||||||
redirectUri: Uri.parse('https://nt.solian.app/auth/callback/apple'),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (context.mounted) showLoadingModal(context);
|
|
||||||
final resp = await client.post(
|
|
||||||
'/pass/auth/login/apple/mobile',
|
|
||||||
data: {
|
|
||||||
'identity_token': credential.identityToken!,
|
|
||||||
'authorization_code': credential.authorizationCode,
|
|
||||||
'device_id': await getUdid(),
|
|
||||||
'device_name': await getDeviceName(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
final challenge = SnAuthChallenge.fromJson(resp.data);
|
|
||||||
onChallenge(challenge);
|
|
||||||
final factorResp = await client.get(
|
|
||||||
'/pass/auth/challenge/${challenge.id}/factors',
|
|
||||||
);
|
|
||||||
onFactor(
|
|
||||||
List<SnAuthFactor>.from(
|
|
||||||
factorResp.data.map((ele) => SnAuthFactor.fromJson(ele)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
onNext();
|
|
||||||
} catch (err) {
|
|
||||||
if (err is SignInWithAppleAuthorizationException) return;
|
|
||||||
showErrorAlert(err);
|
|
||||||
} finally {
|
|
||||||
if (context.mounted) hideLoadingModal(context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> withOidc(String provider) async {
|
|
||||||
final challengeId = await Navigator.of(context, rootNavigator: true).push(
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => OidcScreen(provider: provider.toLowerCase()),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final client = ref.watch(apiClientProvider);
|
|
||||||
try {
|
|
||||||
final resp = await client.get('/pass/auth/challenge/$challengeId');
|
|
||||||
final challenge = SnAuthChallenge.fromJson(resp.data);
|
|
||||||
onChallenge(challenge);
|
|
||||||
final factorResp = await client.get(
|
|
||||||
'/pass/auth/challenge/${challenge.id}/factors',
|
|
||||||
);
|
|
||||||
onFactor(
|
|
||||||
List<SnAuthFactor>.from(
|
|
||||||
factorResp.data.map((ele) => SnAuthFactor.fromJson(ele)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
onNext();
|
|
||||||
} catch (err) {
|
|
||||||
showErrorAlert(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: CircleAvatar(
|
|
||||||
radius: 26,
|
|
||||||
child: const Icon(Symbols.login, size: 28),
|
|
||||||
).padding(bottom: 8),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'loginGreeting',
|
|
||||||
style: const TextStyle(fontSize: 28, fontWeight: FontWeight.w900),
|
|
||||||
).tr().padding(left: 4, bottom: 16),
|
|
||||||
TextField(
|
|
||||||
autocorrect: false,
|
|
||||||
enableSuggestions: false,
|
|
||||||
controller: usernameController,
|
|
||||||
autofillHints: const [AutofillHints.username],
|
|
||||||
decoration: InputDecoration(
|
|
||||||
isDense: true,
|
|
||||||
border: const UnderlineInputBorder(),
|
|
||||||
labelText: 'username'.tr(),
|
|
||||||
helperText: 'usernameLookupHint'.tr(),
|
|
||||||
),
|
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
|
||||||
onSubmitted: isBusy.value ? null : (_) => performNewTicket(),
|
|
||||||
).padding(horizontal: 7),
|
|
||||||
if (!kIsWeb)
|
|
||||||
Row(
|
|
||||||
spacing: 6,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: <Widget>[
|
|
||||||
Text("loginOr").tr().fontSize(11).opacity(0.85),
|
|
||||||
const Gap(8),
|
|
||||||
Spacer(),
|
|
||||||
IconButton.filledTonal(
|
|
||||||
onPressed: () => withOidc('github'),
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
icon: getProviderIcon(
|
|
||||||
"github",
|
|
||||||
size: 16,
|
|
||||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
|
||||||
),
|
|
||||||
tooltip: 'GitHub',
|
|
||||||
),
|
|
||||||
IconButton.filledTonal(
|
|
||||||
onPressed: () => withOidc('google'),
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
icon: getProviderIcon(
|
|
||||||
"google",
|
|
||||||
size: 16,
|
|
||||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
|
||||||
),
|
|
||||||
tooltip: 'Google',
|
|
||||||
),
|
|
||||||
IconButton.filledTonal(
|
|
||||||
onPressed: withApple,
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
icon: getProviderIcon(
|
|
||||||
"apple",
|
|
||||||
size: 16,
|
|
||||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
|
||||||
),
|
|
||||||
tooltip: 'Apple Account',
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).padding(horizontal: 8, vertical: 8)
|
|
||||||
else
|
|
||||||
const Gap(12),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: isBusy.value ? null : () => requestResetPassword(),
|
|
||||||
style: TextButton.styleFrom(foregroundColor: Colors.grey),
|
|
||||||
child: Text('forgotPassword'.tr()),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: isBusy.value ? null : () => performNewTicket(),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text('next').tr(),
|
|
||||||
const Icon(Symbols.chevron_right),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const Gap(12),
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.centerRight,
|
|
||||||
child: StyledWidget(
|
|
||||||
Container(
|
|
||||||
constraints: const BoxConstraints(maxWidth: 290),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'termAcceptNextWithAgree'.tr(),
|
|
||||||
textAlign: TextAlign.end,
|
|
||||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
|
||||||
color: Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.onSurface.withAlpha((255 * 0.75).round()),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Material(
|
|
||||||
color: Colors.transparent,
|
|
||||||
child: InkWell(
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text('termAcceptLink'.tr()),
|
|
||||||
const Gap(4),
|
|
||||||
const Icon(Symbols.launch, size: 14),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
launchUrlString('https://solsynth.dev/terms');
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
).padding(horizontal: 16),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
814
lib/screens/auth/login_content.dart
Normal file
814
lib/screens/auth/login_content.dart
Normal file
@@ -0,0 +1,814 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
import 'package:animations/animations.dart';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:flutter_otp_text_field/flutter_otp_text_field.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:island/models/auth.dart';
|
||||||
|
import 'package:island/pods/config.dart';
|
||||||
|
import 'package:island/pods/network.dart';
|
||||||
|
import 'package:island/pods/userinfo.dart';
|
||||||
|
import 'package:island/pods/websocket.dart';
|
||||||
|
import 'package:island/screens/account/me/settings_connections.dart';
|
||||||
|
import 'package:island/services/event_bus.dart';
|
||||||
|
import 'package:island/services/notify.dart';
|
||||||
|
import 'package:island/services/udid.dart';
|
||||||
|
import 'package:island/widgets/alert.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
import 'package:sign_in_with_apple/sign_in_with_apple.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
|
import 'captcha.dart';
|
||||||
|
|
||||||
|
final Map<int, (String, String, IconData)> kFactorTypes = {
|
||||||
|
0: ('authFactorPassword', 'authFactorPasswordDescription', Symbols.password),
|
||||||
|
1: ('authFactorEmail', 'authFactorEmailDescription', Symbols.email),
|
||||||
|
2: (
|
||||||
|
'authFactorInAppNotify',
|
||||||
|
'authFactorInAppNotifyDescription',
|
||||||
|
Symbols.notifications_active,
|
||||||
|
),
|
||||||
|
3: ('authFactorTOTP', 'authFactorTOTPDescription', Symbols.timer),
|
||||||
|
4: ('authFactorPin', 'authFactorPinDescription', Symbols.nest_secure_alarm),
|
||||||
|
};
|
||||||
|
|
||||||
|
class _LoginCheckScreen extends HookConsumerWidget {
|
||||||
|
final SnAuthChallenge? challenge;
|
||||||
|
final SnAuthFactor? factor;
|
||||||
|
final Function(SnAuthChallenge?) onChallenge;
|
||||||
|
final VoidCallback onNext;
|
||||||
|
final Function(bool) onBusy;
|
||||||
|
|
||||||
|
const _LoginCheckScreen({
|
||||||
|
super.key,
|
||||||
|
required this.challenge,
|
||||||
|
required this.factor,
|
||||||
|
required this.onChallenge,
|
||||||
|
required this.onNext,
|
||||||
|
required this.onBusy,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final isBusy = useState(false);
|
||||||
|
final passwordController = useTextEditingController();
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
onBusy.call(isBusy.value);
|
||||||
|
return null;
|
||||||
|
}, [isBusy]);
|
||||||
|
|
||||||
|
Future<void> getToken({String? code}) async {
|
||||||
|
// Get token if challenge is completed
|
||||||
|
final client = ref.watch(apiClientProvider);
|
||||||
|
final tokenResp = await client.post(
|
||||||
|
'/pass/auth/token',
|
||||||
|
data: {
|
||||||
|
'grant_type': 'authorization_code',
|
||||||
|
'code': code ?? challenge!.id,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
final token = tokenResp.data['token'];
|
||||||
|
setToken(ref.watch(sharedPreferencesProvider), token);
|
||||||
|
ref.invalidate(tokenProvider);
|
||||||
|
if (!context.mounted) return;
|
||||||
|
|
||||||
|
// Do post login tasks
|
||||||
|
final userNotifier = ref.read(userInfoProvider.notifier);
|
||||||
|
userNotifier.fetchUser().then((_) {
|
||||||
|
final apiClient = ref.read(apiClientProvider);
|
||||||
|
subscribePushNotification(apiClient);
|
||||||
|
final wsNotifier = ref.read(websocketStateProvider.notifier);
|
||||||
|
wsNotifier.connect();
|
||||||
|
if (context.mounted) Navigator.pop(context, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
if (challenge != null && challenge?.stepRemain == 0) {
|
||||||
|
Future(() {
|
||||||
|
if (isBusy.value) return;
|
||||||
|
isBusy.value = true;
|
||||||
|
getToken().catchError((err) {
|
||||||
|
showErrorAlert(err);
|
||||||
|
isBusy.value = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [challenge]);
|
||||||
|
|
||||||
|
if (factor == null) {
|
||||||
|
// Logging in by third parties
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: CircleAvatar(
|
||||||
|
radius: 26,
|
||||||
|
child: const Icon(Symbols.asterisk, size: 28),
|
||||||
|
).padding(bottom: 8),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'loginInProgress'.tr(),
|
||||||
|
style: const TextStyle(fontSize: 28, fontWeight: FontWeight.w900),
|
||||||
|
).padding(left: 4, bottom: 16),
|
||||||
|
const Gap(16),
|
||||||
|
CircularProgressIndicator().alignment(Alignment.centerLeft),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> performCheckTicket() async {
|
||||||
|
final pwd = passwordController.value.text;
|
||||||
|
if (pwd.isEmpty) return;
|
||||||
|
isBusy.value = true;
|
||||||
|
try {
|
||||||
|
// Pass challenge
|
||||||
|
final client = ref.watch(apiClientProvider);
|
||||||
|
final resp = await client.patch(
|
||||||
|
'/pass/auth/challenge/${challenge!.id}',
|
||||||
|
data: {'factor_id': factor!.id, 'password': pwd},
|
||||||
|
);
|
||||||
|
final result = SnAuthChallenge.fromJson(resp.data);
|
||||||
|
onChallenge(result);
|
||||||
|
if (result.stepRemain > 0) {
|
||||||
|
onNext();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await getToken(code: result.id);
|
||||||
|
} catch (err) {
|
||||||
|
showErrorAlert(err);
|
||||||
|
return;
|
||||||
|
} finally {
|
||||||
|
isBusy.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final width = math.min(380, MediaQuery.of(context).size.width);
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: CircleAvatar(
|
||||||
|
radius: 26,
|
||||||
|
child: const Icon(Symbols.asterisk, size: 28),
|
||||||
|
).padding(bottom: 8),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'loginEnterPassword'.tr(),
|
||||||
|
style: const TextStyle(fontSize: 28, fontWeight: FontWeight.w900),
|
||||||
|
).padding(left: 4, bottom: 16),
|
||||||
|
if ([0].contains(factor!.type))
|
||||||
|
TextField(
|
||||||
|
autocorrect: false,
|
||||||
|
enableSuggestions: false,
|
||||||
|
controller: passwordController,
|
||||||
|
obscureText: true,
|
||||||
|
autofillHints: [
|
||||||
|
factor!.type == 0
|
||||||
|
? AutofillHints.password
|
||||||
|
: AutofillHints.oneTimeCode,
|
||||||
|
],
|
||||||
|
decoration: InputDecoration(
|
||||||
|
isDense: true,
|
||||||
|
labelText: 'password'.tr(),
|
||||||
|
),
|
||||||
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
onSubmitted: isBusy.value ? null : (_) => performCheckTicket(),
|
||||||
|
).padding(horizontal: 7)
|
||||||
|
else
|
||||||
|
OtpTextField(
|
||||||
|
showCursor: false,
|
||||||
|
numberOfFields: 6,
|
||||||
|
obscureText: false,
|
||||||
|
showFieldAsBox: true,
|
||||||
|
focusedBorderColor: Theme.of(context).colorScheme.primary,
|
||||||
|
fieldWidth: (width / 6) - 10,
|
||||||
|
onSubmit: (value) {
|
||||||
|
passwordController.text = value;
|
||||||
|
performCheckTicket();
|
||||||
|
},
|
||||||
|
textStyle: Theme.of(context).textTheme.titleLarge!,
|
||||||
|
),
|
||||||
|
const Gap(12),
|
||||||
|
ListTile(
|
||||||
|
leading: Icon(
|
||||||
|
kFactorTypes[factor!.type]?.$3 ?? Symbols.question_mark,
|
||||||
|
),
|
||||||
|
title: Text(kFactorTypes[factor!.type]?.$1 ?? 'unknown').tr(),
|
||||||
|
subtitle: Text(kFactorTypes[factor!.type]?.$2 ?? 'unknown').tr(),
|
||||||
|
),
|
||||||
|
const Gap(12),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: isBusy.value ? null : () => performCheckTicket(),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text('next').tr(),
|
||||||
|
const Icon(Symbols.chevron_right),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoginContent extends HookConsumerWidget {
|
||||||
|
const LoginContent({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final isBusy = useState(false);
|
||||||
|
|
||||||
|
final period = useState(0);
|
||||||
|
final currentTicket = useState<SnAuthChallenge?>(null);
|
||||||
|
final factors = useState<List<SnAuthFactor>>([]);
|
||||||
|
final factorPicked = useState<SnAuthFactor?>(null);
|
||||||
|
|
||||||
|
return Theme(
|
||||||
|
data: Theme.of(context).copyWith(canvasColor: Colors.transparent),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
if (isBusy.value)
|
||||||
|
LinearProgressIndicator(
|
||||||
|
minHeight: 4,
|
||||||
|
borderRadius: BorderRadius.zero,
|
||||||
|
trackGap: 0,
|
||||||
|
stopIndicatorRadius: 0,
|
||||||
|
)
|
||||||
|
else if (currentTicket.value != null)
|
||||||
|
LinearProgressIndicator(
|
||||||
|
minHeight: 4,
|
||||||
|
borderRadius: BorderRadius.zero,
|
||||||
|
trackGap: 0,
|
||||||
|
stopIndicatorRadius: 0,
|
||||||
|
value:
|
||||||
|
1 -
|
||||||
|
(currentTicket.value!.stepRemain /
|
||||||
|
currentTicket.value!.stepTotal),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
const Gap(4),
|
||||||
|
Expanded(
|
||||||
|
child:
|
||||||
|
SingleChildScrollView(
|
||||||
|
child: PageTransitionSwitcher(
|
||||||
|
transitionBuilder: (
|
||||||
|
Widget child,
|
||||||
|
Animation<double> primaryAnimation,
|
||||||
|
Animation<double> secondaryAnimation,
|
||||||
|
) {
|
||||||
|
return SharedAxisTransition(
|
||||||
|
animation: primaryAnimation,
|
||||||
|
secondaryAnimation: secondaryAnimation,
|
||||||
|
transitionType: SharedAxisTransitionType.horizontal,
|
||||||
|
child: Container(
|
||||||
|
constraints: BoxConstraints(maxWidth: 380),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: switch (period.value % 3) {
|
||||||
|
1 => _LoginPickerScreen(
|
||||||
|
key: const ValueKey(1),
|
||||||
|
challenge: currentTicket.value,
|
||||||
|
factors: factors.value,
|
||||||
|
onChallenge:
|
||||||
|
(SnAuthChallenge? p0) => currentTicket.value = p0,
|
||||||
|
onPickFactor:
|
||||||
|
(SnAuthFactor p0) => factorPicked.value = p0,
|
||||||
|
onNext: () => period.value++,
|
||||||
|
onBusy: (value) => isBusy.value = value,
|
||||||
|
),
|
||||||
|
2 => _LoginCheckScreen(
|
||||||
|
key: const ValueKey(2),
|
||||||
|
challenge: currentTicket.value,
|
||||||
|
factor: factorPicked.value,
|
||||||
|
onChallenge:
|
||||||
|
(SnAuthChallenge? p0) => currentTicket.value = p0,
|
||||||
|
onNext: () => period.value = 1,
|
||||||
|
onBusy: (value) => isBusy.value = value,
|
||||||
|
),
|
||||||
|
_ => _LoginLookupScreen(
|
||||||
|
key: const ValueKey(0),
|
||||||
|
ticket: currentTicket.value,
|
||||||
|
onChallenge:
|
||||||
|
(SnAuthChallenge? p0) => currentTicket.value = p0,
|
||||||
|
onFactor:
|
||||||
|
(List<SnAuthFactor>? p0) =>
|
||||||
|
factors.value = p0 ?? [],
|
||||||
|
onNext: () => period.value++,
|
||||||
|
onBusy: (value) => isBusy.value = value,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
).padding(all: 24),
|
||||||
|
).center(),
|
||||||
|
),
|
||||||
|
|
||||||
|
const Gap(4),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LoginPickerScreen extends HookConsumerWidget {
|
||||||
|
final SnAuthChallenge? challenge;
|
||||||
|
final List<SnAuthFactor>? factors;
|
||||||
|
final Function(SnAuthChallenge?) onChallenge;
|
||||||
|
final Function(SnAuthFactor) onPickFactor;
|
||||||
|
final VoidCallback onNext;
|
||||||
|
final Function(bool) onBusy;
|
||||||
|
|
||||||
|
const _LoginPickerScreen({
|
||||||
|
super.key,
|
||||||
|
required this.challenge,
|
||||||
|
required this.factors,
|
||||||
|
required this.onChallenge,
|
||||||
|
required this.onPickFactor,
|
||||||
|
required this.onNext,
|
||||||
|
required this.onBusy,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final isBusy = useState(false);
|
||||||
|
final factorPicked = useState<SnAuthFactor?>(null);
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
onBusy.call(isBusy.value);
|
||||||
|
return null;
|
||||||
|
}, [isBusy]);
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
if (challenge != null && challenge?.stepRemain == 0) {
|
||||||
|
Future(() {
|
||||||
|
onNext();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [challenge]);
|
||||||
|
|
||||||
|
final unfocusColor = Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.onSurface.withAlpha((255 * 0.75).round());
|
||||||
|
|
||||||
|
final hintController = useTextEditingController();
|
||||||
|
|
||||||
|
void performGetFactorCode() async {
|
||||||
|
if (factorPicked.value == null) return;
|
||||||
|
|
||||||
|
isBusy.value = true;
|
||||||
|
final client = ref.watch(apiClientProvider);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.post(
|
||||||
|
'/pass/auth/challenge/${challenge!.id}/factors/${factorPicked.value!.id}',
|
||||||
|
data:
|
||||||
|
hintController.text.isNotEmpty
|
||||||
|
? jsonEncode(hintController.text)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
onPickFactor(factors!.where((x) => x == factorPicked.value).first);
|
||||||
|
onNext();
|
||||||
|
} catch (err) {
|
||||||
|
if (err is DioException && err.response?.statusCode == 400) {
|
||||||
|
onPickFactor(factors!.where((x) => x == factorPicked.value).first);
|
||||||
|
onNext();
|
||||||
|
if (context.mounted) {
|
||||||
|
showSnackBar(err.response!.data.toString());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showErrorAlert(err);
|
||||||
|
return;
|
||||||
|
} finally {
|
||||||
|
isBusy.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
key: const ValueKey<int>(1),
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: CircleAvatar(
|
||||||
|
radius: 26,
|
||||||
|
child: const Icon(Symbols.lock, size: 28),
|
||||||
|
).padding(bottom: 8),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'loginPickFactor'.tr(),
|
||||||
|
style: const TextStyle(fontSize: 28, fontWeight: FontWeight.w900),
|
||||||
|
).padding(left: 4),
|
||||||
|
const Gap(8),
|
||||||
|
Card(
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||||
|
child: Column(
|
||||||
|
children:
|
||||||
|
factors
|
||||||
|
?.map(
|
||||||
|
(x) => CheckboxListTile(
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||||
|
),
|
||||||
|
secondary: Icon(
|
||||||
|
kFactorTypes[x.type]?.$3 ?? Symbols.question_mark,
|
||||||
|
),
|
||||||
|
title: Text(kFactorTypes[x.type]?.$1 ?? 'unknown').tr(),
|
||||||
|
enabled: !challenge!.blacklistFactors.contains(x.id),
|
||||||
|
value: factorPicked.value == x,
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value == true) {
|
||||||
|
factorPicked.value = x;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList() ??
|
||||||
|
List.empty(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if ([1].contains(factorPicked.value?.type))
|
||||||
|
TextField(
|
||||||
|
controller: hintController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
isDense: true,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
labelText: 'authFactorHint'.tr(),
|
||||||
|
helperText: 'authFactorHintHelper'.tr(),
|
||||||
|
),
|
||||||
|
).padding(top: 12, bottom: 4, horizontal: 4),
|
||||||
|
const Gap(8),
|
||||||
|
Text(
|
||||||
|
'loginMultiFactor'.plural(challenge!.stepRemain),
|
||||||
|
style: TextStyle(color: unfocusColor, fontSize: 13),
|
||||||
|
).padding(horizontal: 16),
|
||||||
|
const Gap(12),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: isBusy.value ? null : () => performGetFactorCode(),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text('next'.tr()),
|
||||||
|
const Icon(Symbols.chevron_right),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LoginLookupScreen extends HookConsumerWidget {
|
||||||
|
final SnAuthChallenge? ticket;
|
||||||
|
final Function(SnAuthChallenge?) onChallenge;
|
||||||
|
final Function(List<SnAuthFactor>?) onFactor;
|
||||||
|
final VoidCallback onNext;
|
||||||
|
final Function(bool) onBusy;
|
||||||
|
|
||||||
|
const _LoginLookupScreen({
|
||||||
|
super.key,
|
||||||
|
required this.ticket,
|
||||||
|
required this.onChallenge,
|
||||||
|
required this.onFactor,
|
||||||
|
required this.onNext,
|
||||||
|
required this.onBusy,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final isBusy = useState(false);
|
||||||
|
final usernameController = useTextEditingController();
|
||||||
|
final waitingForOidc = useState(false);
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
onBusy.call(isBusy.value);
|
||||||
|
return null;
|
||||||
|
}, [isBusy]);
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
final subscription = eventBus.on<OidcAuthCallbackEvent>().listen((
|
||||||
|
event,
|
||||||
|
) async {
|
||||||
|
if (!waitingForOidc.value || !context.mounted) return;
|
||||||
|
waitingForOidc.value = false;
|
||||||
|
final client = ref.watch(apiClientProvider);
|
||||||
|
try {
|
||||||
|
final resp = await client.get(
|
||||||
|
'/pass/auth/challenge/${event.challengeId}',
|
||||||
|
);
|
||||||
|
final challenge = SnAuthChallenge.fromJson(resp.data);
|
||||||
|
onChallenge(challenge);
|
||||||
|
final factorResp = await client.get(
|
||||||
|
'/pass/auth/challenge/${challenge.id}/factors',
|
||||||
|
);
|
||||||
|
onFactor(
|
||||||
|
List<SnAuthFactor>.from(
|
||||||
|
factorResp.data.map((ele) => SnAuthFactor.fromJson(ele)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
onNext();
|
||||||
|
} catch (err) {
|
||||||
|
showErrorAlert(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return subscription.cancel;
|
||||||
|
}, [waitingForOidc.value, context.mounted]);
|
||||||
|
|
||||||
|
Future<void> requestResetPassword() async {
|
||||||
|
final uname = usernameController.value.text;
|
||||||
|
if (uname.isEmpty) {
|
||||||
|
showErrorAlert('loginResetPasswordHint'.tr());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final captchaTk = await CaptchaScreen.show(context);
|
||||||
|
if (captchaTk == null) return;
|
||||||
|
isBusy.value = true;
|
||||||
|
try {
|
||||||
|
final client = ref.watch(apiClientProvider);
|
||||||
|
await client.post(
|
||||||
|
'/pass/accounts/recovery/password',
|
||||||
|
data: {'account': uname, 'captcha_token': captchaTk},
|
||||||
|
);
|
||||||
|
showInfoAlert('loginResetPasswordSent'.tr(), 'done'.tr());
|
||||||
|
} catch (err) {
|
||||||
|
showErrorAlert(err);
|
||||||
|
} finally {
|
||||||
|
isBusy.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> performNewTicket() async {
|
||||||
|
final uname = usernameController.value.text;
|
||||||
|
if (uname.isEmpty) return;
|
||||||
|
isBusy.value = true;
|
||||||
|
try {
|
||||||
|
final client = ref.watch(apiClientProvider);
|
||||||
|
final resp = await client.post(
|
||||||
|
'/pass/auth/challenge',
|
||||||
|
data: {
|
||||||
|
'account': uname,
|
||||||
|
'device_id': await getUdid(),
|
||||||
|
'device_name': await getDeviceName(),
|
||||||
|
'platform':
|
||||||
|
kIsWeb
|
||||||
|
? 1
|
||||||
|
: switch (defaultTargetPlatform) {
|
||||||
|
TargetPlatform.iOS => 2,
|
||||||
|
TargetPlatform.android => 3,
|
||||||
|
TargetPlatform.macOS => 4,
|
||||||
|
TargetPlatform.windows => 5,
|
||||||
|
TargetPlatform.linux => 6,
|
||||||
|
_ => 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
final result = SnAuthChallenge.fromJson(resp.data);
|
||||||
|
onChallenge(result);
|
||||||
|
final factorResp = await client.get(
|
||||||
|
'/pass/auth/challenge/${result.id}/factors',
|
||||||
|
);
|
||||||
|
onFactor(
|
||||||
|
List<SnAuthFactor>.from(
|
||||||
|
factorResp.data.map((ele) => SnAuthFactor.fromJson(ele)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
onNext();
|
||||||
|
} catch (err) {
|
||||||
|
showErrorAlert(err);
|
||||||
|
return;
|
||||||
|
} finally {
|
||||||
|
isBusy.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> withApple() async {
|
||||||
|
final client = ref.watch(apiClientProvider);
|
||||||
|
try {
|
||||||
|
final credential = await SignInWithApple.getAppleIDCredential(
|
||||||
|
scopes: [AppleIDAuthorizationScopes.email],
|
||||||
|
webAuthenticationOptions: WebAuthenticationOptions(
|
||||||
|
clientId: 'dev.solsynth.solarpass',
|
||||||
|
redirectUri: Uri.parse('https://nt.solian.app/auth/callback/apple'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (context.mounted) showLoadingModal(context);
|
||||||
|
final resp = await client.post(
|
||||||
|
'/pass/auth/login/apple/mobile',
|
||||||
|
data: {
|
||||||
|
'identity_token': credential.identityToken!,
|
||||||
|
'authorization_code': credential.authorizationCode,
|
||||||
|
'device_id': await getUdid(),
|
||||||
|
'device_name': await getDeviceName(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
final challenge = SnAuthChallenge.fromJson(resp.data);
|
||||||
|
onChallenge(challenge);
|
||||||
|
final factorResp = await client.get(
|
||||||
|
'/pass/auth/challenge/${challenge.id}/factors',
|
||||||
|
);
|
||||||
|
onFactor(
|
||||||
|
List<SnAuthFactor>.from(
|
||||||
|
factorResp.data.map((ele) => SnAuthFactor.fromJson(ele)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
onNext();
|
||||||
|
} catch (err) {
|
||||||
|
if (err is SignInWithAppleAuthorizationException) return;
|
||||||
|
showErrorAlert(err);
|
||||||
|
} finally {
|
||||||
|
if (context.mounted) hideLoadingModal(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> withOidc(String provider) async {
|
||||||
|
waitingForOidc.value = true;
|
||||||
|
final serverUrl = ref.watch(serverUrlProvider);
|
||||||
|
final token = ref.watch(tokenProvider);
|
||||||
|
final deviceId = await getUdid();
|
||||||
|
final queryParams = <String, String>{
|
||||||
|
'returnUrl': 'solian://auth/callback',
|
||||||
|
'deviceId': deviceId,
|
||||||
|
'flow': 'login',
|
||||||
|
};
|
||||||
|
if (token?.token != null) {
|
||||||
|
queryParams['token'] = token!.token;
|
||||||
|
}
|
||||||
|
final url =
|
||||||
|
Uri.parse(
|
||||||
|
'$serverUrl/pass/auth/login/${provider.toLowerCase()}',
|
||||||
|
).replace(queryParameters: queryParams).toString();
|
||||||
|
final isLaunched = await launchUrlString(
|
||||||
|
url,
|
||||||
|
mode:
|
||||||
|
kIsWeb
|
||||||
|
? LaunchMode.platformDefault
|
||||||
|
: LaunchMode.externalApplication,
|
||||||
|
webOnlyWindowName:
|
||||||
|
token?.token != null ? 'auth-${token!.token}' : 'auth',
|
||||||
|
);
|
||||||
|
if (!isLaunched) {
|
||||||
|
waitingForOidc.value = false;
|
||||||
|
showErrorAlert('failedToLaunchBrowser'.tr());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: CircleAvatar(
|
||||||
|
radius: 26,
|
||||||
|
child: const Icon(Symbols.login, size: 28),
|
||||||
|
).padding(bottom: 8),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'loginGreeting'.tr(),
|
||||||
|
style: const TextStyle(fontSize: 28, fontWeight: FontWeight.w900),
|
||||||
|
).padding(left: 4, bottom: 16),
|
||||||
|
TextField(
|
||||||
|
autocorrect: false,
|
||||||
|
enableSuggestions: false,
|
||||||
|
controller: usernameController,
|
||||||
|
autofillHints: const [AutofillHints.username],
|
||||||
|
decoration: InputDecoration(
|
||||||
|
isDense: true,
|
||||||
|
border: const UnderlineInputBorder(),
|
||||||
|
labelText: 'username'.tr(),
|
||||||
|
helperText: 'usernameLookupHint'.tr(),
|
||||||
|
),
|
||||||
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
onSubmitted: isBusy.value ? null : (_) => performNewTicket(),
|
||||||
|
).padding(horizontal: 7),
|
||||||
|
if (!kIsWeb)
|
||||||
|
Row(
|
||||||
|
spacing: 6,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Text("loginOr").tr().fontSize(11).opacity(0.85),
|
||||||
|
const Gap(8),
|
||||||
|
Spacer(),
|
||||||
|
IconButton.filledTonal(
|
||||||
|
onPressed: () => withOidc('github'),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
icon: getProviderIcon(
|
||||||
|
"github",
|
||||||
|
size: 16,
|
||||||
|
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||||
|
),
|
||||||
|
tooltip: 'GitHub',
|
||||||
|
),
|
||||||
|
IconButton.filledTonal(
|
||||||
|
onPressed: () => withOidc('google'),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
icon: getProviderIcon(
|
||||||
|
"google",
|
||||||
|
size: 16,
|
||||||
|
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||||
|
),
|
||||||
|
tooltip: 'Google',
|
||||||
|
),
|
||||||
|
IconButton.filledTonal(
|
||||||
|
onPressed: withApple,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
icon: getProviderIcon(
|
||||||
|
"apple",
|
||||||
|
size: 16,
|
||||||
|
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||||
|
),
|
||||||
|
tooltip: 'Apple Account',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 8, vertical: 8)
|
||||||
|
else
|
||||||
|
const Gap(12),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: isBusy.value ? null : () => requestResetPassword(),
|
||||||
|
style: TextButton.styleFrom(foregroundColor: Colors.grey),
|
||||||
|
child: Text('forgotPassword'.tr()),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: isBusy.value ? null : () => performNewTicket(),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text('next').tr(),
|
||||||
|
const Icon(Symbols.chevron_right),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Gap(12),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: StyledWidget(
|
||||||
|
Container(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 290),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'termAcceptNextWithAgree'.tr(),
|
||||||
|
textAlign: TextAlign.end,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||||
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.onSurface.withAlpha((255 * 0.75).round()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text('termAcceptLink'.tr()),
|
||||||
|
const Gap(4),
|
||||||
|
const Icon(Symbols.launch, size: 14),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
launchUrlString('https://solsynth.dev/terms');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).padding(horizontal: 16),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
lib/screens/auth/login_modal.dart
Normal file
19
lib/screens/auth/login_modal.dart
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:island/widgets/content/sheet.dart';
|
||||||
|
|
||||||
|
import 'login_content.dart';
|
||||||
|
|
||||||
|
class LoginModal extends HookConsumerWidget {
|
||||||
|
const LoginModal({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return SheetScaffold(
|
||||||
|
titleText: 'login'.tr(),
|
||||||
|
heightFactor: 0.9,
|
||||||
|
child: LoginContent(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,30 +3,31 @@ import 'package:flutter/material.dart' hide ConnectionState;
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:island/models/chat.dart';
|
||||||
import 'package:island/pods/chat/call.dart';
|
import 'package:island/pods/chat/call.dart';
|
||||||
import 'package:island/talker.dart';
|
import 'package:island/talker.dart';
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
import 'package:island/widgets/chat/call_button.dart';
|
import 'package:island/widgets/chat/call_button.dart';
|
||||||
|
import 'package:island/widgets/chat/call_content.dart';
|
||||||
import 'package:island/widgets/chat/call_overlay.dart';
|
import 'package:island/widgets/chat/call_overlay.dart';
|
||||||
import 'package:island/widgets/chat/call_participant_tile.dart';
|
import 'package:island/widgets/chat/call_participant_tile.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
import 'package:livekit_client/livekit_client.dart';
|
import 'package:livekit_client/livekit_client.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
|
||||||
|
|
||||||
class CallScreen extends HookConsumerWidget {
|
class CallScreen extends HookConsumerWidget {
|
||||||
final String roomId;
|
final SnChatRoom room;
|
||||||
const CallScreen({super.key, required this.roomId});
|
const CallScreen({super.key, required this.room});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final ongoingCall = ref.watch(ongoingCallProvider(roomId));
|
final ongoingCall = ref.watch(ongoingCallProvider(room.id));
|
||||||
final callState = ref.watch(callNotifierProvider);
|
final callState = ref.watch(callNotifierProvider);
|
||||||
final callNotifier = ref.watch(callNotifierProvider.notifier);
|
final callNotifier = ref.watch(callNotifierProvider.notifier);
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
talker.info('[Call] Joining the call...');
|
talker.info('[Call] Joining the call...');
|
||||||
callNotifier.joinRoom(roomId).catchError((_) {
|
callNotifier.joinRoom(room).catchError((_) {
|
||||||
showConfirmAlert(
|
showConfirmAlert(
|
||||||
'Seems there already has a call connected, do you want override it?',
|
'Seems there already has a call connected, do you want override it?',
|
||||||
'Call already connected',
|
'Call already connected',
|
||||||
@@ -35,7 +36,7 @@ class CallScreen extends HookConsumerWidget {
|
|||||||
talker.info('[Call] Joining the call... with overrides');
|
talker.info('[Call] Joining the call... with overrides');
|
||||||
callNotifier.disconnect();
|
callNotifier.disconnect();
|
||||||
callNotifier.dispose();
|
callNotifier.dispose();
|
||||||
callNotifier.joinRoom(roomId);
|
callNotifier.joinRoom(room);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return null;
|
return null;
|
||||||
@@ -110,7 +111,7 @@ class CallScreen extends HookConsumerWidget {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
callNotifier.disconnect();
|
callNotifier.disconnect();
|
||||||
callNotifier.dispose();
|
callNotifier.dispose();
|
||||||
callNotifier.joinRoom(roomId);
|
callNotifier.joinRoom(room);
|
||||||
},
|
},
|
||||||
child: Text('retry').tr(),
|
child: Text('retry').tr(),
|
||||||
),
|
),
|
||||||
@@ -120,72 +121,7 @@ class CallScreen extends HookConsumerWidget {
|
|||||||
)
|
)
|
||||||
: Column(
|
: Column(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(child: CallContent()),
|
||||||
child: Builder(
|
|
||||||
builder: (context) {
|
|
||||||
if (!callState.isConnected) {
|
|
||||||
return const Center(
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (callNotifier.participants.isEmpty) {
|
|
||||||
return const Center(
|
|
||||||
child: Text('No participants in call'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final participants = callNotifier.participants;
|
|
||||||
if (allAudioOnly) {
|
|
||||||
// Audio-only: show avatars in a compact row
|
|
||||||
return Center(
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
child: Wrap(
|
|
||||||
crossAxisAlignment: WrapCrossAlignment.center,
|
|
||||||
alignment: WrapAlignment.center,
|
|
||||||
spacing: 8,
|
|
||||||
runSpacing: 8,
|
|
||||||
children: [
|
|
||||||
for (final live in participants)
|
|
||||||
SpeakingRippleAvatar(
|
|
||||||
live: live,
|
|
||||||
size: 72,
|
|
||||||
).padding(horizontal: 4),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stage view: show main speaker(s) large, others in row
|
|
||||||
final mainSpeakers =
|
|
||||||
participants
|
|
||||||
.where(
|
|
||||||
(p) => p
|
|
||||||
.remoteParticipant
|
|
||||||
.trackPublications
|
|
||||||
.values
|
|
||||||
.any(
|
|
||||||
(pub) =>
|
|
||||||
pub.track != null &&
|
|
||||||
pub.kind == TrackType.VIDEO,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
if (mainSpeakers.isEmpty && participants.isNotEmpty) {
|
|
||||||
mainSpeakers.add(participants.first);
|
|
||||||
}
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
for (final speaker in mainSpeakers)
|
|
||||||
Expanded(
|
|
||||||
child: CallParticipantTile(live: speaker),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
CallControlsBar(),
|
CallControlsBar(),
|
||||||
Gap(MediaQuery.of(context).padding.bottom + 16),
|
Gap(MediaQuery.of(context).padding.bottom + 16),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -6,9 +8,13 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/chat.dart';
|
import 'package:island/models/chat.dart';
|
||||||
|
import 'package:island/models/file.dart';
|
||||||
|
import 'package:island/models/account.dart';
|
||||||
|
import 'package:island/pods/database.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/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
|
import 'package:island/pods/userinfo.dart';
|
||||||
import 'package:island/screens/realm/realms.dart';
|
import 'package:island/screens/realm/realms.dart';
|
||||||
import 'package:island/services/event_bus.dart';
|
import 'package:island/services/event_bus.dart';
|
||||||
import 'package:island/services/responsive.dart';
|
import 'package:island/services/responsive.dart';
|
||||||
@@ -47,6 +53,17 @@ class ChatRoomListTile extends HookConsumerWidget {
|
|||||||
.watch(chatSummaryProvider)
|
.watch(chatSummaryProvider)
|
||||||
.whenData((summaries) => summaries[room.id]);
|
.whenData((summaries) => summaries[room.id]);
|
||||||
|
|
||||||
|
var validMembers = room.members ?? [];
|
||||||
|
if (validMembers.isNotEmpty) {
|
||||||
|
final userInfo = ref.watch(userInfoProvider);
|
||||||
|
if (userInfo.value != null) {
|
||||||
|
validMembers =
|
||||||
|
validMembers
|
||||||
|
.where((e) => e.accountId != userInfo.value!.id)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Widget buildSubtitle() {
|
Widget buildSubtitle() {
|
||||||
if (subtitle != null) return subtitle!;
|
if (subtitle != null) return subtitle!;
|
||||||
|
|
||||||
@@ -55,7 +72,7 @@ class ChatRoomListTile extends HookConsumerWidget {
|
|||||||
if (data == null) {
|
if (data == null) {
|
||||||
return isDirect && room.description == null
|
return isDirect && room.description == null
|
||||||
? Text(
|
? Text(
|
||||||
room.members!.map((e) => '@${e.account.name}').join(', '),
|
validMembers.map((e) => '@${e.account.name}').join(', '),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
)
|
)
|
||||||
: Text(room.description ?? 'descriptionNone'.tr(), maxLines: 1);
|
: Text(room.description ?? 'descriptionNone'.tr(), maxLines: 1);
|
||||||
@@ -111,7 +128,7 @@ class ChatRoomListTile extends HookConsumerWidget {
|
|||||||
(_, _) =>
|
(_, _) =>
|
||||||
isDirect && room.description == null
|
isDirect && room.description == null
|
||||||
? Text(
|
? Text(
|
||||||
room.members!.map((e) => '@${e.account.name}').join(', '),
|
validMembers.map((e) => '@${e.account.name}').join(', '),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
)
|
)
|
||||||
: Text(
|
: Text(
|
||||||
@@ -121,6 +138,17 @@ class ChatRoomListTile extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String titleText;
|
||||||
|
if (isDirect && room.name == null) {
|
||||||
|
if (room.members?.isNotEmpty ?? false) {
|
||||||
|
titleText = validMembers.map((e) => e.account.nick).join(', ');
|
||||||
|
} else {
|
||||||
|
titleText = 'Direct Message';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
titleText = room.name ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: Badge(
|
leading: Badge(
|
||||||
isLabelVisible: summary.when(
|
isLabelVisible: summary.when(
|
||||||
@@ -132,7 +160,7 @@ class ChatRoomListTile extends HookConsumerWidget {
|
|||||||
(isDirect && room.picture?.id == null)
|
(isDirect && room.picture?.id == null)
|
||||||
? SplitAvatarWidget(
|
? SplitAvatarWidget(
|
||||||
filesId:
|
filesId:
|
||||||
room.members!
|
validMembers
|
||||||
.map((e) => e.account.profile.picture?.id)
|
.map((e) => e.account.profile.picture?.id)
|
||||||
.toList(),
|
.toList(),
|
||||||
)
|
)
|
||||||
@@ -140,11 +168,7 @@ class ChatRoomListTile extends HookConsumerWidget {
|
|||||||
? CircleAvatar(child: Text(room.name![0].toUpperCase()))
|
? CircleAvatar(child: Text(room.name![0].toUpperCase()))
|
||||||
: ProfilePictureWidget(fileId: room.picture?.id),
|
: ProfilePictureWidget(fileId: room.picture?.id),
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(titleText),
|
||||||
(isDirect && room.name == null)
|
|
||||||
? room.members!.map((e) => e.account.nick).join(', ')
|
|
||||||
: room.name ?? '',
|
|
||||||
),
|
|
||||||
subtitle: buildSubtitle(),
|
subtitle: buildSubtitle(),
|
||||||
trailing: trailing, // Add this line
|
trailing: trailing, // Add this line
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
@@ -162,12 +186,92 @@ class ChatRoomListTile extends HookConsumerWidget {
|
|||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
Future<List<SnChatRoom>> chatroomsJoined(Ref ref) async {
|
Future<List<SnChatRoom>> chatroomsJoined(Ref ref) async {
|
||||||
|
final db = ref.watch(databaseProvider);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final localRoomsData = await db.select(db.chatRooms).get();
|
||||||
|
if (localRoomsData.isNotEmpty) {
|
||||||
|
final localRooms = await Future.wait(
|
||||||
|
localRoomsData.map((row) async {
|
||||||
|
final membersRows =
|
||||||
|
await (db.select(db.chatMembers)
|
||||||
|
..where((m) => m.chatRoomId.equals(row.id))).get();
|
||||||
|
final members =
|
||||||
|
membersRows.map((mRow) {
|
||||||
|
final account = SnAccount.fromJson(mRow.account);
|
||||||
|
SnAccountStatus? status;
|
||||||
|
if (mRow.status != null) {
|
||||||
|
status = SnAccountStatus.fromJson(jsonDecode(mRow.status!));
|
||||||
|
}
|
||||||
|
return SnChatMember(
|
||||||
|
id: mRow.id,
|
||||||
|
chatRoomId: mRow.chatRoomId,
|
||||||
|
accountId: mRow.accountId,
|
||||||
|
account: account,
|
||||||
|
nick: mRow.nick,
|
||||||
|
role: mRow.role,
|
||||||
|
notify: mRow.notify,
|
||||||
|
joinedAt: mRow.joinedAt,
|
||||||
|
breakUntil: mRow.breakUntil,
|
||||||
|
timeoutUntil: mRow.timeoutUntil,
|
||||||
|
isBot: mRow.isBot,
|
||||||
|
status: status,
|
||||||
|
lastTyped: mRow.lastTyped,
|
||||||
|
createdAt: mRow.createdAt,
|
||||||
|
updatedAt: mRow.updatedAt,
|
||||||
|
deletedAt: mRow.deletedAt,
|
||||||
|
chatRoom: null,
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
return SnChatRoom(
|
||||||
|
id: row.id,
|
||||||
|
name: row.name,
|
||||||
|
description: row.description,
|
||||||
|
type: row.type,
|
||||||
|
isPublic: row.isPublic!,
|
||||||
|
isCommunity: row.isCommunity!,
|
||||||
|
picture:
|
||||||
|
row.picture != null ? SnCloudFile.fromJson(row.picture!) : null,
|
||||||
|
background:
|
||||||
|
row.background != null
|
||||||
|
? SnCloudFile.fromJson(row.background!)
|
||||||
|
: null,
|
||||||
|
realmId: row.realmId,
|
||||||
|
realm: null,
|
||||||
|
createdAt: row.createdAt,
|
||||||
|
updatedAt: row.updatedAt,
|
||||||
|
deletedAt: row.deletedAt,
|
||||||
|
members: members,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Background sync
|
||||||
|
Future(() async {
|
||||||
|
try {
|
||||||
|
final client = ref.read(apiClientProvider);
|
||||||
|
final resp = await client.get('/sphere/chat');
|
||||||
|
final remoteRooms =
|
||||||
|
resp.data
|
||||||
|
.map((e) => SnChatRoom.fromJson(e))
|
||||||
|
.cast<SnChatRoom>()
|
||||||
|
.toList();
|
||||||
|
await db.saveChatRooms(remoteRooms);
|
||||||
|
ref.invalidateSelf();
|
||||||
|
} catch (_) {}
|
||||||
|
}).ignore();
|
||||||
|
|
||||||
|
return localRooms;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
// Fallback to API
|
||||||
final client = ref.watch(apiClientProvider);
|
final client = ref.watch(apiClientProvider);
|
||||||
final resp = await client.get('/sphere/chat');
|
final resp = await client.get('/sphere/chat');
|
||||||
return resp.data
|
final rooms =
|
||||||
.map((e) => SnChatRoom.fromJson(e))
|
resp.data.map((e) => SnChatRoom.fromJson(e)).cast<SnChatRoom>().toList();
|
||||||
.cast<SnChatRoom>()
|
await db.saveChatRooms(rooms);
|
||||||
.toList();
|
return rooms;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ChatListBodyWidget extends HookConsumerWidget {
|
class ChatListBodyWidget extends HookConsumerWidget {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ part of 'chat.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$chatroomsJoinedHash() => r'3bb6389af07e81007680484d04bf5fe6f6c10571';
|
String _$chatroomsJoinedHash() => r'9523efecd1869e7dd26adfc8ec87be48db19ee1c';
|
||||||
|
|
||||||
/// See also [chatroomsJoined].
|
/// See also [chatroomsJoined].
|
||||||
@ProviderFor(chatroomsJoined)
|
@ProviderFor(chatroomsJoined)
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ import 'package:island/screens/realm/realms.dart';
|
|||||||
import 'package:island/services/file.dart';
|
import 'package:island/services/file.dart';
|
||||||
import 'package:island/services/file_uploader.dart';
|
import 'package:island/services/file_uploader.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
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:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:island/widgets/content/sheet.dart';
|
||||||
|
|
||||||
class NewChatScreen extends StatelessWidget {
|
class NewChatScreen extends StatelessWidget {
|
||||||
const NewChatScreen({super.key});
|
const NewChatScreen({super.key});
|
||||||
@@ -99,7 +99,7 @@ class EditChatScreen extends HookConsumerWidget {
|
|||||||
try {
|
try {
|
||||||
final cloudFile =
|
final cloudFile =
|
||||||
await FileUploader.createCloudFile(
|
await FileUploader.createCloudFile(
|
||||||
client: ref.read(apiClientProvider),
|
ref: ref,
|
||||||
fileData: UniversalFile(
|
fileData: UniversalFile(
|
||||||
data: result,
|
data: result,
|
||||||
type: UniversalFileType.image,
|
type: UniversalFileType.image,
|
||||||
@@ -151,12 +151,10 @@ class EditChatScreen extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return AppScaffold(
|
return SheetScaffold(
|
||||||
appBar: AppBar(
|
titleText: (id == null ? 'createChatRoom' : 'editChatRoom').tr(),
|
||||||
title: Text(id == null ? 'createChatRoom' : 'editChatRoom').tr(),
|
onClose: () => context.pop(),
|
||||||
leading: const PageBackButton(),
|
child: SingleChildScrollView(
|
||||||
),
|
|
||||||
body: SingleChildScrollView(
|
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
AspectRatio(
|
AspectRatio(
|
||||||
@@ -204,16 +202,24 @@ class EditChatScreen extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: nameController,
|
controller: nameController,
|
||||||
decoration: const InputDecoration(labelText: 'Name'),
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Name',
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
onTapOutside:
|
onTapOutside:
|
||||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: descriptionController,
|
controller: descriptionController,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Description',
|
labelText: 'Description',
|
||||||
alignLabelWithHint: true,
|
alignLabelWithHint: true,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
minLines: 3,
|
minLines: 3,
|
||||||
maxLines: null,
|
maxLines: null,
|
||||||
@@ -223,7 +229,12 @@ class EditChatScreen extends HookConsumerWidget {
|
|||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
DropdownButtonFormField<SnRealm>(
|
DropdownButtonFormField<SnRealm>(
|
||||||
value: currentRealm.value,
|
value: currentRealm.value,
|
||||||
decoration: InputDecoration(labelText: 'realm'.tr()),
|
decoration: InputDecoration(
|
||||||
|
labelText: 'realm'.tr(),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
items: [
|
items: [
|
||||||
DropdownMenuItem<SnRealm>(
|
DropdownMenuItem<SnRealm>(
|
||||||
value: null,
|
value: null,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import "dart:async";
|
|||||||
import "dart:math" as math;
|
import "dart:math" as math;
|
||||||
import "package:easy_localization/easy_localization.dart";
|
import "package:easy_localization/easy_localization.dart";
|
||||||
import "package:file_picker/file_picker.dart";
|
import "package:file_picker/file_picker.dart";
|
||||||
|
import "package:image_picker/image_picker.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:flutter_hooks/flutter_hooks.dart";
|
||||||
@@ -10,6 +11,8 @@ import "package:hooks_riverpod/hooks_riverpod.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/models/poll.dart";
|
||||||
|
import "package:island/models/wallet.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/chat/messages_notifier.dart";
|
import "package:island/pods/chat/messages_notifier.dart";
|
||||||
@@ -36,6 +39,7 @@ import "package:island/widgets/chat/chat_input.dart";
|
|||||||
import "package:island/widgets/chat/chat_link_attachments.dart";
|
import "package:island/widgets/chat/chat_link_attachments.dart";
|
||||||
import "package:island/widgets/chat/public_room_preview.dart";
|
import "package:island/widgets/chat/public_room_preview.dart";
|
||||||
import "package:island/screens/thought/think_sheet.dart";
|
import "package:island/screens/thought/think_sheet.dart";
|
||||||
|
import "package:island/screens/chat/widgets/message_item_wrapper.dart";
|
||||||
|
|
||||||
class ChatRoomScreen extends HookConsumerWidget {
|
class ChatRoomScreen extends HookConsumerWidget {
|
||||||
final String id;
|
final String id;
|
||||||
@@ -141,19 +145,72 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
final messageController = useTextEditingController();
|
final messageController = useTextEditingController();
|
||||||
final scrollController = useScrollController();
|
final scrollController = useScrollController();
|
||||||
|
|
||||||
|
// Input height measurement for dynamic padding
|
||||||
|
final inputKey = useMemoized(() => GlobalKey());
|
||||||
|
final inputHeight = useState<double>(80.0);
|
||||||
|
|
||||||
|
// Periodic height measurement for dynamic sizing
|
||||||
|
useEffect(() {
|
||||||
|
final timer = Timer.periodic(const Duration(milliseconds: 50), (_) {
|
||||||
|
final renderBox =
|
||||||
|
inputKey.currentContext?.findRenderObject() as RenderBox?;
|
||||||
|
if (renderBox != null) {
|
||||||
|
final newHeight = renderBox.size.height;
|
||||||
|
if (newHeight != inputHeight.value) {
|
||||||
|
inputHeight.value = newHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return timer.cancel;
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Scroll animation notifiers
|
// Scroll animation notifiers
|
||||||
final bottomGradientNotifier = useState(ValueNotifier<double>(0.0));
|
final bottomGradientNotifier = useState(ValueNotifier<double>(0.0));
|
||||||
|
|
||||||
final messageReplyingTo = useState<SnChatMessage?>(null);
|
final messageReplyingTo = useState<SnChatMessage?>(null);
|
||||||
final messageForwardingTo = useState<SnChatMessage?>(null);
|
final messageForwardingTo = useState<SnChatMessage?>(null);
|
||||||
final messageEditingTo = useState<SnChatMessage?>(null);
|
final messageEditingTo = useState<SnChatMessage?>(null);
|
||||||
|
final selectedPoll = useState<SnPoll?>(null);
|
||||||
|
final selectedFund = useState<SnWalletFund?>(null);
|
||||||
final attachments = useState<List<UniversalFile>>([]);
|
final attachments = useState<List<UniversalFile>>([]);
|
||||||
final attachmentProgress = useState<Map<String, Map<int, double>>>({});
|
final attachmentProgress = useState<Map<String, Map<int, double?>>>({});
|
||||||
|
|
||||||
// Selection mode state
|
// Selection mode state
|
||||||
final isSelectionMode = useState<bool>(false);
|
final isSelectionMode = useState<bool>(false);
|
||||||
final selectedMessages = useState<Set<String>>({});
|
final selectedMessages = useState<Set<String>>({});
|
||||||
|
|
||||||
|
final roomOpenTime = useMemoized(() => DateTime.now());
|
||||||
|
|
||||||
|
final onMessageAction = useCallback(
|
||||||
|
(String action, LocalChatMessage message) {
|
||||||
|
switch (action) {
|
||||||
|
case MessageItemAction.delete:
|
||||||
|
messagesNotifier.deleteMessage(message.id);
|
||||||
|
case MessageItemAction.edit:
|
||||||
|
messageEditingTo.value = message.toRemoteMessage();
|
||||||
|
messageController.text = messageEditingTo.value?.content ?? '';
|
||||||
|
attachments.value =
|
||||||
|
messageEditingTo.value!.attachments
|
||||||
|
.map((e) => UniversalFile.fromAttachment(e))
|
||||||
|
.toList();
|
||||||
|
case MessageItemAction.forward:
|
||||||
|
messageForwardingTo.value = message.toRemoteMessage();
|
||||||
|
case MessageItemAction.reply:
|
||||||
|
messageReplyingTo.value = message.toRemoteMessage();
|
||||||
|
case MessageItemAction.resend:
|
||||||
|
messagesNotifier.retryMessage(message.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
messagesNotifier,
|
||||||
|
messageEditingTo,
|
||||||
|
messageController,
|
||||||
|
attachments,
|
||||||
|
messageForwardingTo,
|
||||||
|
messageReplyingTo,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
var isLoading = false;
|
var isLoading = false;
|
||||||
var isScrollingToMessage = false; // Flag to prevent scroll conflicts
|
var isScrollingToMessage = false; // Flag to prevent scroll conflicts
|
||||||
|
|
||||||
@@ -181,16 +238,13 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
}, [scrollController]);
|
}, [scrollController]);
|
||||||
|
|
||||||
Future<void> pickPhotoMedia() async {
|
Future<void> pickPhotoMedia() async {
|
||||||
final result = await FilePicker.platform.pickFiles(
|
final ImagePicker picker = ImagePicker();
|
||||||
type: FileType.image,
|
final List<XFile> results = await picker.pickMultiImage();
|
||||||
allowMultiple: true,
|
if (results.isEmpty) return;
|
||||||
allowCompression: false,
|
|
||||||
);
|
|
||||||
if (result == null || result.count == 0) return;
|
|
||||||
attachments.value = [
|
attachments.value = [
|
||||||
...attachments.value,
|
...attachments.value,
|
||||||
...result.files.map(
|
...results.map(
|
||||||
(e) => UniversalFile(data: e.xFile, type: UniversalFileType.image),
|
(xfile) => UniversalFile(data: xfile, type: UniversalFileType.image),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -265,10 +319,15 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
|
|
||||||
void sendMessage() {
|
void sendMessage() {
|
||||||
if (messageController.text.trim().isNotEmpty ||
|
if (messageController.text.trim().isNotEmpty ||
|
||||||
attachments.value.isNotEmpty) {
|
attachments.value.isNotEmpty ||
|
||||||
|
selectedPoll.value != null ||
|
||||||
|
selectedFund.value != null) {
|
||||||
messagesNotifier.sendMessage(
|
messagesNotifier.sendMessage(
|
||||||
|
ref,
|
||||||
messageController.text.trim(),
|
messageController.text.trim(),
|
||||||
attachments.value,
|
attachments.value,
|
||||||
|
poll: selectedPoll.value,
|
||||||
|
fund: selectedFund.value,
|
||||||
editingTo: messageEditingTo.value,
|
editingTo: messageEditingTo.value,
|
||||||
forwardingTo: messageForwardingTo.value,
|
forwardingTo: messageForwardingTo.value,
|
||||||
replyingTo: messageReplyingTo.value,
|
replyingTo: messageReplyingTo.value,
|
||||||
@@ -283,6 +342,8 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
messageEditingTo.value = null;
|
messageEditingTo.value = null;
|
||||||
messageReplyingTo.value = null;
|
messageReplyingTo.value = null;
|
||||||
messageForwardingTo.value = null;
|
messageForwardingTo.value = null;
|
||||||
|
selectedPoll.value = null;
|
||||||
|
selectedFund.value = null;
|
||||||
attachments.value = [];
|
attachments.value = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -563,7 +624,7 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
|
|
||||||
final cloudFile =
|
final cloudFile =
|
||||||
await FileUploader.createCloudFile(
|
await FileUploader.createCloudFile(
|
||||||
client: ref.read(apiClientProvider),
|
ref: ref,
|
||||||
fileData: attachment,
|
fileData: attachment,
|
||||||
poolId: config.poolId,
|
poolId: config.poolId,
|
||||||
mode:
|
mode:
|
||||||
@@ -573,7 +634,7 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
onProgress: (progress, _) {
|
onProgress: (progress, _) {
|
||||||
attachmentProgress.value = {
|
attachmentProgress.value = {
|
||||||
...attachmentProgress.value,
|
...attachmentProgress.value,
|
||||||
'chat-upload': {index: progress},
|
'chat-upload': {index: progress ?? 0.0},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
).future;
|
).future;
|
||||||
@@ -595,180 +656,67 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
|
|
||||||
Widget chatMessageListWidget(
|
Widget chatMessageListWidget(
|
||||||
List<LocalChatMessage> messageList,
|
List<LocalChatMessage> messageList,
|
||||||
) => SuperListView.builder(
|
) => AnimatedPadding(
|
||||||
listController: listController,
|
duration: const Duration(milliseconds: 200),
|
||||||
|
curve: Curves.easeOut,
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
top: 16,
|
bottom: MediaQuery.of(context).padding.bottom + 8 + inputHeight.value,
|
||||||
bottom:
|
|
||||||
MediaQuery.of(context).padding.bottom +
|
|
||||||
80, // Leave space for chat input
|
|
||||||
),
|
),
|
||||||
controller: scrollController,
|
child: SuperListView.builder(
|
||||||
reverse: true, // Show newest messages at the bottom
|
listController: listController,
|
||||||
itemCount: messageList.length,
|
controller: scrollController,
|
||||||
findChildIndexCallback: (key) {
|
reverse: true, // Show newest messages at the bottom
|
||||||
if (key is! ValueKey<String>) return null;
|
itemCount: messageList.length,
|
||||||
final messageId = key.value.substring(messageKeyPrefix.length);
|
findChildIndexCallback: (key) {
|
||||||
final index = messageList.indexWhere(
|
if (key is! ValueKey<String>) return null;
|
||||||
(m) => (m.nonce ?? m.id) == messageId,
|
final messageId = key.value.substring(messageKeyPrefix.length);
|
||||||
);
|
final index = messageList.indexWhere(
|
||||||
// Return null for invalid indices to let SuperListView handle it properly
|
(m) => (m.nonce ?? m.id) == messageId,
|
||||||
return index >= 0 ? index : null;
|
);
|
||||||
},
|
return index >= 0 ? index : null;
|
||||||
extentEstimation: (_, _) => 40,
|
},
|
||||||
itemBuilder: (context, index) {
|
extentEstimation: (_, _) => 40,
|
||||||
final message = messageList[index];
|
itemBuilder: (context, index) {
|
||||||
final nextMessage =
|
final message = messageList[index];
|
||||||
index < messageList.length - 1 ? messageList[index + 1] : null;
|
final nextMessage =
|
||||||
final isLastInGroup =
|
index < messageList.length - 1 ? messageList[index + 1] : null;
|
||||||
nextMessage == null ||
|
final isLastInGroup =
|
||||||
nextMessage.senderId != message.senderId ||
|
nextMessage == null ||
|
||||||
nextMessage.createdAt
|
nextMessage.senderId != message.senderId ||
|
||||||
.difference(message.createdAt)
|
nextMessage.createdAt
|
||||||
.inMinutes
|
.difference(message.createdAt)
|
||||||
.abs() >
|
.inMinutes
|
||||||
3;
|
.abs() >
|
||||||
|
3;
|
||||||
|
|
||||||
// Use a stable animation key that doesn't change during message lifecycle
|
final key = Key('$messageKeyPrefix${message.nonce ?? message.id}');
|
||||||
final key = Key('$messageKeyPrefix${message.nonce ?? message.id}');
|
|
||||||
|
|
||||||
final messageWidget = chatIdentity.when(
|
return MessageItemWrapper(
|
||||||
skipError: true,
|
key: key,
|
||||||
data:
|
message: message,
|
||||||
(identity) => GestureDetector(
|
index: index,
|
||||||
onLongPress: () {
|
isLastInGroup: isLastInGroup,
|
||||||
if (!isSelectionMode.value) {
|
isSelectionMode: isSelectionMode.value,
|
||||||
toggleSelectionMode();
|
selectedMessages: selectedMessages.value,
|
||||||
toggleMessageSelection(message.id);
|
chatIdentity: chatIdentity,
|
||||||
}
|
toggleSelectionMode: toggleSelectionMode,
|
||||||
},
|
toggleMessageSelection: toggleMessageSelection,
|
||||||
onTap: () {
|
onMessageAction: onMessageAction,
|
||||||
if (isSelectionMode.value) {
|
onJump:
|
||||||
toggleMessageSelection(message.id);
|
(messageId) => scrollToMessage(
|
||||||
}
|
messageId: messageId,
|
||||||
},
|
messageList: messageList,
|
||||||
child: Container(
|
messagesNotifier: messagesNotifier,
|
||||||
color:
|
listController: listController,
|
||||||
selectedMessages.value.contains(message.id)
|
scrollController: scrollController,
|
||||||
? Theme.of(
|
ref: ref,
|
||||||
context,
|
|
||||||
).colorScheme.primaryContainer.withOpacity(0.3)
|
|
||||||
: null,
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
MessageItem(
|
|
||||||
key: settings.disableAnimation ? key : null,
|
|
||||||
message: message,
|
|
||||||
isCurrentUser: identity?.id == message.senderId,
|
|
||||||
onAction:
|
|
||||||
isSelectionMode.value
|
|
||||||
? null
|
|
||||||
: (action) {
|
|
||||||
switch (action) {
|
|
||||||
case MessageItemAction.delete:
|
|
||||||
messagesNotifier.deleteMessage(
|
|
||||||
message.id,
|
|
||||||
);
|
|
||||||
case MessageItemAction.edit:
|
|
||||||
messageEditingTo.value =
|
|
||||||
message.toRemoteMessage();
|
|
||||||
messageController.text =
|
|
||||||
messageEditingTo.value?.content ?? '';
|
|
||||||
attachments.value =
|
|
||||||
messageEditingTo.value!.attachments
|
|
||||||
.map(
|
|
||||||
(e) =>
|
|
||||||
UniversalFile.fromAttachment(
|
|
||||||
e,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
case MessageItemAction.forward:
|
|
||||||
messageForwardingTo.value =
|
|
||||||
message.toRemoteMessage();
|
|
||||||
case MessageItemAction.reply:
|
|
||||||
messageReplyingTo.value =
|
|
||||||
message.toRemoteMessage();
|
|
||||||
case MessageItemAction.resend:
|
|
||||||
messagesNotifier.retryMessage(message.id);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onJump: (messageId) {
|
|
||||||
scrollToMessage(
|
|
||||||
messageId: messageId,
|
|
||||||
messageList: messageList,
|
|
||||||
messagesNotifier: messagesNotifier,
|
|
||||||
listController: listController,
|
|
||||||
scrollController: scrollController,
|
|
||||||
ref: ref,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
progress: attachmentProgress.value[message.id],
|
|
||||||
showAvatar: isLastInGroup,
|
|
||||||
isSelectionMode: isSelectionMode.value,
|
|
||||||
isSelected: selectedMessages.value.contains(message.id),
|
|
||||||
onToggleSelection: toggleMessageSelection,
|
|
||||||
onEnterSelectionMode: () {
|
|
||||||
if (!isSelectionMode.value) {
|
|
||||||
toggleSelectionMode();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (selectedMessages.value.contains(message.id))
|
|
||||||
Positioned(
|
|
||||||
top: 8,
|
|
||||||
right: 8,
|
|
||||||
child: Container(
|
|
||||||
width: 16,
|
|
||||||
height: 16,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
child: Icon(
|
|
||||||
Icons.check,
|
|
||||||
size: 12,
|
|
||||||
color: Theme.of(context).colorScheme.onPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
attachmentProgress: attachmentProgress.value,
|
||||||
loading:
|
disableAnimation: settings.disableAnimation,
|
||||||
() => MessageItem(
|
roomOpenTime: roomOpenTime,
|
||||||
message: message,
|
);
|
||||||
isCurrentUser: false,
|
},
|
||||||
onAction: null,
|
),
|
||||||
progress: null,
|
|
||||||
showAvatar: false,
|
|
||||||
onJump: (_) {},
|
|
||||||
),
|
|
||||||
error: (_, _) => const SizedBox.shrink(),
|
|
||||||
);
|
|
||||||
|
|
||||||
return settings.disableAnimation
|
|
||||||
? messageWidget
|
|
||||||
: TweenAnimationBuilder<double>(
|
|
||||||
key: key,
|
|
||||||
tween: Tween<double>(begin: 0.0, end: 1.0),
|
|
||||||
duration: Duration(
|
|
||||||
milliseconds: 400 + (index % 5) * 50,
|
|
||||||
), // Staggered delay
|
|
||||||
curve: Curves.easeOutCubic,
|
|
||||||
builder: (context, animationValue, child) {
|
|
||||||
return Transform.translate(
|
|
||||||
offset: Offset(
|
|
||||||
0,
|
|
||||||
20 * (1 - animationValue),
|
|
||||||
), // Slide up from bottom
|
|
||||||
child: Opacity(opacity: animationValue, child: child),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: messageWidget,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
@@ -790,7 +738,11 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
AudioCallButton(roomId: id),
|
chatRoom.when(
|
||||||
|
data: (data) => AudioCallButton(room: data!),
|
||||||
|
error: (err, _) => const SizedBox.shrink(),
|
||||||
|
loading: () => const SizedBox.shrink(),
|
||||||
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.more_vert),
|
icon: const Icon(Icons.more_vert),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
@@ -891,7 +843,14 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
top: 0,
|
top: 0,
|
||||||
child: CallOverlayBar().padding(horizontal: 8, top: 12),
|
child: chatRoom.when(
|
||||||
|
data:
|
||||||
|
(data) => CallOverlayBar(
|
||||||
|
room: data!,
|
||||||
|
).padding(horizontal: 8, top: 12),
|
||||||
|
error: (_, _) => const SizedBox.shrink(),
|
||||||
|
loading: () => const SizedBox.shrink(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (isSyncing)
|
if (isSyncing)
|
||||||
Positioned(
|
Positioned(
|
||||||
@@ -965,6 +924,7 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
child: chatRoom.when(
|
child: chatRoom.when(
|
||||||
data:
|
data:
|
||||||
(room) => Column(
|
(room) => Column(
|
||||||
|
key: inputKey,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
ChatInput(
|
ChatInput(
|
||||||
@@ -979,10 +939,16 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
messageEditingTo.value = null;
|
messageEditingTo.value = null;
|
||||||
messageReplyingTo.value = null;
|
messageReplyingTo.value = null;
|
||||||
messageForwardingTo.value = null;
|
messageForwardingTo.value = null;
|
||||||
|
selectedPoll.value = null;
|
||||||
|
selectedFund.value = null;
|
||||||
},
|
},
|
||||||
messageEditingTo: messageEditingTo.value,
|
messageEditingTo: messageEditingTo.value,
|
||||||
messageReplyingTo: messageReplyingTo.value,
|
messageReplyingTo: messageReplyingTo.value,
|
||||||
messageForwardingTo: messageForwardingTo.value,
|
messageForwardingTo: messageForwardingTo.value,
|
||||||
|
selectedPoll: selectedPoll.value,
|
||||||
|
onPollSelected: (poll) => selectedPoll.value = poll,
|
||||||
|
selectedFund: selectedFund.value,
|
||||||
|
onFundSelected: (fund) => selectedFund.value = fund,
|
||||||
onPickFile: (bool isPhoto) {
|
onPickFile: (bool isPhoto) {
|
||||||
if (isPhoto) {
|
if (isPhoto) {
|
||||||
pickPhotoMedia();
|
pickPhotoMedia();
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ 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/screens/chat/chat_form.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';
|
||||||
@@ -447,10 +448,17 @@ class _ChatRoomActionMenu extends HookConsumerWidget {
|
|||||||
if ((chatIdentity.value?.role ?? 0) >= 50)
|
if ((chatIdentity.value?.role ?? 0) >= 50)
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context.pushReplacementNamed(
|
showModalBottomSheet(
|
||||||
'chatEdit',
|
context: context,
|
||||||
pathParameters: {'id': id},
|
useRootNavigator: true,
|
||||||
);
|
isScrollControlled: true,
|
||||||
|
builder: (context) => EditChatScreen(id: id),
|
||||||
|
).then((value) {
|
||||||
|
if (value != null) {
|
||||||
|
// Invalidate to refresh room data after edit
|
||||||
|
ref.invalidate(chatroomProvider(id));
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
169
lib/screens/chat/widgets/message_item_wrapper.dart
Normal file
169
lib/screens/chat/widgets/message_item_wrapper.dart
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
|
import 'package:island/database/message.dart';
|
||||||
|
import 'package:island/models/chat.dart';
|
||||||
|
import 'package:island/widgets/chat/message_item.dart';
|
||||||
|
|
||||||
|
// Provider to track animated messages to prevent replay
|
||||||
|
final animatedMessagesProvider = StateProvider<Set<String>>((ref) => {});
|
||||||
|
|
||||||
|
class MessageItemWrapper extends HookConsumerWidget {
|
||||||
|
final LocalChatMessage message;
|
||||||
|
final int index;
|
||||||
|
final bool isLastInGroup;
|
||||||
|
final bool isSelectionMode;
|
||||||
|
final Set<String> selectedMessages;
|
||||||
|
final AsyncValue<SnChatMember?> chatIdentity;
|
||||||
|
final VoidCallback toggleSelectionMode;
|
||||||
|
final Function(String) toggleMessageSelection;
|
||||||
|
final Function(String, LocalChatMessage) onMessageAction;
|
||||||
|
final Function(String) onJump;
|
||||||
|
final Map<String, Map<int, double?>> attachmentProgress;
|
||||||
|
final bool disableAnimation;
|
||||||
|
final DateTime roomOpenTime;
|
||||||
|
|
||||||
|
const MessageItemWrapper({
|
||||||
|
super.key,
|
||||||
|
required this.message,
|
||||||
|
required this.index,
|
||||||
|
required this.isLastInGroup,
|
||||||
|
required this.isSelectionMode,
|
||||||
|
required this.selectedMessages,
|
||||||
|
required this.chatIdentity,
|
||||||
|
required this.toggleSelectionMode,
|
||||||
|
required this.toggleMessageSelection,
|
||||||
|
required this.onMessageAction,
|
||||||
|
required this.onJump,
|
||||||
|
required this.attachmentProgress,
|
||||||
|
required this.disableAnimation,
|
||||||
|
required this.roomOpenTime,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
// Animation logic
|
||||||
|
final animatedMessages = ref.watch(animatedMessagesProvider);
|
||||||
|
final isNewMessage = message.createdAt.isAfter(roomOpenTime);
|
||||||
|
final hasAnimated = animatedMessages.contains(message.id);
|
||||||
|
|
||||||
|
// Only animate if:
|
||||||
|
// 1. Animation is enabled
|
||||||
|
// 2. Message is new (created after room open)
|
||||||
|
// 3. Has not animated yet
|
||||||
|
final shouldAnimate = !disableAnimation && isNewMessage && !hasAnimated;
|
||||||
|
|
||||||
|
final child = chatIdentity.when(
|
||||||
|
skipError: true,
|
||||||
|
data: (identity) => _buildContent(context, identity),
|
||||||
|
loading: () => _buildLoading(),
|
||||||
|
error: (_, _) => const SizedBox.shrink(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!shouldAnimate) {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TweenAnimationBuilder<double>(
|
||||||
|
key: ValueKey('anim-${message.id}'), // Ensure unique key for animation
|
||||||
|
tween: Tween<double>(begin: 0.0, end: 1.0),
|
||||||
|
duration: Duration(milliseconds: 400 + (index % 5) * 50),
|
||||||
|
curve: Curves.easeOutCubic,
|
||||||
|
builder: (context, value, child) {
|
||||||
|
return Transform.translate(
|
||||||
|
offset: Offset(0, 20 * (1 - value)),
|
||||||
|
child: Opacity(opacity: value, child: child),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onEnd: () {
|
||||||
|
// Mark as animated
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
ref
|
||||||
|
.read(animatedMessagesProvider.notifier)
|
||||||
|
.update((state) => {...state, message.id});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildContent(BuildContext context, SnChatMember? identity) {
|
||||||
|
final isSelected = selectedMessages.contains(message.id);
|
||||||
|
final isCurrentUser = identity?.id == message.senderId;
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onLongPress: () {
|
||||||
|
if (!isSelectionMode) {
|
||||||
|
toggleSelectionMode();
|
||||||
|
toggleMessageSelection(message.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onTap: () {
|
||||||
|
if (isSelectionMode) {
|
||||||
|
toggleMessageSelection(message.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
color:
|
||||||
|
isSelected
|
||||||
|
? Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.primaryContainer.withOpacity(0.3)
|
||||||
|
: null,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
MessageItem(
|
||||||
|
// If animation is disabled, we might want to pass a key to maintain state?
|
||||||
|
// But here we are inside the wrapper.
|
||||||
|
key: ValueKey('item-${message.id}'),
|
||||||
|
message: message,
|
||||||
|
isCurrentUser: isCurrentUser,
|
||||||
|
onAction:
|
||||||
|
isSelectionMode
|
||||||
|
? null
|
||||||
|
: (action) => onMessageAction(action, message),
|
||||||
|
onJump: onJump,
|
||||||
|
progress: attachmentProgress[message.id],
|
||||||
|
showAvatar: isLastInGroup,
|
||||||
|
isSelectionMode: isSelectionMode,
|
||||||
|
isSelected: isSelected,
|
||||||
|
onToggleSelection: toggleMessageSelection,
|
||||||
|
onEnterSelectionMode: () {
|
||||||
|
if (!isSelectionMode) toggleSelectionMode();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (isSelected)
|
||||||
|
Positioned(
|
||||||
|
top: 8,
|
||||||
|
right: 8,
|
||||||
|
child: Container(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.check,
|
||||||
|
size: 12,
|
||||||
|
color: Theme.of(context).colorScheme.onPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLoading() {
|
||||||
|
return MessageItem(
|
||||||
|
message: message,
|
||||||
|
isCurrentUser: false,
|
||||||
|
onAction: null,
|
||||||
|
progress: null,
|
||||||
|
showAvatar: false,
|
||||||
|
onJump: (_) {},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -261,7 +261,11 @@ class _PublisherUnselectedWidget extends HookConsumerWidget {
|
|||||||
subtitle: Text('createPublisherHint').tr(),
|
subtitle: Text('createPublisherHint').tr(),
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context.pushNamed('creatorNew').then((value) {
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
builder: (context) => const NewPublisherScreen(),
|
||||||
|
).then((value) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
ref.invalidate(publishersManagedProvider);
|
ref.invalidate(publishersManagedProvider);
|
||||||
}
|
}
|
||||||
@@ -285,19 +289,18 @@ class CreatorHubScreen extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
void updatePublisher() {
|
void updatePublisher() {
|
||||||
context
|
showModalBottomSheet(
|
||||||
.pushNamed(
|
context: context,
|
||||||
'creatorEdit',
|
isScrollControlled: true,
|
||||||
pathParameters: {'name': currentPublisher.value!.name},
|
builder:
|
||||||
)
|
(context) =>
|
||||||
.then((value) async {
|
EditPublisherScreen(name: currentPublisher.value!.name),
|
||||||
if (value == null) return;
|
).then((value) async {
|
||||||
final data = await ref.refresh(publishersManagedProvider.future);
|
if (value == null) return;
|
||||||
currentPublisher.value =
|
final data = await ref.refresh(publishersManagedProvider.future);
|
||||||
data
|
currentPublisher.value =
|
||||||
.where((e) => e.id == currentPublisher.value!.id)
|
data.where((e) => e.id == currentPublisher.value!.id).firstOrNull;
|
||||||
.firstOrNull;
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void deletePublisher() {
|
void deletePublisher() {
|
||||||
@@ -400,6 +403,21 @@ class CreatorHubScreen extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
ListTile(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
),
|
||||||
|
minTileHeight: 48,
|
||||||
|
title: Text('publicationSites').tr(),
|
||||||
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
|
leading: const Icon(Symbols.web),
|
||||||
|
onTap: () {
|
||||||
|
context.pushNamed(
|
||||||
|
'creatorSites',
|
||||||
|
pathParameters: {'name': currentPublisher.value!.name},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
@@ -582,7 +600,7 @@ class CreatorHubScreen extends HookConsumerWidget {
|
|||||||
).padding(horizontal: 12),
|
).padding(horizontal: 12),
|
||||||
buildNavigationWidget(true),
|
buildNavigationWidget(true),
|
||||||
],
|
],
|
||||||
)
|
).padding(vertical: 24)
|
||||||
: Column(
|
: Column(
|
||||||
spacing: 12,
|
spacing: 12,
|
||||||
children: [
|
children: [
|
||||||
@@ -828,7 +846,7 @@ class _PublisherMemberListSheet extends HookConsumerWidget {
|
|||||||
try {
|
try {
|
||||||
final apiClient = ref.watch(apiClientProvider);
|
final apiClient = ref.watch(apiClientProvider);
|
||||||
await apiClient.post(
|
await apiClient.post(
|
||||||
'/publishers/$publisherUname/invites',
|
'/sphere/publishers/invites/$publisherUname',
|
||||||
data: {'related_user_id': result.id, 'role': 0},
|
data: {'related_user_id': result.id, 'role': 0},
|
||||||
);
|
);
|
||||||
// Refresh both providers
|
// Refresh both providers
|
||||||
@@ -959,7 +977,7 @@ class _PublisherMemberListSheet extends HookConsumerWidget {
|
|||||||
apiClientProvider,
|
apiClientProvider,
|
||||||
);
|
);
|
||||||
await apiClient.delete(
|
await apiClient.delete(
|
||||||
'/publishers/$publisherUname/members/${member.accountId}',
|
'/sphere/publishers/$publisherUname/members/${member.accountId}',
|
||||||
);
|
);
|
||||||
// Refresh both providers
|
// Refresh both providers
|
||||||
memberNotifier.reset();
|
memberNotifier.reset();
|
||||||
@@ -1084,7 +1102,7 @@ class _PublisherMemberRoleSheet extends HookConsumerWidget {
|
|||||||
|
|
||||||
final apiClient = ref.read(apiClientProvider);
|
final apiClient = ref.read(apiClientProvider);
|
||||||
await apiClient.patch(
|
await apiClient.patch(
|
||||||
'/publishers/$publisherUname/members/${member.accountId}/role',
|
'/sphere/publishers/$publisherUname/members/${member.accountId}/role',
|
||||||
data: newRole,
|
data: newRole,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1116,7 +1134,7 @@ class _PublisherInviteSheet extends HookConsumerWidget {
|
|||||||
try {
|
try {
|
||||||
final client = ref.read(apiClientProvider);
|
final client = ref.read(apiClientProvider);
|
||||||
await client.post(
|
await client.post(
|
||||||
'/publishers/invites/${invite.publisher!.name}/accept',
|
'/sphere/publishers/invites/${invite.publisher!.name}/accept',
|
||||||
);
|
);
|
||||||
ref.invalidate(publisherInvitesProvider);
|
ref.invalidate(publisherInvitesProvider);
|
||||||
ref.invalidate(publishersManagedProvider);
|
ref.invalidate(publishersManagedProvider);
|
||||||
@@ -1129,7 +1147,7 @@ class _PublisherInviteSheet extends HookConsumerWidget {
|
|||||||
try {
|
try {
|
||||||
final client = ref.read(apiClientProvider);
|
final client = ref.read(apiClientProvider);
|
||||||
await client.post(
|
await client.post(
|
||||||
'/publishers/invites/${invite.publisher!.name}/decline',
|
'/sphere/publishers/invites/${invite.publisher!.name}/decline',
|
||||||
);
|
);
|
||||||
ref.invalidate(publisherInvitesProvider);
|
ref.invalidate(publisherInvitesProvider);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/poll.dart';
|
import 'package:island/models/poll.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
|
import 'package:island/screens/poll/poll_editor.dart';
|
||||||
|
import 'package:island/widgets/alert.dart';
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
import 'package:island/widgets/poll/poll_feedback.dart';
|
import 'package:island/widgets/poll/poll_feedback.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
@@ -73,10 +74,14 @@ class CreatorPollListScreen extends HookConsumerWidget {
|
|||||||
final String pubName;
|
final String pubName;
|
||||||
|
|
||||||
Future<void> _createPoll(BuildContext context) async {
|
Future<void> _createPoll(BuildContext context) async {
|
||||||
final result = await GoRouter.of(
|
final result = await showModalBottomSheet<SnPollWithStats>(
|
||||||
context,
|
context: context,
|
||||||
).pushNamed('creatorPollNew', pathParameters: {'name': pubName});
|
isScrollControlled: true,
|
||||||
if (result is SnPollWithStats && context.mounted) {
|
isDismissible: false,
|
||||||
|
enableDrag: false,
|
||||||
|
builder: (context) => PollEditorScreen(initialPublisher: pubName),
|
||||||
|
);
|
||||||
|
if (result != null && context.mounted) {
|
||||||
Navigator.of(context).maybePop(result);
|
Navigator.of(context).maybePop(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -176,11 +181,20 @@ class _CreatorPollItem extends HookConsumerWidget {
|
|||||||
Text('edit').tr(),
|
Text('edit').tr(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () async {
|
||||||
GoRouter.of(context).pushNamed(
|
final result = await showModalBottomSheet<SnPoll>(
|
||||||
'creatorPollEdit',
|
context: context,
|
||||||
pathParameters: {'name': pubName, 'id': pollWithStats.id},
|
isScrollControlled: true,
|
||||||
|
isDismissible: false,
|
||||||
|
builder:
|
||||||
|
(context) => PollEditorScreen(
|
||||||
|
initialPublisher: pubName,
|
||||||
|
initialPollId: pollWithStats.id,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
if (result != null && context.mounted) {
|
||||||
|
ref.invalidate(pollListNotifierProvider(pubName));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
@@ -221,19 +235,9 @@ class _CreatorPollItem extends HookConsumerWidget {
|
|||||||
'/sphere/polls/${pollWithStats.id}',
|
'/sphere/polls/${pollWithStats.id}',
|
||||||
);
|
);
|
||||||
ref.invalidate(pollListNotifierProvider(pubName));
|
ref.invalidate(pollListNotifierProvider(pubName));
|
||||||
if (context.mounted) {
|
showSnackBar('Poll deleted successfully');
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text('Poll deleted successfully'),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (context.mounted) {
|
showErrorAlert(e);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(content: Text('Failed to delete poll')),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ import 'package:island/screens/realm/realms.dart';
|
|||||||
import 'package:island/services/file.dart';
|
import 'package:island/services/file.dart';
|
||||||
import 'package:island/services/file_uploader.dart';
|
import 'package:island/services/file_uploader.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
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: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';
|
||||||
@@ -95,11 +95,11 @@ class EditPublisherScreen extends HookConsumerWidget {
|
|||||||
try {
|
try {
|
||||||
final cloudFile =
|
final cloudFile =
|
||||||
await FileUploader.createCloudFile(
|
await FileUploader.createCloudFile(
|
||||||
|
ref: ref,
|
||||||
fileData: UniversalFile(
|
fileData: UniversalFile(
|
||||||
data: result,
|
data: result,
|
||||||
type: UniversalFileType.image,
|
type: UniversalFileType.image,
|
||||||
),
|
),
|
||||||
client: ref.read(apiClientProvider),
|
|
||||||
).future;
|
).future;
|
||||||
if (cloudFile == null) {
|
if (cloudFile == null) {
|
||||||
throw ArgumentError('Failed to upload the file...');
|
throw ArgumentError('Failed to upload the file...');
|
||||||
@@ -177,13 +177,11 @@ class EditPublisherScreen extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return AppScaffold(
|
final titleText = (name == null ? 'createPublisher' : 'editPublisher').tr();
|
||||||
isNoBackground: false,
|
|
||||||
appBar: AppBar(
|
return SheetScaffold(
|
||||||
title: Text(name == null ? 'createPublisher' : 'editPublisher').tr(),
|
titleText: titleText,
|
||||||
leading: const PageBackButton(),
|
child: SingleChildScrollView(
|
||||||
),
|
|
||||||
body: SingleChildScrollView(
|
|
||||||
padding: EdgeInsets.only(bottom: 16),
|
padding: EdgeInsets.only(bottom: 16),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
233
lib/screens/creators/sites/site_detail.dart
Normal file
233
lib/screens/creators/sites/site_detail.dart
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:island/models/publication_site.dart';
|
||||||
|
import 'package:island/pods/network.dart';
|
||||||
|
import 'package:island/pods/site_pages.dart';
|
||||||
|
import 'package:island/widgets/sites/page_form.dart';
|
||||||
|
import 'package:island/widgets/sites/site_action_menu.dart';
|
||||||
|
import 'package:island/widgets/sites/site_detail_content.dart';
|
||||||
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
import 'package:island/widgets/sites/info_row.dart';
|
||||||
|
import 'package:island/widgets/sites/pages_section.dart';
|
||||||
|
import 'package:island/widgets/sites/file_management_section.dart';
|
||||||
|
import 'package:island/widgets/sites/file_management_action_section.dart';
|
||||||
|
import 'package:island/services/responsive.dart';
|
||||||
|
import 'package:island/services/time.dart';
|
||||||
|
import 'package:island/widgets/extended_refresh_indicator.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
|
part 'site_detail.g.dart';
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
Future<SnPublicationSite> publicationSiteDetail(
|
||||||
|
Ref ref,
|
||||||
|
String pubName,
|
||||||
|
String siteSlug,
|
||||||
|
) async {
|
||||||
|
final apiClient = ref.watch(apiClientProvider);
|
||||||
|
final resp = await apiClient.get('/zone/sites/$pubName/$siteSlug');
|
||||||
|
return SnPublicationSite.fromJson(resp.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
class PublicationSiteDetailScreen extends HookConsumerWidget {
|
||||||
|
final String siteSlug;
|
||||||
|
final String pubName;
|
||||||
|
|
||||||
|
const PublicationSiteDetailScreen({
|
||||||
|
super.key,
|
||||||
|
required this.siteSlug,
|
||||||
|
required this.pubName,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final siteAsync = ref.watch(
|
||||||
|
publicationSiteDetailProvider(pubName, siteSlug),
|
||||||
|
);
|
||||||
|
|
||||||
|
return AppScaffold(
|
||||||
|
isNoBackground: false,
|
||||||
|
appBar: AppBar(
|
||||||
|
title: siteAsync.maybeWhen(
|
||||||
|
data: (site) => Text(site.name),
|
||||||
|
orElse: () => Text('siteDetails'.tr()),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
siteAsync.maybeWhen(
|
||||||
|
data: (site) => SiteActionMenu(site: site, pubName: pubName),
|
||||||
|
orElse: () => const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: siteAsync.when(
|
||||||
|
data: (site) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
if (isWideScreen(context)) {
|
||||||
|
return ExtendedRefreshIndicator(
|
||||||
|
onRefresh:
|
||||||
|
() async => ref.invalidate(
|
||||||
|
publicationSiteDetailProvider(pubName, site.slug),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
spacing: 8,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
flex: 3,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
PagesSection(site: site, pubName: pubName),
|
||||||
|
if (site.mode == 1) // Self-Managed only
|
||||||
|
FileManagementSection(site: site, pubName: pubName),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
flex: 2,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'siteInformation'.tr(),
|
||||||
|
style: theme.textTheme.titleMedium
|
||||||
|
?.copyWith(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const Gap(16),
|
||||||
|
InfoRow(
|
||||||
|
label: 'name'.tr(),
|
||||||
|
value: site.name,
|
||||||
|
icon: Symbols.title,
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
InfoRow(
|
||||||
|
label: 'slug'.tr(),
|
||||||
|
value: site.slug,
|
||||||
|
icon: Symbols.tag,
|
||||||
|
monospace: true,
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
InfoRow(
|
||||||
|
label: 'siteDomain'.tr(),
|
||||||
|
value: '${site.slug}.solian.page',
|
||||||
|
icon: Symbols.globe,
|
||||||
|
monospace: true,
|
||||||
|
onTap: () {
|
||||||
|
final url =
|
||||||
|
'https://${site.slug}.solian.page';
|
||||||
|
launchUrlString(url);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
InfoRow(
|
||||||
|
label: 'siteMode'.tr(),
|
||||||
|
value:
|
||||||
|
site.mode == 0
|
||||||
|
? 'siteModeFullyManaged'.tr()
|
||||||
|
: 'siteModeSelfManaged'.tr(),
|
||||||
|
icon: Symbols.settings,
|
||||||
|
),
|
||||||
|
if (site.description != null &&
|
||||||
|
site.description!.isNotEmpty) ...[
|
||||||
|
const Gap(8),
|
||||||
|
InfoRow(
|
||||||
|
label: 'description'.tr(),
|
||||||
|
value: site.description!,
|
||||||
|
icon: Symbols.description,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
const Gap(8),
|
||||||
|
InfoRow(
|
||||||
|
label: 'siteCreated'.tr(),
|
||||||
|
value: site.createdAt.formatSystem(),
|
||||||
|
icon: Symbols.calendar_add_on,
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
InfoRow(
|
||||||
|
label: 'siteUpdated'.tr(),
|
||||||
|
value: site.updatedAt.formatSystem(),
|
||||||
|
icon: Symbols.update,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
if (site.mode == 1) // Self-Managed only
|
||||||
|
FileManagementActionSection(
|
||||||
|
site: site,
|
||||||
|
pubName: pubName,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 12),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return SiteDetailContent(site: site, pubName: pubName);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error:
|
||||||
|
(error, stack) => Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'failedToLoadSite'.tr(),
|
||||||
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
|
),
|
||||||
|
const Gap(16),
|
||||||
|
Text(error.toString()),
|
||||||
|
const Gap(24),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed:
|
||||||
|
() => ref.invalidate(
|
||||||
|
publicationSiteDetailProvider(pubName, siteSlug),
|
||||||
|
),
|
||||||
|
child: Text('retry'.tr()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
|
),
|
||||||
|
floatingActionButton: siteAsync.maybeWhen(
|
||||||
|
data:
|
||||||
|
(site) => FloatingActionButton(
|
||||||
|
onPressed: () {
|
||||||
|
// Create new page
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
builder: (context) => PageForm(site: site, pubName: pubName),
|
||||||
|
).then((_) {
|
||||||
|
// Refresh pages after creation
|
||||||
|
ref.invalidate(sitePagesProvider(pubName, site.slug));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: const Icon(Symbols.add),
|
||||||
|
),
|
||||||
|
orElse: () => null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
173
lib/screens/creators/sites/site_detail.g.dart
Normal file
173
lib/screens/creators/sites/site_detail.g.dart
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'site_detail.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
String _$publicationSiteDetailHash() =>
|
||||||
|
r'e5d259ea39c4ba47e92d37e644fc3d84984927a9';
|
||||||
|
|
||||||
|
/// Copied from Dart SDK
|
||||||
|
class _SystemHash {
|
||||||
|
_SystemHash._();
|
||||||
|
|
||||||
|
static int combine(int hash, int value) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + value);
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||||
|
return hash ^ (hash >> 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int finish(int hash) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = hash ^ (hash >> 11);
|
||||||
|
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [publicationSiteDetail].
|
||||||
|
@ProviderFor(publicationSiteDetail)
|
||||||
|
const publicationSiteDetailProvider = PublicationSiteDetailFamily();
|
||||||
|
|
||||||
|
/// See also [publicationSiteDetail].
|
||||||
|
class PublicationSiteDetailFamily
|
||||||
|
extends Family<AsyncValue<SnPublicationSite>> {
|
||||||
|
/// See also [publicationSiteDetail].
|
||||||
|
const PublicationSiteDetailFamily();
|
||||||
|
|
||||||
|
/// See also [publicationSiteDetail].
|
||||||
|
PublicationSiteDetailProvider call(String pubName, String siteSlug) {
|
||||||
|
return PublicationSiteDetailProvider(pubName, siteSlug);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
PublicationSiteDetailProvider getProviderOverride(
|
||||||
|
covariant PublicationSiteDetailProvider provider,
|
||||||
|
) {
|
||||||
|
return call(provider.pubName, provider.siteSlug);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||||
|
_allTransitiveDependencies;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get name => r'publicationSiteDetailProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [publicationSiteDetail].
|
||||||
|
class PublicationSiteDetailProvider
|
||||||
|
extends AutoDisposeFutureProvider<SnPublicationSite> {
|
||||||
|
/// See also [publicationSiteDetail].
|
||||||
|
PublicationSiteDetailProvider(String pubName, String siteSlug)
|
||||||
|
: this._internal(
|
||||||
|
(ref) => publicationSiteDetail(
|
||||||
|
ref as PublicationSiteDetailRef,
|
||||||
|
pubName,
|
||||||
|
siteSlug,
|
||||||
|
),
|
||||||
|
from: publicationSiteDetailProvider,
|
||||||
|
name: r'publicationSiteDetailProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$publicationSiteDetailHash,
|
||||||
|
dependencies: PublicationSiteDetailFamily._dependencies,
|
||||||
|
allTransitiveDependencies:
|
||||||
|
PublicationSiteDetailFamily._allTransitiveDependencies,
|
||||||
|
pubName: pubName,
|
||||||
|
siteSlug: siteSlug,
|
||||||
|
);
|
||||||
|
|
||||||
|
PublicationSiteDetailProvider._internal(
|
||||||
|
super._createNotifier, {
|
||||||
|
required super.name,
|
||||||
|
required super.dependencies,
|
||||||
|
required super.allTransitiveDependencies,
|
||||||
|
required super.debugGetCreateSourceHash,
|
||||||
|
required super.from,
|
||||||
|
required this.pubName,
|
||||||
|
required this.siteSlug,
|
||||||
|
}) : super.internal();
|
||||||
|
|
||||||
|
final String pubName;
|
||||||
|
final String siteSlug;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Override overrideWith(
|
||||||
|
FutureOr<SnPublicationSite> Function(PublicationSiteDetailRef provider)
|
||||||
|
create,
|
||||||
|
) {
|
||||||
|
return ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
override: PublicationSiteDetailProvider._internal(
|
||||||
|
(ref) => create(ref as PublicationSiteDetailRef),
|
||||||
|
from: from,
|
||||||
|
name: null,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
debugGetCreateSourceHash: null,
|
||||||
|
pubName: pubName,
|
||||||
|
siteSlug: siteSlug,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AutoDisposeFutureProviderElement<SnPublicationSite> createElement() {
|
||||||
|
return _PublicationSiteDetailProviderElement(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is PublicationSiteDetailProvider &&
|
||||||
|
other.pubName == pubName &&
|
||||||
|
other.siteSlug == siteSlug;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, pubName.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, siteSlug.hashCode);
|
||||||
|
|
||||||
|
return _SystemHash.finish(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
mixin PublicationSiteDetailRef
|
||||||
|
on AutoDisposeFutureProviderRef<SnPublicationSite> {
|
||||||
|
/// The parameter `pubName` of this provider.
|
||||||
|
String get pubName;
|
||||||
|
|
||||||
|
/// The parameter `siteSlug` of this provider.
|
||||||
|
String get siteSlug;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PublicationSiteDetailProviderElement
|
||||||
|
extends AutoDisposeFutureProviderElement<SnPublicationSite>
|
||||||
|
with PublicationSiteDetailRef {
|
||||||
|
_PublicationSiteDetailProviderElement(super.provider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pubName => (origin as PublicationSiteDetailProvider).pubName;
|
||||||
|
@override
|
||||||
|
String get siteSlug => (origin as PublicationSiteDetailProvider).siteSlug;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
384
lib/screens/creators/sites/site_edit.dart
Normal file
384
lib/screens/creators/sites/site_edit.dart
Normal file
@@ -0,0 +1,384 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:island/pods/network.dart';
|
||||||
|
import 'package:island/screens/creators/sites/site_detail.dart';
|
||||||
|
import 'package:island/screens/creators/sites/site_list.dart';
|
||||||
|
import 'package:island/widgets/alert.dart';
|
||||||
|
import 'package:island/widgets/content/sheet.dart';
|
||||||
|
import 'package:island/widgets/response.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
|
||||||
|
class SiteForm extends HookConsumerWidget {
|
||||||
|
final String pubName;
|
||||||
|
final String? siteSlug;
|
||||||
|
|
||||||
|
const SiteForm({super.key, required this.pubName, this.siteSlug});
|
||||||
|
|
||||||
|
Widget _buildForm(
|
||||||
|
GlobalKey<FormState> formKey,
|
||||||
|
TextEditingController slugController,
|
||||||
|
TextEditingController nameController,
|
||||||
|
TextEditingController descriptionController,
|
||||||
|
ValueNotifier<int> modeController,
|
||||||
|
Function() saveSite,
|
||||||
|
Function() deleteSite,
|
||||||
|
String siteSlug,
|
||||||
|
) {
|
||||||
|
final formFields = Column(
|
||||||
|
children: [
|
||||||
|
TextFormField(
|
||||||
|
controller: slugController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'siteSlug'.tr(),
|
||||||
|
hintText: 'siteSlugHint'.tr(),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'siteSlugRequired'.tr();
|
||||||
|
}
|
||||||
|
final slugRegex = RegExp(r'^[a-z0-9]+(?:-[a-z0-9]+)*$');
|
||||||
|
if (!slugRegex.hasMatch(value)) {
|
||||||
|
return 'siteSlugInvalid'.tr();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
controller: nameController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'siteName'.tr(),
|
||||||
|
hintText: 'siteNameHint'.tr(),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'siteNameRequired'.tr();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
controller: descriptionController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'description'.tr(),
|
||||||
|
alignLabelWithHint: true,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
maxLines: 3,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
DropdownButtonFormField<int>(
|
||||||
|
value: modeController.value,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'siteMode'.tr(),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
items: [
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: 0,
|
||||||
|
child: Text('siteModeFullyManaged'.tr()),
|
||||||
|
),
|
||||||
|
DropdownMenuItem(value: 1, child: Text('siteModeSelfManaged'.tr())),
|
||||||
|
],
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value != null) {
|
||||||
|
modeController.value = value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(all: 20);
|
||||||
|
|
||||||
|
return SheetScaffold(
|
||||||
|
titleText: 'editPublicationSite'.tr(),
|
||||||
|
child: Builder(
|
||||||
|
builder:
|
||||||
|
(context) => SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Form(key: formKey, child: formFields),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: deleteSite,
|
||||||
|
icon: const Icon(Symbols.delete_forever),
|
||||||
|
label: Text('deletePublicationSite'.tr()),
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
foregroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
).alignment(Alignment.centerRight),
|
||||||
|
const Spacer(),
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: saveSite,
|
||||||
|
icon: const Icon(Symbols.save),
|
||||||
|
label: Text('saveChanges').tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 20, vertical: 12),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final formKey = useMemoized(() => GlobalKey<FormState>());
|
||||||
|
final slugController = useTextEditingController();
|
||||||
|
final nameController = useTextEditingController();
|
||||||
|
final descriptionController = useTextEditingController();
|
||||||
|
final modeController = useState<int>(0); // Default to fully managed (0)
|
||||||
|
final isLoading = useState(false);
|
||||||
|
|
||||||
|
final saveSite = useCallback(() async {
|
||||||
|
if (!formKey.currentState!.validate()) return;
|
||||||
|
|
||||||
|
isLoading.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final client = ref.read(apiClientProvider);
|
||||||
|
final url = '/zone/sites/$pubName';
|
||||||
|
final payload = <String, dynamic>{
|
||||||
|
'slug': slugController.text,
|
||||||
|
'name': nameController.text,
|
||||||
|
'mode': modeController.value,
|
||||||
|
if (descriptionController.text.isNotEmpty)
|
||||||
|
'description': descriptionController.text,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (siteSlug != null) {
|
||||||
|
await client.patch('$url/$siteSlug', data: payload);
|
||||||
|
} else {
|
||||||
|
await client.post(url, data: payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh the site list
|
||||||
|
ref.invalidate(siteListNotifierProvider(pubName));
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
showSnackBar('publicationSiteSavedSuccess'.tr());
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showErrorAlert(e);
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}, [pubName, siteSlug, context]);
|
||||||
|
|
||||||
|
final deleteSite = useCallback(() async {
|
||||||
|
if (siteSlug == null) return; // Shouldn't happen for editing
|
||||||
|
|
||||||
|
final confirmed = await showConfirmAlert(
|
||||||
|
'publicationSiteDeleteConfirm'.tr(),
|
||||||
|
'deletePublicationSite'.tr(),
|
||||||
|
);
|
||||||
|
if (confirmed != true) return;
|
||||||
|
|
||||||
|
isLoading.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final client = ref.read(apiClientProvider);
|
||||||
|
await client.delete('/zone/sites/$pubName/$siteSlug');
|
||||||
|
|
||||||
|
ref.invalidate(siteListNotifierProvider(pubName));
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
showSnackBar('publicationSiteDeletedSuccess'.tr());
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showErrorAlert(e);
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}, [pubName, siteSlug, context]);
|
||||||
|
|
||||||
|
// Use Riverpod provider for loading and error states for editing
|
||||||
|
if (siteSlug != null) {
|
||||||
|
final editingSiteSlug =
|
||||||
|
siteSlug!; // Assert non-null since we checked above
|
||||||
|
final siteAsync = ref.watch(
|
||||||
|
publicationSiteDetailProvider(pubName, editingSiteSlug),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initialize form fields when site data is loaded
|
||||||
|
useEffect(() {
|
||||||
|
if (siteAsync.value != null && nameController.text.isEmpty) {
|
||||||
|
final site = siteAsync.value!;
|
||||||
|
slugController.text = site.slug;
|
||||||
|
nameController.text = site.name;
|
||||||
|
descriptionController.text = site.description ?? '';
|
||||||
|
modeController.value = site.mode ?? 0;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [siteAsync]);
|
||||||
|
|
||||||
|
// Handle loading and error states for editing using AsyncValue
|
||||||
|
return siteAsync.when(
|
||||||
|
data:
|
||||||
|
(_) => _buildForm(
|
||||||
|
formKey,
|
||||||
|
slugController,
|
||||||
|
nameController,
|
||||||
|
descriptionController,
|
||||||
|
modeController,
|
||||||
|
saveSite,
|
||||||
|
deleteSite,
|
||||||
|
editingSiteSlug,
|
||||||
|
),
|
||||||
|
loading:
|
||||||
|
() => SheetScaffold(
|
||||||
|
titleText: 'editPublicationSite'.tr(),
|
||||||
|
child: Center(child: CircularProgressIndicator()),
|
||||||
|
),
|
||||||
|
error:
|
||||||
|
(error, _) => SheetScaffold(
|
||||||
|
titleText: 'editPublicationSite'.tr(),
|
||||||
|
child: ResponseErrorWidget(
|
||||||
|
error: error.toString(),
|
||||||
|
onRetry: () {
|
||||||
|
ref.invalidate(
|
||||||
|
publicationSiteDetailProvider(pubName, editingSiteSlug),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For new sites, directly show the form
|
||||||
|
|
||||||
|
final formFields = Column(
|
||||||
|
children: [
|
||||||
|
TextFormField(
|
||||||
|
controller: slugController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Slug',
|
||||||
|
hintText: 'my-site',
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Please enter a slug';
|
||||||
|
}
|
||||||
|
final slugRegex = RegExp(r'^[a-z0-9]+(?:-[a-z0-9]+)*$');
|
||||||
|
if (!slugRegex.hasMatch(value)) {
|
||||||
|
return 'Slug can only contain lowercase letters, numbers, and dashes';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
controller: nameController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Site Name',
|
||||||
|
hintText: 'My Publication Site',
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Please enter a site name';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
controller: descriptionController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Description',
|
||||||
|
alignLabelWithHint: true,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
maxLines: 3,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
DropdownButtonFormField<int>(
|
||||||
|
value: modeController.value,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Mode',
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
items: [
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: 0,
|
||||||
|
child: Text('siteModeFullyManaged'.tr()),
|
||||||
|
),
|
||||||
|
DropdownMenuItem(value: 1, child: Text('siteModeSelfManaged'.tr())),
|
||||||
|
],
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value != null) {
|
||||||
|
modeController.value = value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(all: 20);
|
||||||
|
|
||||||
|
final saveButton = TextButton.icon(
|
||||||
|
onPressed: isLoading.value ? null : saveSite,
|
||||||
|
icon: const Icon(Symbols.save),
|
||||||
|
label: Text('saveChanges').tr(),
|
||||||
|
).padding(horizontal: 20, vertical: 12);
|
||||||
|
|
||||||
|
return SheetScaffold(
|
||||||
|
titleText:
|
||||||
|
siteSlug == null
|
||||||
|
? 'newPublicationSite'.tr()
|
||||||
|
: 'editPublicationSite'.tr(),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Form(key: formKey, child: formFields),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
if (siteSlug != null) ...[
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: isLoading.value ? null : deleteSite,
|
||||||
|
icon: const Icon(Symbols.delete_forever),
|
||||||
|
label: Text('deletePublicationSite'.tr()),
|
||||||
|
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||||
|
).alignment(Alignment.centerRight),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
const Spacer(),
|
||||||
|
saveButton,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
241
lib/screens/creators/sites/site_list.dart
Normal file
241
lib/screens/creators/sites/site_list.dart
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:island/models/publication_site.dart';
|
||||||
|
import 'package:island/pods/network.dart';
|
||||||
|
import 'package:island/screens/creators/sites/site_edit.dart';
|
||||||
|
import 'package:island/widgets/alert.dart';
|
||||||
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
|
||||||
|
import 'package:island/widgets/extended_refresh_indicator.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
|
||||||
|
part 'site_list.g.dart';
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
class SiteListNotifier extends _$SiteListNotifier
|
||||||
|
with CursorPagingNotifierMixin<SnPublicationSite> {
|
||||||
|
static const int _pageSize = 20;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<CursorPagingData<SnPublicationSite>> build(String? pubName) {
|
||||||
|
// immediately load first page
|
||||||
|
return fetch(cursor: null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<CursorPagingData<SnPublicationSite>> fetch({
|
||||||
|
required String? cursor,
|
||||||
|
}) async {
|
||||||
|
final client = ref.read(apiClientProvider);
|
||||||
|
final offset = cursor == null ? 0 : int.parse(cursor);
|
||||||
|
|
||||||
|
// read the current family argument passed to provider
|
||||||
|
final queryParams = {'offset': offset, 'take': _pageSize};
|
||||||
|
|
||||||
|
final response = await client.get(
|
||||||
|
'/zone/sites/$pubName',
|
||||||
|
queryParameters: queryParams,
|
||||||
|
);
|
||||||
|
final total = int.parse(response.headers.value('X-Total') ?? '0');
|
||||||
|
final List<dynamic> data = response.data;
|
||||||
|
final items = data.map((json) => SnPublicationSite.fromJson(json)).toList();
|
||||||
|
|
||||||
|
final hasMore = offset + items.length < total;
|
||||||
|
final nextCursor = hasMore ? (offset + items.length).toString() : null;
|
||||||
|
|
||||||
|
return CursorPagingData(
|
||||||
|
items: items,
|
||||||
|
hasMore: hasMore,
|
||||||
|
nextCursor: nextCursor,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CreatorSiteListScreen extends HookConsumerWidget {
|
||||||
|
const CreatorSiteListScreen({super.key, required this.pubName});
|
||||||
|
|
||||||
|
final String pubName;
|
||||||
|
|
||||||
|
Future<void> _createSite(BuildContext context) async {
|
||||||
|
await showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
builder: (context) => SiteForm(pubName: pubName),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return AppScaffold(
|
||||||
|
isNoBackground: false,
|
||||||
|
appBar: AppBar(title: Text('publicationSites'.tr())),
|
||||||
|
floatingActionButton: FloatingActionButton(
|
||||||
|
onPressed: () => _createSite(context),
|
||||||
|
child: Icon(Icons.add),
|
||||||
|
),
|
||||||
|
body: ExtendedRefreshIndicator(
|
||||||
|
onRefresh: () => ref.refresh(siteListNotifierProvider(pubName).future),
|
||||||
|
child: CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
const SliverGap(8),
|
||||||
|
PagingHelperSliverView(
|
||||||
|
provider: siteListNotifierProvider(pubName),
|
||||||
|
futureRefreshable: siteListNotifierProvider(pubName).future,
|
||||||
|
notifierRefreshable: siteListNotifierProvider(pubName).notifier,
|
||||||
|
contentBuilder:
|
||||||
|
(data, widgetCount, endItemView) => SliverList.builder(
|
||||||
|
itemCount: widgetCount,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
if (index == widgetCount - 1) {
|
||||||
|
return endItemView;
|
||||||
|
}
|
||||||
|
final site = data.items[index];
|
||||||
|
return ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(maxWidth: 640),
|
||||||
|
child: _CreatorSiteItem(site: site, pubName: pubName),
|
||||||
|
).center();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CreatorSiteItem extends HookConsumerWidget {
|
||||||
|
final String pubName;
|
||||||
|
const _CreatorSiteItem({required this.site, required this.pubName});
|
||||||
|
|
||||||
|
final SnPublicationSite site;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () {
|
||||||
|
// Navigate to site detail screen
|
||||||
|
context.pushNamed(
|
||||||
|
'creatorSiteDetail',
|
||||||
|
pathParameters: {'name': pubName, 'siteSlug': site.slug},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
spacing: 2,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Symbols.globe,
|
||||||
|
size: 18,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
const Gap(6),
|
||||||
|
Text(site.name).bold(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (site.description != null &&
|
||||||
|
site.description!.isNotEmpty)
|
||||||
|
Text(
|
||||||
|
site.description!,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
const Divider(height: 8),
|
||||||
|
Text(
|
||||||
|
'${site.slug}.solian.page',
|
||||||
|
style: GoogleFonts.robotoMono(fontSize: 11),
|
||||||
|
).opacity(0.8),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
PopupMenuButton<String>(
|
||||||
|
itemBuilder:
|
||||||
|
(context) => [
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.edit),
|
||||||
|
const Gap(16),
|
||||||
|
Text('edit').tr(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
builder:
|
||||||
|
(context) => SiteForm(
|
||||||
|
pubName: pubName,
|
||||||
|
siteSlug: site.slug,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.delete, color: Colors.red),
|
||||||
|
const Gap(16),
|
||||||
|
Text('delete').tr().textColor(Colors.red),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () async {
|
||||||
|
final confirmed = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder:
|
||||||
|
(context) => AlertDialog(
|
||||||
|
title: Text('deleteSite'.tr()),
|
||||||
|
content: Text('deleteSiteConfirm'.tr()),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed:
|
||||||
|
() =>
|
||||||
|
Navigator.of(context).pop(false),
|
||||||
|
child: Text('cancel'.tr()),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed:
|
||||||
|
() => Navigator.of(context).pop(true),
|
||||||
|
child: Text('delete'.tr()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (confirmed == true) {
|
||||||
|
try {
|
||||||
|
final client = ref.read(apiClientProvider);
|
||||||
|
await client.delete('/zone/sites/${site.id}');
|
||||||
|
ref.invalidate(siteListNotifierProvider(pubName));
|
||||||
|
showSnackBar('siteDeletedSuccess'.tr());
|
||||||
|
} catch (e) {
|
||||||
|
showErrorAlert(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
183
lib/screens/creators/sites/site_list.g.dart
Normal file
183
lib/screens/creators/sites/site_list.g.dart
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'site_list.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
String _$siteListNotifierHash() => r'1670cadcc0c7ccbd98bc33bbf5b4af21e9cb166c';
|
||||||
|
|
||||||
|
/// Copied from Dart SDK
|
||||||
|
class _SystemHash {
|
||||||
|
_SystemHash._();
|
||||||
|
|
||||||
|
static int combine(int hash, int value) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + value);
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||||
|
return hash ^ (hash >> 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int finish(int hash) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = hash ^ (hash >> 11);
|
||||||
|
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _$SiteListNotifier
|
||||||
|
extends
|
||||||
|
BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnPublicationSite>> {
|
||||||
|
late final String? pubName;
|
||||||
|
|
||||||
|
FutureOr<CursorPagingData<SnPublicationSite>> build(String? pubName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [SiteListNotifier].
|
||||||
|
@ProviderFor(SiteListNotifier)
|
||||||
|
const siteListNotifierProvider = SiteListNotifierFamily();
|
||||||
|
|
||||||
|
/// See also [SiteListNotifier].
|
||||||
|
class SiteListNotifierFamily
|
||||||
|
extends Family<AsyncValue<CursorPagingData<SnPublicationSite>>> {
|
||||||
|
/// See also [SiteListNotifier].
|
||||||
|
const SiteListNotifierFamily();
|
||||||
|
|
||||||
|
/// See also [SiteListNotifier].
|
||||||
|
SiteListNotifierProvider call(String? pubName) {
|
||||||
|
return SiteListNotifierProvider(pubName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
SiteListNotifierProvider getProviderOverride(
|
||||||
|
covariant SiteListNotifierProvider provider,
|
||||||
|
) {
|
||||||
|
return call(provider.pubName);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||||
|
_allTransitiveDependencies;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get name => r'siteListNotifierProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [SiteListNotifier].
|
||||||
|
class SiteListNotifierProvider
|
||||||
|
extends
|
||||||
|
AutoDisposeAsyncNotifierProviderImpl<
|
||||||
|
SiteListNotifier,
|
||||||
|
CursorPagingData<SnPublicationSite>
|
||||||
|
> {
|
||||||
|
/// See also [SiteListNotifier].
|
||||||
|
SiteListNotifierProvider(String? pubName)
|
||||||
|
: this._internal(
|
||||||
|
() => SiteListNotifier()..pubName = pubName,
|
||||||
|
from: siteListNotifierProvider,
|
||||||
|
name: r'siteListNotifierProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$siteListNotifierHash,
|
||||||
|
dependencies: SiteListNotifierFamily._dependencies,
|
||||||
|
allTransitiveDependencies:
|
||||||
|
SiteListNotifierFamily._allTransitiveDependencies,
|
||||||
|
pubName: pubName,
|
||||||
|
);
|
||||||
|
|
||||||
|
SiteListNotifierProvider._internal(
|
||||||
|
super._createNotifier, {
|
||||||
|
required super.name,
|
||||||
|
required super.dependencies,
|
||||||
|
required super.allTransitiveDependencies,
|
||||||
|
required super.debugGetCreateSourceHash,
|
||||||
|
required super.from,
|
||||||
|
required this.pubName,
|
||||||
|
}) : super.internal();
|
||||||
|
|
||||||
|
final String? pubName;
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOr<CursorPagingData<SnPublicationSite>> runNotifierBuild(
|
||||||
|
covariant SiteListNotifier notifier,
|
||||||
|
) {
|
||||||
|
return notifier.build(pubName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Override overrideWith(SiteListNotifier Function() create) {
|
||||||
|
return ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
override: SiteListNotifierProvider._internal(
|
||||||
|
() => create()..pubName = pubName,
|
||||||
|
from: from,
|
||||||
|
name: null,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
debugGetCreateSourceHash: null,
|
||||||
|
pubName: pubName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AutoDisposeAsyncNotifierProviderElement<
|
||||||
|
SiteListNotifier,
|
||||||
|
CursorPagingData<SnPublicationSite>
|
||||||
|
>
|
||||||
|
createElement() {
|
||||||
|
return _SiteListNotifierProviderElement(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is SiteListNotifierProvider && other.pubName == pubName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, pubName.hashCode);
|
||||||
|
|
||||||
|
return _SystemHash.finish(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
mixin SiteListNotifierRef
|
||||||
|
on
|
||||||
|
AutoDisposeAsyncNotifierProviderRef<
|
||||||
|
CursorPagingData<SnPublicationSite>
|
||||||
|
> {
|
||||||
|
/// The parameter `pubName` of this provider.
|
||||||
|
String? get pubName;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SiteListNotifierProviderElement
|
||||||
|
extends
|
||||||
|
AutoDisposeAsyncNotifierProviderElement<
|
||||||
|
SiteListNotifier,
|
||||||
|
CursorPagingData<SnPublicationSite>
|
||||||
|
>
|
||||||
|
with SiteListNotifierRef {
|
||||||
|
_SiteListNotifierProviderElement(super.provider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get pubName => (origin as SiteListNotifierProvider).pubName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
@@ -141,7 +141,7 @@ class EditAppScreen extends HookConsumerWidget {
|
|||||||
try {
|
try {
|
||||||
final cloudFile =
|
final cloudFile =
|
||||||
await FileUploader.createCloudFile(
|
await FileUploader.createCloudFile(
|
||||||
client: ref.read(apiClientProvider),
|
ref: ref,
|
||||||
fileData: UniversalFile(
|
fileData: UniversalFile(
|
||||||
data: result,
|
data: result,
|
||||||
type: UniversalFileType.image,
|
type: UniversalFileType.image,
|
||||||
|
|||||||
@@ -127,11 +127,11 @@ class EditBotScreen extends HookConsumerWidget {
|
|||||||
try {
|
try {
|
||||||
final cloudFile =
|
final cloudFile =
|
||||||
await FileUploader.createCloudFile(
|
await FileUploader.createCloudFile(
|
||||||
|
ref: ref,
|
||||||
fileData: UniversalFile(
|
fileData: UniversalFile(
|
||||||
data: result,
|
data: result,
|
||||||
type: UniversalFileType.image,
|
type: UniversalFileType.image,
|
||||||
),
|
),
|
||||||
client: ref.read(apiClientProvider),
|
|
||||||
).future;
|
).future;
|
||||||
if (cloudFile == null) {
|
if (cloudFile == null) {
|
||||||
throw ArgumentError('Failed to upload the file...');
|
throw ArgumentError('Failed to upload the file...');
|
||||||
|
|||||||
@@ -11,15 +11,16 @@ import 'package:island/models/realm.dart';
|
|||||||
import 'package:island/models/webfeed.dart';
|
import 'package:island/models/webfeed.dart';
|
||||||
import 'package:island/pods/event_calendar.dart';
|
import 'package:island/pods/event_calendar.dart';
|
||||||
import 'package:island/pods/userinfo.dart';
|
import 'package:island/pods/userinfo.dart';
|
||||||
|
import 'package:island/screens/auth/login_modal.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/account/friends_overview.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';
|
||||||
import 'package:island/widgets/navigation/fab_menu.dart';
|
import 'package:island/widgets/navigation/fab_menu.dart';
|
||||||
import 'package:island/widgets/post/post_featured.dart';
|
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: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';
|
||||||
@@ -341,7 +342,7 @@ class ExploreScreen extends HookConsumerWidget {
|
|||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
),
|
),
|
||||||
PostFeaturedList(),
|
PostFeaturedList(),
|
||||||
const PostComposeCard(),
|
FriendsOverviewWidget(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -350,21 +351,39 @@ class ExploreScreen extends HookConsumerWidget {
|
|||||||
else
|
else
|
||||||
Flexible(
|
Flexible(
|
||||||
flex: 2,
|
flex: 2,
|
||||||
child: Column(
|
child:
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
Text(
|
children: [
|
||||||
'Welcome to\nthe Solar Network',
|
const Icon(Symbols.emoji_people_rounded, size: 40),
|
||||||
style: Theme.of(context).textTheme.titleLarge,
|
const Gap(8),
|
||||||
).bold(),
|
Text(
|
||||||
const Gap(2),
|
'Welcome to\nthe Solar Network',
|
||||||
Text(
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
'Login to explore more!',
|
textAlign: TextAlign.center,
|
||||||
style: Theme.of(context).textTheme.bodyLarge,
|
).bold(),
|
||||||
),
|
const Gap(2),
|
||||||
],
|
Text(
|
||||||
).padding(horizontal: 36, vertical: 16),
|
'Login to explore more!',
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const Gap(4),
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
useRootNavigator: true,
|
||||||
|
isScrollControlled: true,
|
||||||
|
builder: (context) => LoginModal(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: const Icon(Symbols.login),
|
||||||
|
label: Text('login').tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 36, vertical: 16).center(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
).padding(horizontal: 12);
|
).padding(horizontal: 12);
|
||||||
@@ -523,6 +542,12 @@ class ExploreScreen extends HookConsumerWidget {
|
|||||||
child: PostFeaturedList(),
|
child: PostFeaturedList(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: FriendsOverviewWidget(
|
||||||
|
padding: const EdgeInsets.only(bottom: 8),
|
||||||
|
hideWhenEmpty: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
if (notificationCount.value != null &&
|
if (notificationCount.value != null &&
|
||||||
notificationCount.value! > 0)
|
notificationCount.value! > 0)
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
|
|||||||
342
lib/screens/files/file_detail.dart
Normal file
342
lib/screens/files/file_detail.dart
Normal file
@@ -0,0 +1,342 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:file_saver/file_saver.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:gal/gal.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:island/models/file.dart';
|
||||||
|
import 'package:island/pods/config.dart';
|
||||||
|
import 'package:island/pods/file_references.dart';
|
||||||
|
import 'package:island/pods/network.dart';
|
||||||
|
import 'package:island/pods/upload_tasks.dart';
|
||||||
|
import 'package:island/models/drive_task.dart';
|
||||||
|
import 'package:island/services/responsive.dart';
|
||||||
|
import 'package:island/services/time.dart';
|
||||||
|
import 'package:island/widgets/alert.dart';
|
||||||
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
|
import 'package:island/widgets/content/file_info_sheet.dart';
|
||||||
|
import 'package:island/widgets/content/file_viewer_contents.dart';
|
||||||
|
import 'package:island/widgets/content/sheet.dart';
|
||||||
|
import 'package:path/path.dart' show extension;
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
|
||||||
|
class FileDetailScreen extends HookConsumerWidget {
|
||||||
|
final SnCloudFile item;
|
||||||
|
|
||||||
|
const FileDetailScreen({super.key, required this.item});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final serverUrl = ref.watch(serverUrlProvider);
|
||||||
|
final isWide = isWideScreen(context);
|
||||||
|
|
||||||
|
// Animation controller for the drawer
|
||||||
|
final animationController = useAnimationController(
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
);
|
||||||
|
final animation = useMemoized(
|
||||||
|
() => Tween<double>(begin: 0, end: 1).animate(
|
||||||
|
CurvedAnimation(parent: animationController, curve: Curves.easeInOut),
|
||||||
|
),
|
||||||
|
[animationController],
|
||||||
|
);
|
||||||
|
|
||||||
|
final showDrawer = useState(false);
|
||||||
|
|
||||||
|
void showInfoSheet() {
|
||||||
|
if (isWide) {
|
||||||
|
// Show as animated right panel on wide screens
|
||||||
|
showDrawer.value = !showDrawer.value;
|
||||||
|
if (showDrawer.value) {
|
||||||
|
animationController.forward();
|
||||||
|
} else {
|
||||||
|
animationController.reverse();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Show as bottom sheet on narrow screens
|
||||||
|
showModalBottomSheet(
|
||||||
|
useRootNavigator: true,
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
builder: (context) => FileInfoSheet(item: item),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen to drawer state changes
|
||||||
|
useEffect(() {
|
||||||
|
void listener() {
|
||||||
|
if (!animationController.isAnimating) {
|
||||||
|
if (animationController.value == 0) {
|
||||||
|
showDrawer.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
animationController.addListener(listener);
|
||||||
|
return () => animationController.removeListener(listener);
|
||||||
|
}, [animationController]);
|
||||||
|
|
||||||
|
return AppScaffold(
|
||||||
|
isNoBackground: false,
|
||||||
|
appBar: AppBar(
|
||||||
|
elevation: 0,
|
||||||
|
leading: IconButton(
|
||||||
|
icon: Icon(Icons.close),
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
),
|
||||||
|
title: Text(item.name.isEmpty ? 'File Details' : item.name),
|
||||||
|
actions: _buildAppBarActions(context, ref, showInfoSheet),
|
||||||
|
),
|
||||||
|
body: LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
return AnimatedBuilder(
|
||||||
|
animation: animation,
|
||||||
|
builder: (context, child) {
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
// Main content area - resizes with animation
|
||||||
|
Positioned(
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
width: constraints.maxWidth - animation.value * 400,
|
||||||
|
child: _buildContent(context, ref, serverUrl),
|
||||||
|
),
|
||||||
|
// Animated drawer panel - overlays
|
||||||
|
if (isWide)
|
||||||
|
Positioned(
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
width: 400,
|
||||||
|
child: Transform.translate(
|
||||||
|
offset: Offset((1 - animation.value) * 400, 0),
|
||||||
|
child: SizedBox(
|
||||||
|
width: 400,
|
||||||
|
child: Material(
|
||||||
|
color:
|
||||||
|
Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
elevation: 8,
|
||||||
|
child: FileInfoSheet(
|
||||||
|
item: item,
|
||||||
|
onClose: showInfoSheet,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> _buildAppBarActions(
|
||||||
|
BuildContext context,
|
||||||
|
WidgetRef ref,
|
||||||
|
VoidCallback showInfoSheet,
|
||||||
|
) {
|
||||||
|
final actions = <Widget>[];
|
||||||
|
|
||||||
|
// Add content-specific actions
|
||||||
|
switch (item.mimeType?.split('/').firstOrNull) {
|
||||||
|
case 'image':
|
||||||
|
if (!kIsWeb) {
|
||||||
|
actions.add(
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.save_alt),
|
||||||
|
onPressed: () async => _saveToGallery(ref),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// HD/SD toggle will be handled in the image content overlay
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (!kIsWeb) {
|
||||||
|
actions.add(
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.save_alt),
|
||||||
|
onPressed: () async => _downloadFile(ref),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add references button
|
||||||
|
actions.add(
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.link),
|
||||||
|
onPressed:
|
||||||
|
() => showModalBottomSheet(
|
||||||
|
useRootNavigator: true,
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
builder:
|
||||||
|
(context) => SheetScaffold(
|
||||||
|
titleText: 'File References',
|
||||||
|
child: ReferencesList(fileId: item.id),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Always add info button
|
||||||
|
actions.add(
|
||||||
|
IconButton(icon: Icon(Icons.info_outline), onPressed: showInfoSheet),
|
||||||
|
);
|
||||||
|
|
||||||
|
actions.add(const Gap(8));
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _saveToGallery(WidgetRef ref) async {
|
||||||
|
try {
|
||||||
|
showSnackBar('Saving image...');
|
||||||
|
|
||||||
|
final client = ref.read(apiClientProvider);
|
||||||
|
final tempDir = await getTemporaryDirectory();
|
||||||
|
var extName = extension(item.name).trim();
|
||||||
|
if (extName.isEmpty) {
|
||||||
|
extName = item.mimeType?.split('/').lastOrNull ?? 'jpeg';
|
||||||
|
}
|
||||||
|
final filePath = '${tempDir.path}/${item.id}.$extName';
|
||||||
|
|
||||||
|
await client.download(
|
||||||
|
'/drive/files/${item.id}',
|
||||||
|
filePath,
|
||||||
|
queryParameters: {'original': true},
|
||||||
|
);
|
||||||
|
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
|
||||||
|
await Gal.putImage(filePath, album: 'Solar Network');
|
||||||
|
showSnackBar('Image saved to gallery');
|
||||||
|
} else {
|
||||||
|
await FileSaver.instance.saveFile(
|
||||||
|
name: item.name.isEmpty ? '${item.id}.$extName' : item.name,
|
||||||
|
file: File(filePath),
|
||||||
|
);
|
||||||
|
showSnackBar('Image saved to $filePath');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showErrorAlert(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _downloadFile(WidgetRef ref) async {
|
||||||
|
final taskNotifier = ref.read(uploadTasksProvider.notifier);
|
||||||
|
final taskId = taskNotifier.addLocalDownloadTask(item);
|
||||||
|
try {
|
||||||
|
showSnackBar('Downloading file...');
|
||||||
|
|
||||||
|
final client = ref.read(apiClientProvider);
|
||||||
|
final tempDir = await getTemporaryDirectory();
|
||||||
|
var extName = extension(item.name).trim();
|
||||||
|
if (extName.isEmpty) {
|
||||||
|
extName = item.mimeType?.split('/').lastOrNull ?? 'bin';
|
||||||
|
}
|
||||||
|
final filePath = '${tempDir.path}/${item.id}.$extName';
|
||||||
|
|
||||||
|
await client.download(
|
||||||
|
'/drive/files/${item.id}',
|
||||||
|
filePath,
|
||||||
|
queryParameters: {'original': true},
|
||||||
|
onReceiveProgress: (count, total) {
|
||||||
|
if (total > 0) {
|
||||||
|
taskNotifier.updateDownloadProgress(taskId, count, total);
|
||||||
|
taskNotifier.updateTransmissionProgress(taskId, count / total);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await FileSaver.instance.saveFile(
|
||||||
|
name: item.name.isEmpty ? '${item.id}.$extName' : item.name,
|
||||||
|
file: File(filePath),
|
||||||
|
);
|
||||||
|
taskNotifier.updateTaskStatus(taskId, DriveTaskStatus.completed);
|
||||||
|
showSnackBar('File saved to downloads');
|
||||||
|
} catch (e) {
|
||||||
|
taskNotifier.updateTaskStatus(
|
||||||
|
taskId,
|
||||||
|
DriveTaskStatus.failed,
|
||||||
|
errorMessage: e.toString(),
|
||||||
|
);
|
||||||
|
showErrorAlert(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildContent(BuildContext context, WidgetRef ref, String serverUrl) {
|
||||||
|
final uri = '$serverUrl/drive/files/${item.id}';
|
||||||
|
|
||||||
|
return switch (item.mimeType?.split('/').firstOrNull) {
|
||||||
|
'image' => ImageFileContent(item: item, uri: uri),
|
||||||
|
'video' => VideoFileContent(item: item, uri: uri),
|
||||||
|
'audio' => AudioFileContent(item: item, uri: uri),
|
||||||
|
_ when item.mimeType == 'application/pdf' => PdfFileContent(uri: uri),
|
||||||
|
_ when item.mimeType?.startsWith('text/') == true => TextFileContent(
|
||||||
|
uri: uri,
|
||||||
|
),
|
||||||
|
_ => GenericFileContent(item: item),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ReferencesList extends ConsumerWidget {
|
||||||
|
const ReferencesList({super.key, required this.fileId});
|
||||||
|
|
||||||
|
final String fileId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final asyncReferences = ref.watch(fileReferencesProvider(fileId));
|
||||||
|
|
||||||
|
return asyncReferences.when(
|
||||||
|
data:
|
||||||
|
(references) => ListView.builder(
|
||||||
|
itemCount: references.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final reference = references[index];
|
||||||
|
return ListTile(
|
||||||
|
leading: const Icon(Icons.link),
|
||||||
|
title: Row(
|
||||||
|
spacing: 6,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
reference.usage,
|
||||||
|
style: GoogleFonts.robotoMono(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 13,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
reference.id,
|
||||||
|
style: GoogleFonts.robotoMono(fontSize: 13),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
subtitle: Row(
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
Text(reference.createdAt.formatRelative(context)),
|
||||||
|
const VerticalDivider(width: 1, thickness: 1).height(12),
|
||||||
|
Text(reference.createdAt.formatSystem()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
|
error:
|
||||||
|
(error, _) => Center(child: Text('Error loading references: $error')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,122 +1,73 @@
|
|||||||
|
import 'package:cross_file/cross_file.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:fl_chart/fl_chart.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/file.dart';
|
import 'package:island/models/file.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/models/file_pool.dart';
|
||||||
import 'package:island/pods/file_pool.dart';
|
import 'package:island/pods/file_list.dart';
|
||||||
import 'package:island/utils/format.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/sheet.dart';
|
||||||
import 'package:island/widgets/content/file_info_sheet.dart';
|
import 'package:island/widgets/file_list_view.dart';
|
||||||
|
import 'package:island/widgets/usage_overview.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_paging_utils/riverpod_paging_utils.dart';
|
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
|
||||||
part 'file_list.g.dart';
|
|
||||||
|
|
||||||
@riverpod
|
|
||||||
class CloudFileListNotifier extends _$CloudFileListNotifier
|
|
||||||
with CursorPagingNotifierMixin<SnCloudFile> {
|
|
||||||
String? _poolId;
|
|
||||||
bool _includeRecycled = false;
|
|
||||||
|
|
||||||
void setFilters(String? poolId, bool includeRecycled) {
|
|
||||||
_poolId = poolId;
|
|
||||||
_includeRecycled = includeRecycled;
|
|
||||||
ref.invalidateSelf();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<CursorPagingData<SnCloudFile>> build() => fetch(cursor: null);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<CursorPagingData<SnCloudFile>> fetch({required String? cursor}) async {
|
|
||||||
final client = ref.read(apiClientProvider);
|
|
||||||
final offset = cursor == null ? 0 : int.parse(cursor);
|
|
||||||
final take = 20;
|
|
||||||
|
|
||||||
final queryParameters = <String, dynamic>{'offset': offset, 'take': take};
|
|
||||||
|
|
||||||
// Add filter parameters
|
|
||||||
if (_poolId != null) {
|
|
||||||
queryParameters['pool'] = _poolId!;
|
|
||||||
}
|
|
||||||
if (_includeRecycled) {
|
|
||||||
queryParameters['recycled'] = 'true';
|
|
||||||
}
|
|
||||||
|
|
||||||
final response = await client.get(
|
|
||||||
'/drive/files/me',
|
|
||||||
queryParameters: queryParameters,
|
|
||||||
);
|
|
||||||
|
|
||||||
final List<SnCloudFile> items =
|
|
||||||
(response.data as List)
|
|
||||||
.map((e) => SnCloudFile.fromJson(e as Map<String, dynamic>))
|
|
||||||
.toList();
|
|
||||||
final total = int.parse(response.headers.value('X-Total') ?? '0');
|
|
||||||
|
|
||||||
final hasMore = offset + items.length < total;
|
|
||||||
final nextCursor = hasMore ? (offset + items.length).toString() : null;
|
|
||||||
|
|
||||||
return CursorPagingData(
|
|
||||||
items: items,
|
|
||||||
hasMore: hasMore,
|
|
||||||
nextCursor: nextCursor,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@riverpod
|
|
||||||
Future<Map<String, dynamic>?> billingUsage(Ref ref) async {
|
|
||||||
final client = ref.read(apiClientProvider);
|
|
||||||
final response = await client.get('/drive/billing/usage');
|
|
||||||
return response.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
@riverpod
|
|
||||||
Future<Map<String, dynamic>?> billingQuota(Ref ref) async {
|
|
||||||
final client = ref.read(apiClientProvider);
|
|
||||||
final response = await client.get('/drive/billing/quota');
|
|
||||||
return response.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
class FileListScreen extends HookConsumerWidget {
|
class FileListScreen extends HookConsumerWidget {
|
||||||
const FileListScreen({super.key});
|
const FileListScreen({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
// Filter state
|
// Path navigation state
|
||||||
final selectedPool = useState<String?>(null);
|
final currentPath = useState<String>('/');
|
||||||
final includeRecycled = useState(false);
|
final mode = useState<FileListMode>(FileListMode.normal);
|
||||||
|
final selectedPool = useState<SnFilePool?>(null);
|
||||||
|
|
||||||
final usageAsync = ref.watch(billingUsageProvider);
|
final usageAsync = ref.watch(billingUsageProvider);
|
||||||
final quotaAsync = ref.watch(billingQuotaProvider);
|
final quotaAsync = ref.watch(billingQuotaProvider);
|
||||||
|
|
||||||
// Update notifier filters when state changes
|
final viewMode = useState(FileListViewMode.list);
|
||||||
useEffect(() {
|
|
||||||
final notifier = ref.read(cloudFileListNotifierProvider.notifier);
|
|
||||||
notifier.setFilters(selectedPool.value, includeRecycled.value);
|
|
||||||
return null;
|
|
||||||
}, [selectedPool.value, includeRecycled.value]);
|
|
||||||
|
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(title: Text('Files'), leading: const PageBackButton()),
|
isNoBackground: false,
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('files').tr(),
|
||||||
|
leading: const PageBackButton(backTo: '/account'),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Symbols.bar_chart),
|
||||||
|
onPressed:
|
||||||
|
() => _showUsageSheet(
|
||||||
|
context,
|
||||||
|
usageAsync.value,
|
||||||
|
quotaAsync.value,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
],
|
||||||
|
),
|
||||||
body: usageAsync.when(
|
body: usageAsync.when(
|
||||||
data:
|
data:
|
||||||
(usage) => quotaAsync.when(
|
(usage) => quotaAsync.when(
|
||||||
data:
|
data:
|
||||||
(quota) => _buildQuotaUI(
|
(quota) => FileListView(
|
||||||
usage,
|
usage: usage,
|
||||||
quota,
|
quota: quota,
|
||||||
ref,
|
currentPath: currentPath,
|
||||||
selectedPool,
|
selectedPool: selectedPool,
|
||||||
includeRecycled,
|
onPickAndUpload:
|
||||||
|
() => _pickAndUploadFile(
|
||||||
|
ref,
|
||||||
|
currentPath.value,
|
||||||
|
selectedPool.value?.id,
|
||||||
|
),
|
||||||
|
onShowCreateDirectory: _showCreateDirectoryDialog,
|
||||||
|
mode: mode,
|
||||||
|
viewMode: viewMode,
|
||||||
),
|
),
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
error: (e, _) => Center(child: Text('Error loading quota')),
|
error: (e, _) => Center(child: Text('Error loading quota')),
|
||||||
@@ -127,430 +78,143 @@ class FileListScreen extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildQuotaUI(
|
Future<void> _pickAndUploadFile(
|
||||||
Map<String, dynamic>? usage,
|
|
||||||
Map<String, dynamic>? quota,
|
|
||||||
WidgetRef ref,
|
WidgetRef ref,
|
||||||
ValueNotifier<String?> selectedPool,
|
String currentPath,
|
||||||
ValueNotifier<bool> includeRecycled,
|
String? poolId,
|
||||||
) {
|
) async {
|
||||||
if (usage == null) return const SizedBox.shrink();
|
|
||||||
return CustomScrollView(
|
|
||||||
slivers: [
|
|
||||||
const SliverGap(8),
|
|
||||||
SliverToBoxAdapter(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: _buildStatCard(
|
|
||||||
'All Uploads',
|
|
||||||
'${((usage['total_usage_bytes'] as num) / (1024 * 1024 * 1024)).toStringAsFixed(3)} GiB',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: _buildStatCard(
|
|
||||||
'All Files',
|
|
||||||
'${usage['total_file_count']}',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: _buildStatCard(
|
|
||||||
'Quota',
|
|
||||||
'${usage['total_quota']} MiB',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: _buildStatCard(
|
|
||||||
'Used Quota',
|
|
||||||
'${((usage['used_quota'] as num) / (usage['total_quota'] as num) * 100).toStringAsFixed(2)}%',
|
|
||||||
progress:
|
|
||||||
(usage['used_quota'] as num) /
|
|
||||||
(usage['total_quota'] as num),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).padding(horizontal: 8),
|
|
||||||
),
|
|
||||||
SliverToBoxAdapter(
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Card(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
const Text('Pool Usage'),
|
|
||||||
SizedBox(
|
|
||||||
height: 200,
|
|
||||||
child: PieChart(_buildPoolChartData(usage)),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Card(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
const Text('Verbose Quota'),
|
|
||||||
SizedBox(
|
|
||||||
height: 200,
|
|
||||||
child: PieChart(_buildQuotaChartData(quota)),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).padding(horizontal: 8),
|
|
||||||
),
|
|
||||||
const SliverGap(8),
|
|
||||||
SliverToBoxAdapter(
|
|
||||||
child: _buildFilters(ref, selectedPool, includeRecycled),
|
|
||||||
),
|
|
||||||
const SliverGap(8),
|
|
||||||
PagingHelperSliverView(
|
|
||||||
provider: cloudFileListNotifierProvider,
|
|
||||||
futureRefreshable: cloudFileListNotifierProvider.future,
|
|
||||||
notifierRefreshable: cloudFileListNotifierProvider.notifier,
|
|
||||||
contentBuilder:
|
|
||||||
(data, widgetCount, endItemView) => SliverList.builder(
|
|
||||||
itemCount: widgetCount,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
if (index == widgetCount - 1) {
|
|
||||||
return endItemView;
|
|
||||||
}
|
|
||||||
|
|
||||||
final item = data.items[index];
|
|
||||||
final itemType = item.mimeType?.split('/').firstOrNull;
|
|
||||||
return ListTile(
|
|
||||||
leading: ClipRRect(
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
|
||||||
child: SizedBox(
|
|
||||||
height: 48,
|
|
||||||
width: 48,
|
|
||||||
child: switch (itemType) {
|
|
||||||
'image' => CloudImageWidget(file: item),
|
|
||||||
'audio' =>
|
|
||||||
const Icon(Symbols.audio_file, fill: 1).center(),
|
|
||||||
'video' =>
|
|
||||||
const Icon(Symbols.video_file, fill: 1).center(),
|
|
||||||
_ =>
|
|
||||||
const Icon(Symbols.body_system, fill: 1).center(),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
title:
|
|
||||||
item.name.isEmpty
|
|
||||||
? Text('untitled').tr().italic()
|
|
||||||
: Text(
|
|
||||||
item.name,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
subtitle: Text(formatFileSize(item.size)),
|
|
||||||
onTap: () {
|
|
||||||
showModalBottomSheet(
|
|
||||||
useRootNavigator: true,
|
|
||||||
context: context,
|
|
||||||
isScrollControlled: true,
|
|
||||||
builder: (context) => FileInfoSheet(item: item),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
trailing: IconButton(
|
|
||||||
icon: const Icon(Symbols.delete),
|
|
||||||
onPressed: () async {
|
|
||||||
final confirmed = await showConfirmAlert(
|
|
||||||
'confirmDeleteFile'.tr(),
|
|
||||||
'deleteFile'.tr(),
|
|
||||||
);
|
|
||||||
if (!confirmed) return;
|
|
||||||
|
|
||||||
if (context.mounted) showLoadingModal(context);
|
|
||||||
try {
|
|
||||||
final client = ref.read(apiClientProvider);
|
|
||||||
await client.delete('/drive/files/${item.id}');
|
|
||||||
ref.invalidate(cloudFileListNotifierProvider);
|
|
||||||
} catch (e) {
|
|
||||||
showSnackBar('failedToDeleteFile'.tr());
|
|
||||||
} finally {
|
|
||||||
if (context.mounted) hideLoadingModal(context);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
PieChartData _buildPoolChartData(Map<String, dynamic> usage) {
|
|
||||||
final pools = usage['pool_usages'] as List<dynamic>;
|
|
||||||
final colors = [
|
|
||||||
Colors.blue,
|
|
||||||
Colors.green,
|
|
||||||
Colors.orange,
|
|
||||||
Colors.red,
|
|
||||||
Colors.purple,
|
|
||||||
];
|
|
||||||
return PieChartData(
|
|
||||||
sections:
|
|
||||||
pools.asMap().entries.map((entry) {
|
|
||||||
final pool = entry.value as Map<String, dynamic>;
|
|
||||||
final title = pool['pool_name'] as String;
|
|
||||||
final truncatedTitle =
|
|
||||||
title.length > 8 ? '${title.substring(0, 8)}...' : title;
|
|
||||||
return PieChartSectionData(
|
|
||||||
value: (pool['usage_bytes'] as num).toDouble(),
|
|
||||||
title: truncatedTitle,
|
|
||||||
color: colors[entry.key % colors.length],
|
|
||||||
radius: 60,
|
|
||||||
titleStyle: const TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: Colors.white,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
PieChartData _buildQuotaChartData(Map<String, dynamic>? quota) {
|
|
||||||
if (quota == null) return PieChartData(sections: []);
|
|
||||||
return PieChartData(
|
|
||||||
sections: [
|
|
||||||
PieChartSectionData(
|
|
||||||
value: (quota['based_quota'] as num).toDouble(),
|
|
||||||
title: 'Base',
|
|
||||||
color: Colors.green,
|
|
||||||
radius: 60,
|
|
||||||
titleStyle: const TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: Colors.white,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
PieChartSectionData(
|
|
||||||
value: (quota['extra_quota'] as num).toDouble(),
|
|
||||||
title: 'Extra',
|
|
||||||
color: Colors.orange,
|
|
||||||
radius: 60,
|
|
||||||
titleStyle: const TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: Colors.white,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildFilters(
|
|
||||||
WidgetRef ref,
|
|
||||||
ValueNotifier<String?> selectedPool,
|
|
||||||
ValueNotifier<bool> includeRecycled,
|
|
||||||
) {
|
|
||||||
final poolsAsync = ref.watch(poolsProvider);
|
|
||||||
|
|
||||||
return Card(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'filters'.tr(),
|
|
||||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
const Gap(16),
|
|
||||||
LayoutBuilder(
|
|
||||||
builder: (context, constraints) {
|
|
||||||
final isWide = constraints.maxWidth > 600;
|
|
||||||
return isWide
|
|
||||||
? Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
flex: 2,
|
|
||||||
child: poolsAsync.when(
|
|
||||||
data:
|
|
||||||
(pools) => DropdownButtonFormField<String?>(
|
|
||||||
value: selectedPool.value,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: 'Pool',
|
|
||||||
border: const OutlineInputBorder(),
|
|
||||||
),
|
|
||||||
items: [
|
|
||||||
DropdownMenuItem<String?>(
|
|
||||||
value: null,
|
|
||||||
child: Text('allPools'.tr()),
|
|
||||||
),
|
|
||||||
...pools.map(
|
|
||||||
(pool) => DropdownMenuItem<String?>(
|
|
||||||
value: pool.id,
|
|
||||||
child: Text(pool.name),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
onChanged:
|
|
||||||
(value) => selectedPool.value = value,
|
|
||||||
),
|
|
||||||
loading: () => const CircularProgressIndicator(),
|
|
||||||
error: (e, _) => const Text('Error loading pools'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Gap(8),
|
|
||||||
Expanded(
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Text('includeRecycled'.tr()),
|
|
||||||
const Gap(8),
|
|
||||||
Switch(
|
|
||||||
value: includeRecycled.value,
|
|
||||||
onChanged:
|
|
||||||
(value) => includeRecycled.value = value,
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Gap(16),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Symbols.delete_sweep),
|
|
||||||
tooltip: 'deleteRecycledFiles'.tr(),
|
|
||||||
onPressed:
|
|
||||||
includeRecycled.value
|
|
||||||
? () => _deleteRecycledFiles(ref)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
poolsAsync.when(
|
|
||||||
data:
|
|
||||||
(pools) => DropdownButtonFormField<String?>(
|
|
||||||
value: selectedPool.value,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Pool',
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
),
|
|
||||||
items: [
|
|
||||||
DropdownMenuItem<String?>(
|
|
||||||
value: null,
|
|
||||||
child: Text('allPools'.tr()),
|
|
||||||
),
|
|
||||||
...pools.map(
|
|
||||||
(pool) => DropdownMenuItem<String?>(
|
|
||||||
value: pool.id,
|
|
||||||
child: Text(pool.name),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
onChanged:
|
|
||||||
(value) => selectedPool.value = value,
|
|
||||||
),
|
|
||||||
loading: () => const CircularProgressIndicator(),
|
|
||||||
error: (e, _) => const Text('Error loading pools'),
|
|
||||||
),
|
|
||||||
const Gap(16),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Text('includeRecycled'.tr()),
|
|
||||||
const Gap(8),
|
|
||||||
Switch(
|
|
||||||
value: includeRecycled.value,
|
|
||||||
onChanged:
|
|
||||||
(value) => includeRecycled.value = value,
|
|
||||||
),
|
|
||||||
const Spacer(),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Symbols.delete_sweep),
|
|
||||||
tooltip: 'deleteRecycledFiles'.tr(),
|
|
||||||
onPressed:
|
|
||||||
includeRecycled.value
|
|
||||||
? () => _deleteRecycledFiles(ref)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
).padding(horizontal: 8);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _deleteRecycledFiles(WidgetRef ref) async {
|
|
||||||
final confirmed = await showConfirmAlert(
|
|
||||||
'confirmDeleteRecycledFiles'.tr(),
|
|
||||||
'deleteRecycledFiles'.tr(),
|
|
||||||
);
|
|
||||||
if (!confirmed) return;
|
|
||||||
|
|
||||||
if (ref.context.mounted) showLoadingModal(ref.context);
|
|
||||||
try {
|
try {
|
||||||
final client = ref.read(apiClientProvider);
|
final result = await FilePicker.platform.pickFiles(
|
||||||
await client.delete('/drive/files/recycled');
|
allowMultiple: true,
|
||||||
ref.invalidate(cloudFileListNotifierProvider);
|
withData: false,
|
||||||
showSnackBar('recycledFilesDeleted'.tr());
|
);
|
||||||
|
|
||||||
|
if (result != null && result.files.isNotEmpty) {
|
||||||
|
for (final file in result.files) {
|
||||||
|
if (file.path != null) {
|
||||||
|
// Create UniversalFile from the picked file
|
||||||
|
final universalFile = UniversalFile(
|
||||||
|
data: XFile(file.path!),
|
||||||
|
type: UniversalFileType.file,
|
||||||
|
displayName: file.name,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Upload the file with the current path
|
||||||
|
final completer = FileUploader.createCloudFile(
|
||||||
|
fileData: universalFile,
|
||||||
|
ref: ref,
|
||||||
|
path: currentPath,
|
||||||
|
poolId: poolId,
|
||||||
|
onProgress: (progress, _) {
|
||||||
|
// Progress is handled by the upload tasks system
|
||||||
|
if (progress != null) {
|
||||||
|
debugPrint('Upload progress: ${(progress * 100).toInt()}%');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
completer.future
|
||||||
|
.then((uploadedFile) {
|
||||||
|
if (uploadedFile != null) {
|
||||||
|
ref.invalidate(cloudFileListNotifierProvider);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catchError((error) {
|
||||||
|
showSnackBar('Failed to upload file: $error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showSnackBar('failedToDeleteRecycledFiles'.tr());
|
showSnackBar('Error picking file: $e');
|
||||||
} finally {
|
|
||||||
if (ref.context.mounted) hideLoadingModal(ref.context);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildStatCard(String label, String value, {double? progress}) {
|
Future<void> _showCreateDirectoryDialog(
|
||||||
return Card(
|
BuildContext context,
|
||||||
child: Padding(
|
ValueNotifier<String> currentPath,
|
||||||
padding: const EdgeInsets.all(16),
|
) async {
|
||||||
child: Column(
|
final controller = TextEditingController(text: currentPath.value);
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
String? newPath;
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
void handleChangeDirectory(BuildContext context) {
|
||||||
Text(label, style: const TextStyle(fontSize: 14)),
|
newPath = controller.text.trim();
|
||||||
Row(
|
if (newPath!.isNotEmpty) {
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
// Normalize the path
|
||||||
|
String fullPath = newPath!;
|
||||||
|
|
||||||
|
// Ensure it starts with /
|
||||||
|
if (!fullPath.startsWith('/')) {
|
||||||
|
fullPath = '/$fullPath';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove double slashes and normalize
|
||||||
|
fullPath = fullPath.replaceAll(RegExp(r'/+'), '/');
|
||||||
|
|
||||||
|
currentPath.value = fullPath;
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await showDialog(
|
||||||
|
context: context,
|
||||||
|
builder:
|
||||||
|
(context) => AlertDialog(
|
||||||
|
title: const Text('Navigate to Directory'),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
const Gap(8),
|
||||||
value,
|
TextField(
|
||||||
style: const TextStyle(
|
controller: controller,
|
||||||
fontSize: 24,
|
decoration: const InputDecoration(
|
||||||
fontWeight: FontWeight.bold,
|
labelText: 'Directory path',
|
||||||
|
hintText: 'e.g., documents, projects/my-app',
|
||||||
|
helperText:
|
||||||
|
'Enter a directory path. The directory will be created when you upload files to it.',
|
||||||
|
helperMaxLines: 3,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
onSubmitted: (_) {
|
||||||
|
handleChangeDirectory(context);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
if (progress != null) ...[
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
SizedBox(
|
|
||||||
width: 28,
|
|
||||||
height: 28,
|
|
||||||
child: CircularProgressIndicator(value: progress),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
actions: [
|
||||||
),
|
TextButton(
|
||||||
),
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: () => handleChangeDirectory(context),
|
||||||
|
label: const Text('Go to Directory'),
|
||||||
|
icon: const Icon(Symbols.arrow_right_alt),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showUsageSheet(
|
||||||
|
BuildContext context,
|
||||||
|
Map<String, dynamic>? usage,
|
||||||
|
Map<String, dynamic>? quota,
|
||||||
|
) {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
builder:
|
||||||
|
(context) => SheetScaffold(
|
||||||
|
titleText: 'Usage Overview',
|
||||||
|
child: UsageOverviewWidget(
|
||||||
|
usage: usage,
|
||||||
|
quota: quota,
|
||||||
|
).padding(horizontal: 8, vertical: 16),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -534,7 +534,7 @@ class _LotteryPurchaseSheetState extends State<LotteryPurchaseSheet> {
|
|||||||
),
|
),
|
||||||
const Gap(4),
|
const Gap(4),
|
||||||
Text(
|
Text(
|
||||||
'The last selected number will be your special number.',
|
'lotteryLastNumberSpecial'.tr(),
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
color: Theme.of(context).colorScheme.secondary,
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
@@ -738,11 +738,11 @@ class _LotteryPurchaseSheetState extends State<LotteryPurchaseSheet> {
|
|||||||
},
|
},
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value == null || value.isEmpty) {
|
if (value == null || value.isEmpty) {
|
||||||
return 'Please enter a multiplier';
|
return 'lotteryMultiplierRequired'.tr();
|
||||||
}
|
}
|
||||||
final parsed = int.tryParse(value);
|
final parsed = int.tryParse(value);
|
||||||
if (parsed == null || parsed < 1 || parsed > 10) {
|
if (parsed == null || parsed < 1 || parsed > 10) {
|
||||||
return 'Multiplier must be between 1 and 10';
|
return 'lotteryMultiplierRange'.tr();
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import 'package:island/pods/network.dart';
|
|||||||
import 'package:island/pods/websocket.dart';
|
import 'package:island/pods/websocket.dart';
|
||||||
import 'package:island/route.dart';
|
import 'package:island/route.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
|
import 'package:island/widgets/content/cloud_files.dart';
|
||||||
import 'package:island/widgets/content/markdown.dart';
|
import 'package:island/widgets/content/markdown.dart';
|
||||||
import 'package:island/widgets/content/sheet.dart';
|
import 'package:island/widgets/content/sheet.dart';
|
||||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||||
@@ -68,6 +69,16 @@ class NotificationUnreadCountNotifier
|
|||||||
void clear() async {
|
void clear() async {
|
||||||
state = AsyncData(0);
|
state = AsyncData(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> refresh() async {
|
||||||
|
try {
|
||||||
|
final client = ref.read(apiClientProvider);
|
||||||
|
final response = await client.get('/ring/notifications/count');
|
||||||
|
state = AsyncData((response.data as num).toInt());
|
||||||
|
} catch (_) {
|
||||||
|
// Keep the current state if refresh fails
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
@@ -115,8 +126,36 @@ class NotificationListNotifier extends _$NotificationListNotifier
|
|||||||
class NotificationSheet extends HookConsumerWidget {
|
class NotificationSheet extends HookConsumerWidget {
|
||||||
const NotificationSheet({super.key});
|
const NotificationSheet({super.key});
|
||||||
|
|
||||||
|
IconData _getNotificationIcon(String topic) {
|
||||||
|
switch (topic) {
|
||||||
|
case 'post.replies':
|
||||||
|
return Symbols.reply;
|
||||||
|
case 'wallet.transactions':
|
||||||
|
return Symbols.account_balance_wallet;
|
||||||
|
case 'relationships.friends.request':
|
||||||
|
return Symbols.person_add;
|
||||||
|
case 'invites.chat':
|
||||||
|
return Symbols.chat;
|
||||||
|
case 'invites.realm':
|
||||||
|
return Symbols.domain;
|
||||||
|
case 'auth.login':
|
||||||
|
return Symbols.login;
|
||||||
|
case 'posts.new':
|
||||||
|
return Symbols.post_add;
|
||||||
|
case 'wallet.orders.paid':
|
||||||
|
return Symbols.shopping_bag;
|
||||||
|
case 'posts.reactions.new':
|
||||||
|
return Symbols.add_reaction;
|
||||||
|
default:
|
||||||
|
return Symbols.notifications;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
// Refresh unread count when sheet opens to sync across devices
|
||||||
|
ref.read(notificationUnreadCountNotifierProvider.notifier).refresh();
|
||||||
|
|
||||||
Future<void> markAllRead() async {
|
Future<void> markAllRead() async {
|
||||||
showLoadingModal(context);
|
showLoadingModal(context);
|
||||||
final apiClient = ref.watch(apiClientProvider);
|
final apiClient = ref.watch(apiClientProvider);
|
||||||
@@ -149,12 +188,30 @@ class NotificationSheet extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final notification = data.items[index];
|
final notification = data.items[index];
|
||||||
|
final pfp = notification.meta['pfp'] as String?;
|
||||||
|
final images = notification.meta['images'] as List?;
|
||||||
|
final imageIds = images?.cast<String>() ?? [];
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
isThreeLine: true,
|
isThreeLine: true,
|
||||||
contentPadding: EdgeInsets.symmetric(
|
contentPadding: EdgeInsets.symmetric(
|
||||||
horizontal: 16,
|
horizontal: 16,
|
||||||
vertical: 8,
|
vertical: 8,
|
||||||
),
|
),
|
||||||
|
leading:
|
||||||
|
pfp != null
|
||||||
|
? ProfilePictureWidget(fileId: pfp, radius: 20)
|
||||||
|
: CircleAvatar(
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).colorScheme.primaryContainer,
|
||||||
|
child: Icon(
|
||||||
|
_getNotificationIcon(notification.topic),
|
||||||
|
color:
|
||||||
|
Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.onPrimaryContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
title: Text(notification.title),
|
title: Text(notification.title),
|
||||||
subtitle: Column(
|
subtitle: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
@@ -187,6 +244,29 @@ class NotificationSheet extends HookConsumerWidget {
|
|||||||
).colorScheme.onSurface.withOpacity(0.8),
|
).colorScheme.onSurface.withOpacity(0.8),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (imageIds.isNotEmpty)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8),
|
||||||
|
child: Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 8,
|
||||||
|
children:
|
||||||
|
imageIds.map((imageId) {
|
||||||
|
return SizedBox(
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: CloudImageWidget(
|
||||||
|
fileId: imageId,
|
||||||
|
aspectRatio: 1,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
trailing:
|
trailing:
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import 'package:island/pods/network.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:island/models/poll.dart';
|
import 'package:island/models/poll.dart';
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
import 'package:island/widgets/content/sheet.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
@@ -393,7 +393,7 @@ class PollEditorScreen extends ConsumerWidget {
|
|||||||
showSnackBar(isUpdate ? 'pollUpdated'.tr() : 'pollCreated'.tr());
|
showSnackBar(isUpdate ? 'pollUpdated'.tr() : 'pollCreated'.tr());
|
||||||
|
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
Navigator.of(context).maybePop(res.data);
|
Navigator.of(context).maybePop(SnPoll.fromJson(res.data));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showErrorAlert(e);
|
showErrorAlert(e);
|
||||||
}
|
}
|
||||||
@@ -415,23 +415,46 @@ class PollEditorScreen extends ConsumerWidget {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return AppScaffold(
|
return SheetScaffold(
|
||||||
isNoBackground: false,
|
titleText: model.id == null ? 'pollCreate'.tr() : 'pollEdit'.tr(),
|
||||||
appBar: AppBar(
|
actions: [
|
||||||
title: Text(model.id == null ? 'pollCreate'.tr() : 'pollEdit'.tr()),
|
if (kDebugMode)
|
||||||
actions: [
|
IconButton(
|
||||||
if (kDebugMode)
|
tooltip: 'pollPreviewJsonDebug'.tr(),
|
||||||
IconButton(
|
onPressed: () {
|
||||||
tooltip: 'pollPreviewJsonDebug'.tr(),
|
_showDebugPreview(context, model);
|
||||||
onPressed: () {
|
},
|
||||||
_showDebugPreview(context, model);
|
icon: const Icon(Icons.visibility_outlined),
|
||||||
},
|
),
|
||||||
icon: const Icon(Icons.visibility_outlined),
|
],
|
||||||
),
|
heightFactor: 0.9,
|
||||||
const Gap(8),
|
onClose: () async {
|
||||||
],
|
final confirmed = await showDialog<bool>(
|
||||||
),
|
context: context,
|
||||||
body: Column(
|
builder:
|
||||||
|
(ctx) => AlertDialog(
|
||||||
|
title: Text('confirm'.tr()),
|
||||||
|
content: Text('pollConfirmDiscard'.tr()),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(ctx).pop(false),
|
||||||
|
child: Text('cancel'.tr()),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(ctx).pop(true),
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
foregroundColor: Theme.of(ctx).colorScheme.error,
|
||||||
|
),
|
||||||
|
child: Text('discard'.tr()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (confirmed == true) {
|
||||||
|
if (context.mounted) Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
|
|||||||
@@ -306,7 +306,7 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
ValueListenableBuilder<Map<int, double>>(
|
ValueListenableBuilder<Map<int, double?>>(
|
||||||
valueListenable: state.attachmentProgress,
|
valueListenable: state.attachmentProgress,
|
||||||
builder: (context, progressMap, _) {
|
builder: (context, progressMap, _) {
|
||||||
return Wrap(
|
return Wrap(
|
||||||
|
|||||||
@@ -237,7 +237,9 @@ class PostSearchScreen extends HookConsumerWidget {
|
|||||||
controller: pubNameController,
|
controller: pubNameController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'pubName'.tr(),
|
labelText: 'pubName'.tr(),
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onChanged:
|
onChanged:
|
||||||
(value) => onSearchWithFilters(searchController.text),
|
(value) => onSearchWithFilters(searchController.text),
|
||||||
@@ -247,7 +249,9 @@ class PostSearchScreen extends HookConsumerWidget {
|
|||||||
controller: realmController,
|
controller: realmController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'realm'.tr(),
|
labelText: 'realm'.tr(),
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onChanged:
|
onChanged:
|
||||||
(value) => onSearchWithFilters(searchController.text),
|
(value) => onSearchWithFilters(searchController.text),
|
||||||
|
|||||||
@@ -73,10 +73,33 @@ class _PublisherBasisWidget extends StatelessWidget {
|
|||||||
Positioned(
|
Positioned(
|
||||||
bottom: -24,
|
bottom: -24,
|
||||||
left: 16,
|
left: 16,
|
||||||
child: ProfilePictureWidget(
|
child: GestureDetector(
|
||||||
file: data.picture,
|
child: Badge(
|
||||||
radius: 32,
|
isLabelVisible: data.type == 0,
|
||||||
borderRadius: data.type == 0 ? null : 12,
|
padding: EdgeInsets.all(3),
|
||||||
|
label: Icon(
|
||||||
|
Symbols.launch,
|
||||||
|
size: 12,
|
||||||
|
color: Theme.of(context).colorScheme.onPrimary,
|
||||||
|
),
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).colorScheme.primary,
|
||||||
|
offset: Offset(0, 48),
|
||||||
|
child: ProfilePictureWidget(
|
||||||
|
file: data.picture,
|
||||||
|
radius: 32,
|
||||||
|
borderRadius: data.type == 0 ? null : 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
if (data.account?.name != null) {
|
||||||
|
Navigator.pop(context, true);
|
||||||
|
context.pushNamed(
|
||||||
|
'accountProfile',
|
||||||
|
pathParameters: {'name': data.account!.name},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ class EditRealmScreen extends HookConsumerWidget {
|
|||||||
try {
|
try {
|
||||||
final cloudFile =
|
final cloudFile =
|
||||||
await FileUploader.createCloudFile(
|
await FileUploader.createCloudFile(
|
||||||
client: ref.read(apiClientProvider),
|
ref: ref,
|
||||||
fileData: UniversalFile(
|
fileData: UniversalFile(
|
||||||
data: result,
|
data: result,
|
||||||
type: UniversalFileType.image,
|
type: UniversalFileType.image,
|
||||||
|
|||||||
@@ -5,13 +5,16 @@ import 'package:flutter/material.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:go_router/go_router.dart';
|
import 'package:go_router/go_router.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/content/cloud_files.dart';
|
||||||
import 'package:island/widgets/navigation/conditional_bottom_nav.dart';
|
import 'package:island/widgets/navigation/conditional_bottom_nav.dart';
|
||||||
import 'package:island/widgets/navigation/fab_menu.dart';
|
import 'package:island/widgets/navigation/fab_menu.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:island/pods/config.dart';
|
import 'package:island/pods/config.dart';
|
||||||
|
import 'package:island/pods/chat/chat_summary.dart';
|
||||||
|
|
||||||
final currentRouteProvider = StateProvider<String?>((ref) => null);
|
final currentRouteProvider = StateProvider<String?>((ref) => null);
|
||||||
|
|
||||||
@@ -21,6 +24,8 @@ const kTabRoutes = [
|
|||||||
'/chat',
|
'/chat',
|
||||||
'/realms',
|
'/realms',
|
||||||
'/account',
|
'/account',
|
||||||
|
'/files',
|
||||||
|
'/thought',
|
||||||
'/creators',
|
'/creators',
|
||||||
'/developers',
|
'/developers',
|
||||||
];
|
];
|
||||||
@@ -46,6 +51,8 @@ class TabsScreen extends HookConsumerWidget {
|
|||||||
notificationUnreadCountNotifierProvider,
|
notificationUnreadCountNotifierProvider,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final chatUnreadCount = ref.watch(chatUnreadCountNotifierProvider);
|
||||||
|
|
||||||
final wideScreen = isWideScreen(context);
|
final wideScreen = isWideScreen(context);
|
||||||
|
|
||||||
final destinations = [
|
final destinations = [
|
||||||
@@ -55,7 +62,11 @@ class TabsScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
NavigationDestination(
|
NavigationDestination(
|
||||||
label: 'chat'.tr(),
|
label: 'chat'.tr(),
|
||||||
icon: const Icon(Symbols.forum_rounded),
|
icon: Badge.count(
|
||||||
|
count: chatUnreadCount.value ?? 0,
|
||||||
|
isLabelVisible: (chatUnreadCount.value ?? 0) > 0,
|
||||||
|
child: const Icon(Symbols.forum_rounded),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
NavigationDestination(
|
NavigationDestination(
|
||||||
label: 'realms'.tr(),
|
label: 'realms'.tr(),
|
||||||
@@ -66,19 +77,40 @@ class TabsScreen extends HookConsumerWidget {
|
|||||||
icon: Badge.count(
|
icon: Badge.count(
|
||||||
count: notificationUnreadCount.value ?? 0,
|
count: notificationUnreadCount.value ?? 0,
|
||||||
isLabelVisible: (notificationUnreadCount.value ?? 0) > 0,
|
isLabelVisible: (notificationUnreadCount.value ?? 0) > 0,
|
||||||
child: const Icon(Symbols.person_rounded),
|
child: Consumer(
|
||||||
|
child: const Icon(Symbols.account_circle_rounded),
|
||||||
|
builder: (context, ref, fallbackChild) {
|
||||||
|
final userInfo = ref.watch(userInfoProvider);
|
||||||
|
if (userInfo.value?.profile.picture != null) {
|
||||||
|
return ProfilePictureWidget(
|
||||||
|
file: userInfo.value!.profile.picture,
|
||||||
|
radius: 12,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return fallbackChild!;
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (wideScreen)
|
if (wideScreen)
|
||||||
NavigationDestination(
|
...([
|
||||||
label: 'creatorHub'.tr(),
|
NavigationDestination(
|
||||||
icon: const Icon(Symbols.design_services_rounded),
|
label: 'files'.tr(),
|
||||||
),
|
icon: const Icon(Symbols.folder_rounded),
|
||||||
if (wideScreen)
|
),
|
||||||
NavigationDestination(
|
NavigationDestination(
|
||||||
label: 'developerHub'.tr(),
|
label: 'aiThought'.tr(),
|
||||||
icon: const Icon(Symbols.data_object_rounded),
|
icon: const Icon(Symbols.bubble_chart),
|
||||||
),
|
),
|
||||||
|
NavigationDestination(
|
||||||
|
label: 'creatorHub'.tr(),
|
||||||
|
icon: const Icon(Symbols.design_services_rounded),
|
||||||
|
),
|
||||||
|
NavigationDestination(
|
||||||
|
label: 'developerHub'.tr(),
|
||||||
|
icon: const Icon(Symbols.data_object_rounded),
|
||||||
|
),
|
||||||
|
]),
|
||||||
];
|
];
|
||||||
|
|
||||||
int getCurrentIndex() {
|
int getCurrentIndex() {
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
import "dart:convert";
|
|
||||||
import "dart:math" as math;
|
|
||||||
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:flutter_hooks/flutter_hooks.dart";
|
import "package:flutter_hooks/flutter_hooks.dart";
|
||||||
@@ -9,18 +6,22 @@ import "package:riverpod_annotation/riverpod_annotation.dart";
|
|||||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||||
import "package:island/models/thought.dart";
|
import "package:island/models/thought.dart";
|
||||||
import "package:island/pods/network.dart";
|
import "package:island/pods/network.dart";
|
||||||
import "package:island/pods/userinfo.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/response.dart";
|
import "package:island/widgets/response.dart";
|
||||||
import "package:island/widgets/thought/thought_sequence_list.dart";
|
import "package:island/widgets/thought/thought_sequence_list.dart";
|
||||||
import "package:island/widgets/thought/thought_shared.dart";
|
import "package:island/widgets/thought/thought_shared.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:collection/collection.dart";
|
|
||||||
|
|
||||||
part 'think.g.dart';
|
part 'think.g.dart';
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
Future<bool> thoughtAvailableStaus(Ref ref) async {
|
||||||
|
final apiClient = ref.watch(apiClientProvider);
|
||||||
|
final response = await apiClient.get('/insight/billing/status');
|
||||||
|
return response.data['status'] == 'ok';
|
||||||
|
}
|
||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
Future<List<SnThinkingThought>> thoughtSequence(
|
Future<List<SnThinkingThought>> thoughtSequence(
|
||||||
Ref ref,
|
Ref ref,
|
||||||
@@ -35,6 +36,13 @@ Future<List<SnThinkingThought>> thoughtSequence(
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
Future<ThoughtServicesResponse> thoughtServices(Ref ref) async {
|
||||||
|
final apiClient = ref.watch(apiClientProvider);
|
||||||
|
final response = await apiClient.get('/insight/thought/services');
|
||||||
|
return ThoughtServicesResponse.fromJson(response.data);
|
||||||
|
}
|
||||||
|
|
||||||
class ThoughtScreen extends HookConsumerWidget {
|
class ThoughtScreen extends HookConsumerWidget {
|
||||||
const ThoughtScreen({super.key});
|
const ThoughtScreen({super.key});
|
||||||
|
|
||||||
@@ -46,203 +54,32 @@ class ThoughtScreen extends HookConsumerWidget {
|
|||||||
? ref.watch(thoughtSequenceProvider(selectedSequenceId.value!))
|
? ref.watch(thoughtSequenceProvider(selectedSequenceId.value!))
|
||||||
: const AsyncValue<List<SnThinkingThought>>.data([]);
|
: const AsyncValue<List<SnThinkingThought>>.data([]);
|
||||||
|
|
||||||
final localThoughts = useState<List<SnThinkingThought>>([]);
|
// Extract sequence ID from loaded thoughts for the chat interface
|
||||||
final currentTopic = useState<String?>('aiThought'.tr());
|
final sequenceIdFromThoughts = thoughts.maybeWhen(
|
||||||
|
data: (thoughts) {
|
||||||
final messageController = useTextEditingController();
|
if (thoughts.isNotEmpty && thoughts.first.sequenceId.isNotEmpty) {
|
||||||
final scrollController = useScrollController();
|
return thoughts.first.sequenceId;
|
||||||
final isStreaming = useState(false);
|
|
||||||
final streamingText = useState<String>('');
|
|
||||||
final functionCalls = useState<List<String>>([]);
|
|
||||||
final reasoningChunks = useState<List<String>>([]);
|
|
||||||
|
|
||||||
final listController = useMemoized(() => ListController(), []);
|
|
||||||
|
|
||||||
// Scroll animation notifiers
|
|
||||||
final bottomGradientNotifier = useState(ValueNotifier<double>(0.0));
|
|
||||||
|
|
||||||
// Update local thoughts when provider data changes
|
|
||||||
useEffect(() {
|
|
||||||
thoughts.whenData((data) {
|
|
||||||
// Server returns messages in DESC order (newest first), keep as-is for UI
|
|
||||||
localThoughts.value = data;
|
|
||||||
// Update topic from the first thought's sequence
|
|
||||||
if (data.isNotEmpty && data.first.sequence?.topic != null) {
|
|
||||||
currentTopic.value = data.first.sequence!.topic;
|
|
||||||
} else {
|
|
||||||
currentTopic.value = 'aiThought'.tr();
|
|
||||||
}
|
}
|
||||||
});
|
return null;
|
||||||
return null;
|
},
|
||||||
}, [thoughts]);
|
orElse: () => null,
|
||||||
|
);
|
||||||
|
|
||||||
// Scroll to bottom when thoughts change or streaming state changes
|
// Get initial thoughts and topic from provider
|
||||||
useEffect(() {
|
final initialThoughts = thoughts.valueOrNull;
|
||||||
if (localThoughts.value.isNotEmpty || isStreaming.value) {
|
final initialTopic =
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
(initialThoughts?.isNotEmpty ?? false) &&
|
||||||
scrollController.animateTo(
|
initialThoughts!.first.sequence?.topic != null
|
||||||
0,
|
? initialThoughts.first.sequence!.topic
|
||||||
duration: const Duration(milliseconds: 300),
|
: 'aiThought'.tr();
|
||||||
curve: Curves.easeOut,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}, [localThoughts.value.length, isStreaming.value]);
|
|
||||||
|
|
||||||
// Add scroll listener for gradient animations
|
final statusAsync = ref.watch(thoughtAvailableStausProvider);
|
||||||
useEffect(() {
|
|
||||||
void onScroll() {
|
|
||||||
// Update gradient animations
|
|
||||||
final pixels = scrollController.position.pixels;
|
|
||||||
|
|
||||||
// Bottom gradient: appears when not at bottom (pixels > 0)
|
|
||||||
bottomGradientNotifier.value.value = (pixels / 500.0).clamp(0.0, 1.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
scrollController.addListener(onScroll);
|
|
||||||
return () => scrollController.removeListener(onScroll);
|
|
||||||
}, [scrollController]);
|
|
||||||
|
|
||||||
void sendMessage() async {
|
|
||||||
if (messageController.text.trim().isEmpty) return;
|
|
||||||
|
|
||||||
final userMessage = messageController.text.trim();
|
|
||||||
|
|
||||||
// Add user message to local thoughts
|
|
||||||
final userInfo = ref.read(userInfoProvider);
|
|
||||||
final now = DateTime.now();
|
|
||||||
final userThought = SnThinkingThought(
|
|
||||||
id: 'user-${DateTime.now().millisecondsSinceEpoch}',
|
|
||||||
content: userMessage,
|
|
||||||
files: [],
|
|
||||||
role: ThinkingThoughtRole.user,
|
|
||||||
sequenceId: selectedSequenceId.value ?? '',
|
|
||||||
createdAt: now,
|
|
||||||
updatedAt: now,
|
|
||||||
sequence:
|
|
||||||
selectedSequenceId.value != null
|
|
||||||
? thoughts.value?.firstOrNull?.sequence ??
|
|
||||||
SnThinkingSequence(
|
|
||||||
id: selectedSequenceId.value!,
|
|
||||||
accountId: '',
|
|
||||||
createdAt: now,
|
|
||||||
updatedAt: now,
|
|
||||||
)
|
|
||||||
: SnThinkingSequence(
|
|
||||||
id: '',
|
|
||||||
accountId: userInfo.value!.id,
|
|
||||||
createdAt: now,
|
|
||||||
updatedAt: now,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
localThoughts.value = [userThought, ...localThoughts.value];
|
|
||||||
|
|
||||||
final request = StreamThinkingRequest(
|
|
||||||
userMessage: userMessage,
|
|
||||||
sequenceId: selectedSequenceId.value,
|
|
||||||
accpetProposals: ['post_create'],
|
|
||||||
attachedMessages: [], // Message datas
|
|
||||||
attachedPosts: [], // ID list for posts
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
isStreaming.value = true;
|
|
||||||
streamingText.value = '';
|
|
||||||
functionCalls.value = [];
|
|
||||||
reasoningChunks.value = [];
|
|
||||||
|
|
||||||
final apiClient = ref.read(apiClientProvider);
|
|
||||||
final response = await apiClient.post(
|
|
||||||
'/insight/thought',
|
|
||||||
data: request.toJson(),
|
|
||||||
options: Options(
|
|
||||||
responseType: ResponseType.stream,
|
|
||||||
sendTimeout: Duration(minutes: 1),
|
|
||||||
receiveTimeout: Duration(minutes: 1),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final stream = response.data.stream;
|
|
||||||
final lineBuffer = StringBuffer();
|
|
||||||
|
|
||||||
stream.listen(
|
|
||||||
(data) {
|
|
||||||
final chunk = utf8.decode(data);
|
|
||||||
lineBuffer.write(chunk);
|
|
||||||
final lines = lineBuffer.toString().split('\n');
|
|
||||||
lineBuffer.clear();
|
|
||||||
lineBuffer.write(lines.last); // keep incomplete line
|
|
||||||
|
|
||||||
for (final line in lines.sublist(0, lines.length - 1)) {
|
|
||||||
if (line.trim().isEmpty) continue;
|
|
||||||
try {
|
|
||||||
if (line.startsWith('data: ')) {
|
|
||||||
final jsonStr = line.substring(6);
|
|
||||||
final event = jsonDecode(jsonStr);
|
|
||||||
final type = event['type'];
|
|
||||||
final eventData = event['data'];
|
|
||||||
if (type == 'text') {
|
|
||||||
streamingText.value += eventData;
|
|
||||||
} else if (type == 'function_call') {
|
|
||||||
functionCalls.value = [
|
|
||||||
...functionCalls.value,
|
|
||||||
JsonEncoder.withIndent(' ').convert(eventData),
|
|
||||||
];
|
|
||||||
} else if (type == 'reasoning') {
|
|
||||||
reasoningChunks.value = [
|
|
||||||
...reasoningChunks.value,
|
|
||||||
eventData,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
} else if (line.startsWith('topic: ')) {
|
|
||||||
final jsonStr = line.substring(7);
|
|
||||||
final event = jsonDecode(jsonStr);
|
|
||||||
currentTopic.value = event['data'];
|
|
||||||
} else if (line.startsWith('thought: ')) {
|
|
||||||
final jsonStr = line.substring(9);
|
|
||||||
final event = jsonDecode(jsonStr);
|
|
||||||
final aiThought = SnThinkingThought.fromJson(event['data']);
|
|
||||||
localThoughts.value = [aiThought, ...localThoughts.value];
|
|
||||||
if (selectedSequenceId.value == null &&
|
|
||||||
aiThought.sequenceId.isNotEmpty) {
|
|
||||||
selectedSequenceId.value = aiThought.sequenceId;
|
|
||||||
}
|
|
||||||
isStreaming.value = false;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// Ignore parsing errors for individual events
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDone: () {
|
|
||||||
if (isStreaming.value) {
|
|
||||||
isStreaming.value = false;
|
|
||||||
showErrorAlert('thoughtParseError'.tr());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onError: (error) {
|
|
||||||
isStreaming.value = false;
|
|
||||||
if (error is DioException && error.response?.data is ResponseBody) {
|
|
||||||
showErrorAlert('toughtParseError'.tr());
|
|
||||||
} else {
|
|
||||||
showErrorAlert(error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
messageController.clear();
|
|
||||||
FocusManager.instance.primaryFocus?.unfocus();
|
|
||||||
} catch (error) {
|
|
||||||
isStreaming.value = false;
|
|
||||||
showErrorAlert(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
isNoBackground: false,
|
isNoBackground: false,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(currentTopic.value ?? 'aiThought'.tr()),
|
title: Text(initialTopic ?? 'aiThought'.tr()),
|
||||||
|
leading: const PageBackButton(),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Symbols.history),
|
icon: const Icon(Symbols.history),
|
||||||
@@ -259,137 +96,96 @@ class ThoughtScreen extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (localThoughts.value.isNotEmpty &&
|
|
||||||
!isStreaming.value &&
|
|
||||||
localThoughts.value.last.role == ThinkingThoughtRole.assistant)
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Symbols.add),
|
|
||||||
tooltip: 'thoughtNewConversation'.tr(),
|
|
||||||
onPressed: () {
|
|
||||||
// Clear current conversation and start new one
|
|
||||||
selectedSequenceId.value = null;
|
|
||||||
localThoughts.value = [];
|
|
||||||
currentTopic.value = 'aiThought'.tr();
|
|
||||||
messageController.clear();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: Stack(
|
body: statusAsync.maybeWhen(
|
||||||
children: [
|
data: (status) {
|
||||||
// Thoughts list
|
final retry = useMemoized(
|
||||||
Center(
|
() => () async {
|
||||||
child: Container(
|
showLoadingModal(context);
|
||||||
constraints: BoxConstraints(maxWidth: 640),
|
try {
|
||||||
child: Column(
|
await ref
|
||||||
|
.read(apiClientProvider)
|
||||||
|
.post('/insight/billing/retry');
|
||||||
|
showSnackBar('Retried billing process');
|
||||||
|
ref.invalidate(thoughtAvailableStausProvider);
|
||||||
|
} catch (e) {
|
||||||
|
showSnackBar('Failed to retry billing');
|
||||||
|
}
|
||||||
|
if (context.mounted) hideLoadingModal(context);
|
||||||
|
},
|
||||||
|
[context, ref],
|
||||||
|
);
|
||||||
|
|
||||||
|
final thoughtsBody = thoughts.when(
|
||||||
|
data:
|
||||||
|
(thoughtList) => ThoughtChatInterface(
|
||||||
|
initialThoughts: thoughtList,
|
||||||
|
initialSequenceId: sequenceIdFromThoughts,
|
||||||
|
initialTopic: initialTopic,
|
||||||
|
isDisabled: !status,
|
||||||
|
),
|
||||||
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
|
error:
|
||||||
|
(error, _) => ResponseErrorWidget(
|
||||||
|
error: error,
|
||||||
|
onRetry:
|
||||||
|
() =>
|
||||||
|
selectedSequenceId.value != null
|
||||||
|
? ref.invalidate(
|
||||||
|
thoughtSequenceProvider(
|
||||||
|
selectedSequenceId.value!,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return status
|
||||||
|
? thoughtsBody
|
||||||
|
: Column(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
MaterialBanner(
|
||||||
child: thoughts.when(
|
leading: const Icon(Symbols.error),
|
||||||
data:
|
content: const Text(
|
||||||
(thoughtList) => SuperListView.builder(
|
'You have unpaid orders. Please settle your payment to continue using the service.',
|
||||||
listController: listController,
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
controller: scrollController,
|
|
||||||
padding: EdgeInsets.only(
|
|
||||||
top: 16,
|
|
||||||
bottom:
|
|
||||||
MediaQuery.of(context).padding.bottom +
|
|
||||||
80, // Leave space for thought input
|
|
||||||
),
|
|
||||||
reverse: true,
|
|
||||||
itemCount:
|
|
||||||
localThoughts.value.length +
|
|
||||||
(isStreaming.value ? 1 : 0),
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
if (isStreaming.value && index == 0) {
|
|
||||||
return ThoughtItem(
|
|
||||||
isStreaming: true,
|
|
||||||
streamingText: streamingText.value,
|
|
||||||
reasoningChunks: reasoningChunks.value,
|
|
||||||
streamingFunctionCalls: functionCalls.value,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
final thoughtIndex =
|
|
||||||
isStreaming.value ? index - 1 : index;
|
|
||||||
final thought = localThoughts.value[thoughtIndex];
|
|
||||||
return ThoughtItem(
|
|
||||||
thought: thought,
|
|
||||||
thoughtIndex: thoughtIndex,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
loading:
|
|
||||||
() =>
|
|
||||||
const Center(child: CircularProgressIndicator()),
|
|
||||||
error:
|
|
||||||
(error, _) => ResponseErrorWidget(
|
|
||||||
error: error,
|
|
||||||
onRetry:
|
|
||||||
() =>
|
|
||||||
selectedSequenceId.value != null
|
|
||||||
? ref.invalidate(
|
|
||||||
thoughtSequenceProvider(
|
|
||||||
selectedSequenceId.value!,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
retry();
|
||||||
|
},
|
||||||
|
child: Text('retry'.tr()),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
|
Expanded(child: thoughtsBody),
|
||||||
],
|
],
|
||||||
),
|
);
|
||||||
),
|
},
|
||||||
),
|
orElse:
|
||||||
// Bottom gradient - appears when scrolling towards newer thoughts (behind thought input)
|
() => thoughts.when(
|
||||||
AnimatedBuilder(
|
data:
|
||||||
animation: bottomGradientNotifier.value,
|
(thoughtList) => ThoughtChatInterface(
|
||||||
builder:
|
initialThoughts: thoughtList,
|
||||||
(context, child) => Positioned(
|
initialTopic: initialTopic,
|
||||||
left: 0,
|
),
|
||||||
right: 0,
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
bottom: 0,
|
error:
|
||||||
child: Opacity(
|
(error, _) => ResponseErrorWidget(
|
||||||
opacity: bottomGradientNotifier.value.value,
|
error: error,
|
||||||
child: Container(
|
onRetry:
|
||||||
height: math.min(
|
() =>
|
||||||
MediaQuery.of(context).size.height * 0.1,
|
selectedSequenceId.value != null
|
||||||
128,
|
? ref.invalidate(
|
||||||
),
|
thoughtSequenceProvider(
|
||||||
decoration: BoxDecoration(
|
selectedSequenceId.value!,
|
||||||
gradient: LinearGradient(
|
),
|
||||||
begin: Alignment.bottomCenter,
|
)
|
||||||
end: Alignment.topCenter,
|
: null,
|
||||||
colors: [
|
|
||||||
Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.surfaceContainer.withOpacity(0.8),
|
|
||||||
Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.surfaceContainer.withOpacity(0.0),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
// Thought Input positioned above gradient (higher z-index)
|
|
||||||
Positioned(
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: 0, // At the very bottom, above gradient
|
|
||||||
child: Center(
|
|
||||||
child: Container(
|
|
||||||
constraints: BoxConstraints(maxWidth: 640),
|
|
||||||
child: ThoughtInput(
|
|
||||||
messageController: messageController,
|
|
||||||
isStreaming: isStreaming.value,
|
|
||||||
onSend: sendMessage,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user