Compare commits
64 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
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
|
@@ -180,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.",
|
||||||
@@ -1336,5 +1337,139 @@
|
|||||||
"fundCreateNewHint": "Create a new fund for your message. Select recipients and amount.",
|
"fundCreateNewHint": "Create a new fund for your message. Select recipients and amount.",
|
||||||
"amountOfSplits": "Amount of Splits",
|
"amountOfSplits": "Amount of Splits",
|
||||||
"enterNumberOfSplits": "Enter Splits Amount",
|
"enterNumberOfSplits": "Enter Splits Amount",
|
||||||
"orCreateWith": "Or\ncreate with"
|
"orCreateWith": "Or\ncreate with",
|
||||||
|
"unindexedFiles": "Unindexed files",
|
||||||
|
"folder": "Folder",
|
||||||
|
"clearCompleted": "Clear Completed",
|
||||||
|
"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"
|
||||||
}
|
}
|
||||||
1
drift_schemas/app_database/drift_schema_v7.json
Normal file
1
drift_schemas/app_database/drift_schema_v7.json
Normal file
File diff suppressed because one or more lines are too long
@@ -57,7 +57,7 @@ PODS:
|
|||||||
- firebase_core (4.2.1):
|
- firebase_core (4.2.1):
|
||||||
- Firebase/CoreOnly (= 12.4.0)
|
- Firebase/CoreOnly (= 12.4.0)
|
||||||
- Flutter
|
- Flutter
|
||||||
- firebase_crashlytics (5.0.4):
|
- firebase_crashlytics (5.0.5):
|
||||||
- Firebase/Crashlytics (= 12.4.0)
|
- Firebase/Crashlytics (= 12.4.0)
|
||||||
- firebase_core
|
- firebase_core
|
||||||
- Flutter
|
- Flutter
|
||||||
@@ -140,15 +140,13 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- flutter_native_splash (2.4.3):
|
- flutter_native_splash (2.4.3):
|
||||||
- Flutter
|
- Flutter
|
||||||
- flutter_platform_alert (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- flutter_secure_storage (6.0.0):
|
- flutter_secure_storage (6.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- flutter_timezone (0.0.1):
|
- flutter_timezone (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- flutter_udid (0.0.1):
|
- flutter_udid (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- SAMKeychain
|
- KeychainAccess
|
||||||
- flutter_webrtc (1.2.0):
|
- flutter_webrtc (1.2.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- WebRTC-SDK (= 137.7151.04)
|
- WebRTC-SDK (= 137.7151.04)
|
||||||
@@ -216,7 +214,8 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- irondash_engine_context (0.0.1):
|
- irondash_engine_context (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- Kingfisher (8.6.1)
|
- KeychainAccess (4.2.2)
|
||||||
|
- Kingfisher (8.6.2)
|
||||||
- KingfisherWebP (1.7.2):
|
- KingfisherWebP (1.7.2):
|
||||||
- Kingfisher (~> 8.0)
|
- Kingfisher (~> 8.0)
|
||||||
- libwebp (>= 1.1.0)
|
- libwebp (>= 1.1.0)
|
||||||
@@ -250,14 +249,13 @@ PODS:
|
|||||||
- nanopb/encode (3.30910.0)
|
- nanopb/encode (3.30910.0)
|
||||||
- native_exif (0.0.1):
|
- native_exif (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- objective_c (0.0.1):
|
||||||
|
- Flutter
|
||||||
- OrderedSet (6.0.3)
|
- OrderedSet (6.0.3)
|
||||||
- package_info_plus (0.4.5):
|
- package_info_plus (0.4.5):
|
||||||
- Flutter
|
- Flutter
|
||||||
- pasteboard (0.0.1):
|
- pasteboard (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- path_provider_foundation (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- FlutterMacOS
|
|
||||||
- pointer_interceptor_ios (0.0.1):
|
- pointer_interceptor_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- PromisesObjC (2.4.0)
|
- PromisesObjC (2.4.0)
|
||||||
@@ -269,7 +267,6 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- record_ios (1.1.0):
|
- record_ios (1.1.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- SAMKeychain (1.5.3)
|
|
||||||
- SDWebImage (5.21.3):
|
- SDWebImage (5.21.3):
|
||||||
- SDWebImage/Core (= 5.21.3)
|
- SDWebImage/Core (= 5.21.3)
|
||||||
- SDWebImage/Core (5.21.3)
|
- SDWebImage/Core (5.21.3)
|
||||||
@@ -315,8 +312,6 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- url_launcher_ios (0.0.1):
|
- url_launcher_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- volume_controller (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- wakelock_plus (0.0.1):
|
- wakelock_plus (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- WebRTC-SDK (137.7151.04)
|
- WebRTC-SDK (137.7151.04)
|
||||||
@@ -338,7 +333,6 @@ DEPENDENCIES:
|
|||||||
- flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
|
- flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
|
||||||
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
||||||
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
||||||
- flutter_platform_alert (from `.symlinks/plugins/flutter_platform_alert/ios`)
|
|
||||||
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
||||||
- flutter_timezone (from `.symlinks/plugins/flutter_timezone/ios`)
|
- flutter_timezone (from `.symlinks/plugins/flutter_timezone/ios`)
|
||||||
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
|
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
|
||||||
@@ -353,9 +347,9 @@ DEPENDENCIES:
|
|||||||
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
|
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
|
||||||
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
|
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
|
||||||
- native_exif (from `.symlinks/plugins/native_exif/ios`)
|
- native_exif (from `.symlinks/plugins/native_exif/ios`)
|
||||||
|
- objective_c (from `.symlinks/plugins/objective_c/ios`)
|
||||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||||
- pasteboard (from `.symlinks/plugins/pasteboard/ios`)
|
- pasteboard (from `.symlinks/plugins/pasteboard/ios`)
|
||||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
|
||||||
- pointer_interceptor_ios (from `.symlinks/plugins/pointer_interceptor_ios/ios`)
|
- pointer_interceptor_ios (from `.symlinks/plugins/pointer_interceptor_ios/ios`)
|
||||||
- protocol_handler_ios (from `.symlinks/plugins/protocol_handler_ios/ios`)
|
- protocol_handler_ios (from `.symlinks/plugins/protocol_handler_ios/ios`)
|
||||||
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
|
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
|
||||||
@@ -368,7 +362,6 @@ DEPENDENCIES:
|
|||||||
- super_native_extensions (from `.symlinks/plugins/super_native_extensions/ios`)
|
- super_native_extensions (from `.symlinks/plugins/super_native_extensions/ios`)
|
||||||
- syncfusion_flutter_pdfviewer (from `.symlinks/plugins/syncfusion_flutter_pdfviewer/ios`)
|
- syncfusion_flutter_pdfviewer (from `.symlinks/plugins/syncfusion_flutter_pdfviewer/ios`)
|
||||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||||
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
|
|
||||||
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
||||||
|
|
||||||
SPEC REPOS:
|
SPEC REPOS:
|
||||||
@@ -390,6 +383,7 @@ SPEC REPOS:
|
|||||||
- GoogleAppMeasurement
|
- GoogleAppMeasurement
|
||||||
- GoogleDataTransport
|
- GoogleDataTransport
|
||||||
- GoogleUtilities
|
- GoogleUtilities
|
||||||
|
- KeychainAccess
|
||||||
- Kingfisher
|
- Kingfisher
|
||||||
- KingfisherWebP
|
- KingfisherWebP
|
||||||
- libwebp
|
- libwebp
|
||||||
@@ -397,7 +391,6 @@ SPEC REPOS:
|
|||||||
- OrderedSet
|
- OrderedSet
|
||||||
- PromisesObjC
|
- PromisesObjC
|
||||||
- PromisesSwift
|
- PromisesSwift
|
||||||
- SAMKeychain
|
|
||||||
- SDWebImage
|
- SDWebImage
|
||||||
- sqlite3
|
- sqlite3
|
||||||
- SwiftyGif
|
- SwiftyGif
|
||||||
@@ -434,8 +427,6 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/flutter_local_notifications/ios"
|
:path: ".symlinks/plugins/flutter_local_notifications/ios"
|
||||||
flutter_native_splash:
|
flutter_native_splash:
|
||||||
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
||||||
flutter_platform_alert:
|
|
||||||
:path: ".symlinks/plugins/flutter_platform_alert/ios"
|
|
||||||
flutter_secure_storage:
|
flutter_secure_storage:
|
||||||
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
||||||
flutter_timezone:
|
flutter_timezone:
|
||||||
@@ -460,12 +451,12 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/media_kit_video/ios"
|
:path: ".symlinks/plugins/media_kit_video/ios"
|
||||||
native_exif:
|
native_exif:
|
||||||
:path: ".symlinks/plugins/native_exif/ios"
|
:path: ".symlinks/plugins/native_exif/ios"
|
||||||
|
objective_c:
|
||||||
|
:path: ".symlinks/plugins/objective_c/ios"
|
||||||
package_info_plus:
|
package_info_plus:
|
||||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||||
pasteboard:
|
pasteboard:
|
||||||
:path: ".symlinks/plugins/pasteboard/ios"
|
:path: ".symlinks/plugins/pasteboard/ios"
|
||||||
path_provider_foundation:
|
|
||||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
|
||||||
pointer_interceptor_ios:
|
pointer_interceptor_ios:
|
||||||
:path: ".symlinks/plugins/pointer_interceptor_ios/ios"
|
:path: ".symlinks/plugins/pointer_interceptor_ios/ios"
|
||||||
protocol_handler_ios:
|
protocol_handler_ios:
|
||||||
@@ -490,8 +481,6 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/syncfusion_flutter_pdfviewer/ios"
|
:path: ".symlinks/plugins/syncfusion_flutter_pdfviewer/ios"
|
||||||
url_launcher_ios:
|
url_launcher_ios:
|
||||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||||
volume_controller:
|
|
||||||
:path: ".symlinks/plugins/volume_controller/ios"
|
|
||||||
wakelock_plus:
|
wakelock_plus:
|
||||||
:path: ".symlinks/plugins/wakelock_plus/ios"
|
:path: ".symlinks/plugins/wakelock_plus/ios"
|
||||||
|
|
||||||
@@ -507,7 +496,7 @@ SPEC CHECKSUMS:
|
|||||||
Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e
|
Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e
|
||||||
firebase_analytics: 67fbdd9f3c04e55048024f3da21cfc36f05e56cf
|
firebase_analytics: 67fbdd9f3c04e55048024f3da21cfc36f05e56cf
|
||||||
firebase_core: f1aafb21c14f497e5498f7ffc4dc63cbb52b2594
|
firebase_core: f1aafb21c14f497e5498f7ffc4dc63cbb52b2594
|
||||||
firebase_crashlytics: 83c7467d7534975a4d779af43bd226d0a4616464
|
firebase_crashlytics: c039028126cb45e32f4c217aa392408b0963d081
|
||||||
firebase_messaging: c17a29984eafce4b2997fe078bb0a9e0b06f5dde
|
firebase_messaging: c17a29984eafce4b2997fe078bb0a9e0b06f5dde
|
||||||
FirebaseAnalytics: 0fc2b20091f0ddd21bf73397cf8f0eb5346dc24f
|
FirebaseAnalytics: 0fc2b20091f0ddd21bf73397cf8f0eb5346dc24f
|
||||||
FirebaseCore: bb595f3114953664e3c1dc032f008a244147cfd3
|
FirebaseCore: bb595f3114953664e3c1dc032f008a244147cfd3
|
||||||
@@ -524,10 +513,9 @@ SPEC CHECKSUMS:
|
|||||||
flutter_keyboard_visibility: 4625131e43015dbbe759d9b20daaf77e0e3f6619
|
flutter_keyboard_visibility: 4625131e43015dbbe759d9b20daaf77e0e3f6619
|
||||||
flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb
|
flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb
|
||||||
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
|
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
|
||||||
flutter_platform_alert: bf3b5fcd4ac14bd637e20527e9c471633071afd3
|
|
||||||
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
|
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
|
||||||
flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544
|
flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544
|
||||||
flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9
|
flutter_udid: 92a5d31fe0526b7b6002a2318df702e12e7eb300
|
||||||
flutter_webrtc: c3e21fc0dcd9d8eb246ae4d5256fcbeb2f5ecd22
|
flutter_webrtc: c3e21fc0dcd9d8eb246ae4d5256fcbeb2f5ecd22
|
||||||
gal: baecd024ebfd13c441269ca7404792a7152fde89
|
gal: baecd024ebfd13c441269ca7404792a7152fde89
|
||||||
GoogleAdsOnDeviceConversion: e03a386840803ea7eef3fd22a061930142c039c1
|
GoogleAdsOnDeviceConversion: e03a386840803ea7eef3fd22a061930142c039c1
|
||||||
@@ -536,7 +524,8 @@ SPEC CHECKSUMS:
|
|||||||
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
||||||
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
|
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
|
||||||
irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
|
irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
|
||||||
Kingfisher: 7ac7a7288653787a54206b11a3c74f49ab650f1f
|
KeychainAccess: c0c4f7f38f6fc7bbe58f5702e25f7bd2f65abf51
|
||||||
|
Kingfisher: 23d18f54677d973b713e54ce6a8f5eef6e7056ba
|
||||||
KingfisherWebP: 38b9721821947f547afb78f933f75f4f9e0ae402
|
KingfisherWebP: 38b9721821947f547afb78f933f75f4f9e0ae402
|
||||||
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
|
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
|
||||||
livekit_client: 86c8af579274e4b7a215185a8080db2d4e176f40
|
livekit_client: 86c8af579274e4b7a215185a8080db2d4e176f40
|
||||||
@@ -545,17 +534,16 @@ SPEC CHECKSUMS:
|
|||||||
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
|
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
|
||||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||||
native_exif: 0eb73d3d5b3ca892719228df8d2d1b13d1ae396c
|
native_exif: 0eb73d3d5b3ca892719228df8d2d1b13d1ae396c
|
||||||
|
objective_c: 89e720c30d716b036faf9c9684022048eee1eee2
|
||||||
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
||||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||||
pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c
|
pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c
|
||||||
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
|
|
||||||
pointer_interceptor_ios: da06a662d5bfd329602b45b2ab41bc0fb5fdb0f0
|
pointer_interceptor_ios: da06a662d5bfd329602b45b2ab41bc0fb5fdb0f0
|
||||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||||
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
|
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
|
||||||
protocol_handler_ios: 59f23ee71f3ec602d67902ca7f669a80957888d5
|
protocol_handler_ios: 59f23ee71f3ec602d67902ca7f669a80957888d5
|
||||||
receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
|
receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
|
||||||
record_ios: f75fa1d57f840012775c0e93a38a7f3ceea1a374
|
record_ios: f75fa1d57f840012775c0e93a38a7f3ceea1a374
|
||||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
|
||||||
SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a
|
SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a
|
||||||
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||||
@@ -567,7 +555,6 @@ SPEC CHECKSUMS:
|
|||||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||||
syncfusion_flutter_pdfviewer: 90dc48305d2e33d4aa20681d1e98ddeda891bc14
|
syncfusion_flutter_pdfviewer: 90dc48305d2e33d4aa20681d1e98ddeda891bc14
|
||||||
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
||||||
volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12
|
|
||||||
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
||||||
WebRTC-SDK: 40d4f5ba05cadff14e4db5614aec402a633f007e
|
WebRTC-SDK: 40d4f5ba05cadff14e4db5614aec402a633f007e
|
||||||
|
|
||||||
|
|||||||
@@ -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,10 @@ 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
616
lib/database/drift_db.steps.dart
Normal file
616
lib/database/drift_db.steps.dart
Normal file
@@ -0,0 +1,616 @@
|
|||||||
|
// dart format width=80
|
||||||
|
import 'package:drift/internal/versioned_schema.dart' as i0;
|
||||||
|
import 'package:drift/drift.dart' as i1;
|
||||||
|
import 'package:drift/drift.dart'; // ignore_for_file: type=lint,unused_import
|
||||||
|
|
||||||
|
// GENERATED BY drift_dev, DO NOT MODIFY.
|
||||||
|
final class Schema7 extends i0.VersionedSchema {
|
||||||
|
Schema7({required super.database}) : super(version: 7);
|
||||||
|
@override
|
||||||
|
late final List<i1.DatabaseSchemaEntity> entities = [
|
||||||
|
chatRooms,
|
||||||
|
chatMembers,
|
||||||
|
chatMessages,
|
||||||
|
postDrafts,
|
||||||
|
];
|
||||||
|
late final Shape0 chatRooms = Shape0(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'chat_rooms',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_1,
|
||||||
|
_column_2,
|
||||||
|
_column_3,
|
||||||
|
_column_4,
|
||||||
|
_column_5,
|
||||||
|
_column_6,
|
||||||
|
_column_7,
|
||||||
|
_column_8,
|
||||||
|
_column_9,
|
||||||
|
_column_10,
|
||||||
|
_column_11,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape1 chatMembers = Shape1(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'chat_members',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_12,
|
||||||
|
_column_13,
|
||||||
|
_column_14,
|
||||||
|
_column_15,
|
||||||
|
_column_16,
|
||||||
|
_column_17,
|
||||||
|
_column_18,
|
||||||
|
_column_19,
|
||||||
|
_column_20,
|
||||||
|
_column_21,
|
||||||
|
_column_22,
|
||||||
|
_column_23,
|
||||||
|
_column_9,
|
||||||
|
_column_10,
|
||||||
|
_column_11,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape2 chatMessages = Shape2(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'chat_messages',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_24,
|
||||||
|
_column_25,
|
||||||
|
_column_26,
|
||||||
|
_column_27,
|
||||||
|
_column_28,
|
||||||
|
_column_9,
|
||||||
|
_column_29,
|
||||||
|
_column_30,
|
||||||
|
_column_31,
|
||||||
|
_column_11,
|
||||||
|
_column_32,
|
||||||
|
_column_33,
|
||||||
|
_column_34,
|
||||||
|
_column_35,
|
||||||
|
_column_36,
|
||||||
|
_column_37,
|
||||||
|
_column_38,
|
||||||
|
_column_39,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape3 postDrafts = Shape3(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'post_drafts',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_40,
|
||||||
|
_column_2,
|
||||||
|
_column_26,
|
||||||
|
_column_41,
|
||||||
|
_column_42,
|
||||||
|
_column_43,
|
||||||
|
_column_44,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Shape0 extends i0.VersionedTable {
|
||||||
|
Shape0({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<String> get id =>
|
||||||
|
columnsByName['id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get name =>
|
||||||
|
columnsByName['name']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get description =>
|
||||||
|
columnsByName['description']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<int> get type =>
|
||||||
|
columnsByName['type']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<bool> get isPublic =>
|
||||||
|
columnsByName['is_public']! as i1.GeneratedColumn<bool>;
|
||||||
|
i1.GeneratedColumn<bool> get isCommunity =>
|
||||||
|
columnsByName['is_community']! as i1.GeneratedColumn<bool>;
|
||||||
|
i1.GeneratedColumn<String> get picture =>
|
||||||
|
columnsByName['picture']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get background =>
|
||||||
|
columnsByName['background']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get realmId =>
|
||||||
|
columnsByName['realm_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<DateTime> get createdAt =>
|
||||||
|
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<DateTime> get updatedAt =>
|
||||||
|
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<DateTime> get deletedAt =>
|
||||||
|
columnsByName['deleted_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<String> _column_0(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'id',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_1(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'name',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_2(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'description',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<int> _column_3(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>(
|
||||||
|
'type',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.int,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<bool> _column_4(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<bool>(
|
||||||
|
'is_public',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.bool,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'CHECK ("is_public" IN (0, 1))',
|
||||||
|
),
|
||||||
|
defaultValue: const CustomExpression('0'),
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<bool> _column_5(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<bool>(
|
||||||
|
'is_community',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.bool,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'CHECK ("is_community" IN (0, 1))',
|
||||||
|
),
|
||||||
|
defaultValue: const CustomExpression('0'),
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_6(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'picture',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_7(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'background',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_8(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'realm_id',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<DateTime> _column_9(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<DateTime>(
|
||||||
|
'created_at',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.dateTime,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<DateTime> _column_10(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<DateTime>(
|
||||||
|
'updated_at',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.dateTime,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<DateTime> _column_11(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<DateTime>(
|
||||||
|
'deleted_at',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.dateTime,
|
||||||
|
);
|
||||||
|
|
||||||
|
class Shape1 extends i0.VersionedTable {
|
||||||
|
Shape1({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<String> get id =>
|
||||||
|
columnsByName['id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get chatRoomId =>
|
||||||
|
columnsByName['chat_room_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get accountId =>
|
||||||
|
columnsByName['account_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get account =>
|
||||||
|
columnsByName['account']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get nick =>
|
||||||
|
columnsByName['nick']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<int> get role =>
|
||||||
|
columnsByName['role']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<int> get notify =>
|
||||||
|
columnsByName['notify']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<DateTime> get joinedAt =>
|
||||||
|
columnsByName['joined_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<DateTime> get breakUntil =>
|
||||||
|
columnsByName['break_until']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<DateTime> get timeoutUntil =>
|
||||||
|
columnsByName['timeout_until']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<bool> get isBot =>
|
||||||
|
columnsByName['is_bot']! as i1.GeneratedColumn<bool>;
|
||||||
|
i1.GeneratedColumn<String> get status =>
|
||||||
|
columnsByName['status']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<DateTime> get lastTyped =>
|
||||||
|
columnsByName['last_typed']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<DateTime> get createdAt =>
|
||||||
|
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<DateTime> get updatedAt =>
|
||||||
|
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<DateTime> get deletedAt =>
|
||||||
|
columnsByName['deleted_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<String> _column_12(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'chat_room_id',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'REFERENCES chat_rooms (id)',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_13(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'account_id',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_14(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'account',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_15(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'nick',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<int> _column_16(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>(
|
||||||
|
'role',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.int,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<int> _column_17(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>(
|
||||||
|
'notify',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.int,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<DateTime> _column_18(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<DateTime>(
|
||||||
|
'joined_at',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.dateTime,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<DateTime> _column_19(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<DateTime>(
|
||||||
|
'break_until',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.dateTime,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<DateTime> _column_20(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<DateTime>(
|
||||||
|
'timeout_until',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.dateTime,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<bool> _column_21(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<bool>(
|
||||||
|
'is_bot',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.bool,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'CHECK ("is_bot" IN (0, 1))',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_22(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'status',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<DateTime> _column_23(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<DateTime>(
|
||||||
|
'last_typed',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.dateTime,
|
||||||
|
);
|
||||||
|
|
||||||
|
class Shape2 extends i0.VersionedTable {
|
||||||
|
Shape2({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<String> get id =>
|
||||||
|
columnsByName['id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get roomId =>
|
||||||
|
columnsByName['room_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get senderId =>
|
||||||
|
columnsByName['sender_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get content =>
|
||||||
|
columnsByName['content']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get nonce =>
|
||||||
|
columnsByName['nonce']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get data =>
|
||||||
|
columnsByName['data']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<DateTime> get createdAt =>
|
||||||
|
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<int> get status =>
|
||||||
|
columnsByName['status']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<bool> get isDeleted =>
|
||||||
|
columnsByName['is_deleted']! as i1.GeneratedColumn<bool>;
|
||||||
|
i1.GeneratedColumn<DateTime> get updatedAt =>
|
||||||
|
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<DateTime> get deletedAt =>
|
||||||
|
columnsByName['deleted_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<String> get type =>
|
||||||
|
columnsByName['type']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get meta =>
|
||||||
|
columnsByName['meta']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get membersMentioned =>
|
||||||
|
columnsByName['members_mentioned']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<DateTime> get editedAt =>
|
||||||
|
columnsByName['edited_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<String> get attachments =>
|
||||||
|
columnsByName['attachments']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get reactions =>
|
||||||
|
columnsByName['reactions']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get repliedMessageId =>
|
||||||
|
columnsByName['replied_message_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get forwardedMessageId =>
|
||||||
|
columnsByName['forwarded_message_id']! as i1.GeneratedColumn<String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<String> _column_24(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'room_id',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'REFERENCES chat_rooms (id)',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_25(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'sender_id',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'REFERENCES chat_members (id)',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_26(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'content',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_27(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'nonce',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_28(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'data',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<int> _column_29(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>(
|
||||||
|
'status',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.int,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<bool> _column_30(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<bool>(
|
||||||
|
'is_deleted',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.bool,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'CHECK ("is_deleted" IN (0, 1))',
|
||||||
|
),
|
||||||
|
defaultValue: const CustomExpression('0'),
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<DateTime> _column_31(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<DateTime>(
|
||||||
|
'updated_at',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.dateTime,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_32(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'type',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
defaultValue: const CustomExpression('\'text\''),
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_33(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'meta',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
defaultValue: const CustomExpression('\'{}\''),
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_34(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'members_mentioned',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
defaultValue: const CustomExpression('\'[]\''),
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<DateTime> _column_35(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<DateTime>(
|
||||||
|
'edited_at',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.dateTime,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_36(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'attachments',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
defaultValue: const CustomExpression('\'[]\''),
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_37(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'reactions',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
defaultValue: const CustomExpression('\'[]\''),
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_38(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'replied_message_id',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_39(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'forwarded_message_id',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
);
|
||||||
|
|
||||||
|
class Shape3 extends i0.VersionedTable {
|
||||||
|
Shape3({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<String> get id =>
|
||||||
|
columnsByName['id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get title =>
|
||||||
|
columnsByName['title']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get description =>
|
||||||
|
columnsByName['description']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get content =>
|
||||||
|
columnsByName['content']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<int> get visibility =>
|
||||||
|
columnsByName['visibility']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<int> get type =>
|
||||||
|
columnsByName['type']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<DateTime> get lastModified =>
|
||||||
|
columnsByName['last_modified']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<String> get postData =>
|
||||||
|
columnsByName['post_data']! as i1.GeneratedColumn<String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<String> _column_40(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'title',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<int> _column_41(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>(
|
||||||
|
'visibility',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.int,
|
||||||
|
defaultValue: const CustomExpression('0'),
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<int> _column_42(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>(
|
||||||
|
'type',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.int,
|
||||||
|
defaultValue: const CustomExpression('0'),
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<DateTime> _column_43(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<DateTime>(
|
||||||
|
'last_modified',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.dateTime,
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_44(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'post_data',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
);
|
||||||
|
i0.MigrationStepWithVersion migrationSteps({
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7,
|
||||||
|
}) {
|
||||||
|
return (currentVersion, database) async {
|
||||||
|
switch (currentVersion) {
|
||||||
|
case 6:
|
||||||
|
final schema = Schema7(database: database);
|
||||||
|
final migrator = i1.Migrator(database, schema);
|
||||||
|
await from6To7(migrator, schema);
|
||||||
|
return 7;
|
||||||
|
default:
|
||||||
|
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.OnUpgrade stepByStep({
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7,
|
||||||
|
}) => i0.VersionedSchema.stepByStepHelper(
|
||||||
|
step: migrationSteps(from6To7: from6To7),
|
||||||
|
);
|
||||||
@@ -36,10 +36,52 @@ class ListMapConverter
|
|||||||
String toSql(List<Map<String, dynamic>> value) => json.encode(value);
|
String toSql(List<Map<String, dynamic>> value) => json.encode(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ChatRooms extends Table {
|
||||||
|
TextColumn get id => text()();
|
||||||
|
TextColumn get name => text().nullable()();
|
||||||
|
TextColumn get description => text().nullable()();
|
||||||
|
IntColumn get type => integer()();
|
||||||
|
BoolColumn get isPublic =>
|
||||||
|
boolean().nullable().withDefault(const Constant(false))();
|
||||||
|
BoolColumn get isCommunity =>
|
||||||
|
boolean().nullable().withDefault(const Constant(false))();
|
||||||
|
TextColumn get picture => text().map(const MapConverter()).nullable()();
|
||||||
|
TextColumn get background => text().map(const MapConverter()).nullable()();
|
||||||
|
TextColumn get realmId => text().nullable()();
|
||||||
|
DateTimeColumn get createdAt => dateTime()();
|
||||||
|
DateTimeColumn get updatedAt => dateTime()();
|
||||||
|
DateTimeColumn get deletedAt => dateTime().nullable()();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<Column> get primaryKey => {id};
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChatMembers extends Table {
|
||||||
|
TextColumn get id => text()();
|
||||||
|
TextColumn get chatRoomId => text().references(ChatRooms, #id)();
|
||||||
|
TextColumn get accountId => text()();
|
||||||
|
TextColumn get account => text().map(const MapConverter())();
|
||||||
|
TextColumn get nick => text().nullable()();
|
||||||
|
IntColumn get role => integer()();
|
||||||
|
IntColumn get notify => integer()();
|
||||||
|
DateTimeColumn get joinedAt => dateTime().nullable()();
|
||||||
|
DateTimeColumn get breakUntil => dateTime().nullable()();
|
||||||
|
DateTimeColumn get timeoutUntil => dateTime().nullable()();
|
||||||
|
BoolColumn get isBot => boolean()();
|
||||||
|
TextColumn get status => text().nullable()();
|
||||||
|
DateTimeColumn get lastTyped => dateTime().nullable()();
|
||||||
|
DateTimeColumn get createdAt => dateTime()();
|
||||||
|
DateTimeColumn get updatedAt => dateTime()();
|
||||||
|
DateTimeColumn get deletedAt => dateTime().nullable()();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<Column> get primaryKey => {id};
|
||||||
|
}
|
||||||
|
|
||||||
class ChatMessages extends Table {
|
class ChatMessages extends Table {
|
||||||
TextColumn get id => text()();
|
TextColumn get id => text()();
|
||||||
TextColumn get roomId => text()();
|
TextColumn get roomId => text().references(ChatRooms, #id)();
|
||||||
TextColumn get senderId => text()();
|
TextColumn get senderId => text().references(ChatMembers, #id)();
|
||||||
TextColumn get content => text().nullable()();
|
TextColumn get content => text().nullable()();
|
||||||
TextColumn get nonce => text().nullable()();
|
TextColumn get nonce => text().nullable()();
|
||||||
TextColumn get data => text()();
|
TextColumn get data => text()();
|
||||||
@@ -72,6 +114,7 @@ class LocalChatMessage {
|
|||||||
final String id;
|
final String id;
|
||||||
final String roomId;
|
final String roomId;
|
||||||
final String senderId;
|
final String senderId;
|
||||||
|
final SnChatMember? sender;
|
||||||
final Map<String, dynamic> data;
|
final Map<String, dynamic> data;
|
||||||
final DateTime createdAt;
|
final DateTime createdAt;
|
||||||
MessageStatus status;
|
MessageStatus status;
|
||||||
@@ -94,6 +137,7 @@ class LocalChatMessage {
|
|||||||
required this.id,
|
required this.id,
|
||||||
required this.roomId,
|
required this.roomId,
|
||||||
required this.senderId,
|
required this.senderId,
|
||||||
|
required this.sender,
|
||||||
required this.data,
|
required this.data,
|
||||||
required this.createdAt,
|
required this.createdAt,
|
||||||
required this.nonce,
|
required this.nonce,
|
||||||
@@ -114,7 +158,12 @@ class LocalChatMessage {
|
|||||||
});
|
});
|
||||||
|
|
||||||
SnChatMessage toRemoteMessage() {
|
SnChatMessage toRemoteMessage() {
|
||||||
return SnChatMessage.fromJson(data);
|
if (sender == null) {
|
||||||
|
throw Exception('Cannot create remote message without sender');
|
||||||
|
}
|
||||||
|
final msgData = Map<String, dynamic>.from(data);
|
||||||
|
msgData['sender'] = sender!.toJson();
|
||||||
|
return SnChatMessage.fromJson(msgData);
|
||||||
}
|
}
|
||||||
|
|
||||||
static LocalChatMessage fromRemoteMessage(
|
static LocalChatMessage fromRemoteMessage(
|
||||||
@@ -122,11 +171,26 @@ class LocalChatMessage {
|
|||||||
MessageStatus status, {
|
MessageStatus status, {
|
||||||
String? nonce,
|
String? nonce,
|
||||||
}) {
|
}) {
|
||||||
|
final jsonData = message.toJson();
|
||||||
|
jsonData.remove('sender');
|
||||||
|
// Ensure proper defaults for collections to prevent type cast errors
|
||||||
|
if (jsonData['meta'] == null) jsonData['meta'] = <String, dynamic>{};
|
||||||
|
if (jsonData['members_mentioned'] == null) {
|
||||||
|
jsonData['members_mentioned'] = <String>[];
|
||||||
|
}
|
||||||
|
if (jsonData['attachments'] == null) {
|
||||||
|
jsonData['attachments'] = <Map<String, dynamic>>[];
|
||||||
|
}
|
||||||
|
if (jsonData['reactions'] == null) {
|
||||||
|
jsonData['reactions'] = <Map<String, dynamic>>[];
|
||||||
|
}
|
||||||
|
final msgData = Map<String, dynamic>.from(jsonData);
|
||||||
return LocalChatMessage(
|
return LocalChatMessage(
|
||||||
id: message.id,
|
id: message.id,
|
||||||
roomId: message.chatRoomId,
|
roomId: message.chatRoomId,
|
||||||
senderId: message.senderId,
|
senderId: message.senderId,
|
||||||
data: message.toJson(),
|
sender: message.sender,
|
||||||
|
data: msgData,
|
||||||
createdAt: message.createdAt,
|
createdAt: message.createdAt,
|
||||||
status: status,
|
status: status,
|
||||||
nonce: nonce ?? message.nonce,
|
nonce: nonce ?? message.nonce,
|
||||||
|
|||||||
41
lib/models/publication_site.dart
Normal file
41
lib/models/publication_site.dart
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'publication_site.freezed.dart';
|
||||||
|
part 'publication_site.g.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
sealed class SnPublicationSite with _$SnPublicationSite {
|
||||||
|
const factory SnPublicationSite({
|
||||||
|
required String id,
|
||||||
|
required String slug,
|
||||||
|
required String name,
|
||||||
|
String? description,
|
||||||
|
int? mode,
|
||||||
|
required String publisherId,
|
||||||
|
required String accountId,
|
||||||
|
required DateTime createdAt,
|
||||||
|
required DateTime updatedAt,
|
||||||
|
required List<SnPublicationPage> pages,
|
||||||
|
}) = _SnPublicationSite;
|
||||||
|
|
||||||
|
factory SnPublicationSite.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SnPublicationSiteFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
sealed class SnPublicationPage with _$SnPublicationPage {
|
||||||
|
const factory SnPublicationPage({
|
||||||
|
required String id,
|
||||||
|
String? preset,
|
||||||
|
String? path,
|
||||||
|
Map<String, dynamic>? config,
|
||||||
|
required String siteId,
|
||||||
|
required DateTime createdAt,
|
||||||
|
required DateTime updatedAt,
|
||||||
|
}) = _SnPublicationPage;
|
||||||
|
|
||||||
|
factory SnPublicationPage.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SnPublicationPageFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PublicationPagePreset { landing, profile, posts, custom }
|
||||||
587
lib/models/publication_site.freezed.dart
Normal file
587
lib/models/publication_site.freezed.dart
Normal file
@@ -0,0 +1,587 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// coverage:ignore-file
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||||
|
|
||||||
|
part of 'publication_site.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// FreezedGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// dart format off
|
||||||
|
T _$identity<T>(T value) => value;
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$SnPublicationSite {
|
||||||
|
|
||||||
|
String get id; String get slug; String get name; String? get description; int? get mode; String get publisherId; String get accountId; DateTime get createdAt; DateTime get updatedAt; List<SnPublicationPage> get pages;
|
||||||
|
/// Create a copy of SnPublicationSite
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnPublicationSiteCopyWith<SnPublicationSite> get copyWith => _$SnPublicationSiteCopyWithImpl<SnPublicationSite>(this as SnPublicationSite, _$identity);
|
||||||
|
|
||||||
|
/// Serializes this SnPublicationSite to a JSON map.
|
||||||
|
Map<String, dynamic> toJson();
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPublicationSite&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.mode, mode) || other.mode == mode)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&const DeepCollectionEquality().equals(other.pages, pages));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,id,slug,name,description,mode,publisherId,accountId,createdAt,updatedAt,const DeepCollectionEquality().hash(pages));
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SnPublicationSite(id: $id, slug: $slug, name: $name, description: $description, mode: $mode, publisherId: $publisherId, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, pages: $pages)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class $SnPublicationSiteCopyWith<$Res> {
|
||||||
|
factory $SnPublicationSiteCopyWith(SnPublicationSite value, $Res Function(SnPublicationSite) _then) = _$SnPublicationSiteCopyWithImpl;
|
||||||
|
@useResult
|
||||||
|
$Res call({
|
||||||
|
String id, String slug, String name, String? description, int? mode, String publisherId, String accountId, DateTime createdAt, DateTime updatedAt, List<SnPublicationPage> pages
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class _$SnPublicationSiteCopyWithImpl<$Res>
|
||||||
|
implements $SnPublicationSiteCopyWith<$Res> {
|
||||||
|
_$SnPublicationSiteCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final SnPublicationSite _self;
|
||||||
|
final $Res Function(SnPublicationSite) _then;
|
||||||
|
|
||||||
|
/// Create a copy of SnPublicationSite
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? slug = null,Object? name = null,Object? description = freezed,Object? mode = freezed,Object? publisherId = null,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? pages = null,}) {
|
||||||
|
return _then(_self.copyWith(
|
||||||
|
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,slug: null == slug ? _self.slug : slug // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,mode: freezed == mode ? _self.mode : mode // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int?,publisherId: null == publisherId ? _self.publisherId : publisherId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,pages: null == pages ? _self.pages : pages // ignore: cast_nullable_to_non_nullable
|
||||||
|
as List<SnPublicationPage>,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Adds pattern-matching-related methods to [SnPublicationSite].
|
||||||
|
extension SnPublicationSitePatterns on SnPublicationSite {
|
||||||
|
/// A variant of `map` that fallback to returning `orElse`.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return orElse();
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnPublicationSite value)? $default,{required TResult orElse(),}){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnPublicationSite() when $default != null:
|
||||||
|
return $default(_that);case _:
|
||||||
|
return orElse();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A `switch`-like method, using callbacks.
|
||||||
|
///
|
||||||
|
/// Callbacks receives the raw object, upcasted.
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case final Subclass2 value:
|
||||||
|
/// return ...;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnPublicationSite value) $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnPublicationSite():
|
||||||
|
return $default(_that);}
|
||||||
|
}
|
||||||
|
/// A variant of `map` that fallback to returning `null`.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return null;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnPublicationSite value)? $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnPublicationSite() when $default != null:
|
||||||
|
return $default(_that);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A variant of `when` that fallback to an `orElse` callback.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return orElse();
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String slug, String name, String? description, int? mode, String publisherId, String accountId, DateTime createdAt, DateTime updatedAt, List<SnPublicationPage> pages)? $default,{required TResult orElse(),}) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnPublicationSite() when $default != null:
|
||||||
|
return $default(_that.id,_that.slug,_that.name,_that.description,_that.mode,_that.publisherId,_that.accountId,_that.createdAt,_that.updatedAt,_that.pages);case _:
|
||||||
|
return orElse();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A `switch`-like method, using callbacks.
|
||||||
|
///
|
||||||
|
/// As opposed to `map`, this offers destructuring.
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case Subclass2(:final field2):
|
||||||
|
/// return ...;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String slug, String name, String? description, int? mode, String publisherId, String accountId, DateTime createdAt, DateTime updatedAt, List<SnPublicationPage> pages) $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnPublicationSite():
|
||||||
|
return $default(_that.id,_that.slug,_that.name,_that.description,_that.mode,_that.publisherId,_that.accountId,_that.createdAt,_that.updatedAt,_that.pages);}
|
||||||
|
}
|
||||||
|
/// A variant of `when` that fallback to returning `null`
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return null;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String slug, String name, String? description, int? mode, String publisherId, String accountId, DateTime createdAt, DateTime updatedAt, List<SnPublicationPage> pages)? $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnPublicationSite() when $default != null:
|
||||||
|
return $default(_that.id,_that.slug,_that.name,_that.description,_that.mode,_that.publisherId,_that.accountId,_that.createdAt,_that.updatedAt,_that.pages);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
|
||||||
|
class _SnPublicationSite implements SnPublicationSite {
|
||||||
|
const _SnPublicationSite({required this.id, required this.slug, required this.name, this.description, this.mode, required this.publisherId, required this.accountId, required this.createdAt, required this.updatedAt, required final List<SnPublicationPage> pages}): _pages = pages;
|
||||||
|
factory _SnPublicationSite.fromJson(Map<String, dynamic> json) => _$SnPublicationSiteFromJson(json);
|
||||||
|
|
||||||
|
@override final String id;
|
||||||
|
@override final String slug;
|
||||||
|
@override final String name;
|
||||||
|
@override final String? description;
|
||||||
|
@override final int? mode;
|
||||||
|
@override final String publisherId;
|
||||||
|
@override final String accountId;
|
||||||
|
@override final DateTime createdAt;
|
||||||
|
@override final DateTime updatedAt;
|
||||||
|
final List<SnPublicationPage> _pages;
|
||||||
|
@override List<SnPublicationPage> get pages {
|
||||||
|
if (_pages is EqualUnmodifiableListView) return _pages;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableListView(_pages);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Create a copy of SnPublicationSite
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$SnPublicationSiteCopyWith<_SnPublicationSite> get copyWith => __$SnPublicationSiteCopyWithImpl<_SnPublicationSite>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$SnPublicationSiteToJson(this, );
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPublicationSite&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.mode, mode) || other.mode == mode)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&const DeepCollectionEquality().equals(other._pages, _pages));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,id,slug,name,description,mode,publisherId,accountId,createdAt,updatedAt,const DeepCollectionEquality().hash(_pages));
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SnPublicationSite(id: $id, slug: $slug, name: $name, description: $description, mode: $mode, publisherId: $publisherId, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, pages: $pages)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class _$SnPublicationSiteCopyWith<$Res> implements $SnPublicationSiteCopyWith<$Res> {
|
||||||
|
factory _$SnPublicationSiteCopyWith(_SnPublicationSite value, $Res Function(_SnPublicationSite) _then) = __$SnPublicationSiteCopyWithImpl;
|
||||||
|
@override @useResult
|
||||||
|
$Res call({
|
||||||
|
String id, String slug, String name, String? description, int? mode, String publisherId, String accountId, DateTime createdAt, DateTime updatedAt, List<SnPublicationPage> pages
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class __$SnPublicationSiteCopyWithImpl<$Res>
|
||||||
|
implements _$SnPublicationSiteCopyWith<$Res> {
|
||||||
|
__$SnPublicationSiteCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final _SnPublicationSite _self;
|
||||||
|
final $Res Function(_SnPublicationSite) _then;
|
||||||
|
|
||||||
|
/// Create a copy of SnPublicationSite
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? slug = null,Object? name = null,Object? description = freezed,Object? mode = freezed,Object? publisherId = null,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? pages = null,}) {
|
||||||
|
return _then(_SnPublicationSite(
|
||||||
|
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,slug: null == slug ? _self.slug : slug // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,mode: freezed == mode ? _self.mode : mode // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int?,publisherId: null == publisherId ? _self.publisherId : publisherId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,pages: null == pages ? _self._pages : pages // ignore: cast_nullable_to_non_nullable
|
||||||
|
as List<SnPublicationPage>,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$SnPublicationPage {
|
||||||
|
|
||||||
|
String get id; String? get preset; String? get path; Map<String, dynamic>? get config; String get siteId; DateTime get createdAt; DateTime get updatedAt;
|
||||||
|
/// Create a copy of SnPublicationPage
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnPublicationPageCopyWith<SnPublicationPage> get copyWith => _$SnPublicationPageCopyWithImpl<SnPublicationPage>(this as SnPublicationPage, _$identity);
|
||||||
|
|
||||||
|
/// Serializes this SnPublicationPage to a JSON map.
|
||||||
|
Map<String, dynamic> toJson();
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPublicationPage&&(identical(other.id, id) || other.id == id)&&(identical(other.preset, preset) || other.preset == preset)&&(identical(other.path, path) || other.path == path)&&const DeepCollectionEquality().equals(other.config, config)&&(identical(other.siteId, siteId) || other.siteId == siteId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,id,preset,path,const DeepCollectionEquality().hash(config),siteId,createdAt,updatedAt);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SnPublicationPage(id: $id, preset: $preset, path: $path, config: $config, siteId: $siteId, createdAt: $createdAt, updatedAt: $updatedAt)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class $SnPublicationPageCopyWith<$Res> {
|
||||||
|
factory $SnPublicationPageCopyWith(SnPublicationPage value, $Res Function(SnPublicationPage) _then) = _$SnPublicationPageCopyWithImpl;
|
||||||
|
@useResult
|
||||||
|
$Res call({
|
||||||
|
String id, String? preset, String? path, Map<String, dynamic>? config, String siteId, DateTime createdAt, DateTime updatedAt
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class _$SnPublicationPageCopyWithImpl<$Res>
|
||||||
|
implements $SnPublicationPageCopyWith<$Res> {
|
||||||
|
_$SnPublicationPageCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final SnPublicationPage _self;
|
||||||
|
final $Res Function(SnPublicationPage) _then;
|
||||||
|
|
||||||
|
/// Create a copy of SnPublicationPage
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? preset = freezed,Object? path = freezed,Object? config = freezed,Object? siteId = null,Object? createdAt = null,Object? updatedAt = null,}) {
|
||||||
|
return _then(_self.copyWith(
|
||||||
|
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,preset: freezed == preset ? _self.preset : preset // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,path: freezed == path ? _self.path : path // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,config: freezed == config ? _self.config : config // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<String, dynamic>?,siteId: null == siteId ? _self.siteId : siteId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Adds pattern-matching-related methods to [SnPublicationPage].
|
||||||
|
extension SnPublicationPagePatterns on SnPublicationPage {
|
||||||
|
/// A variant of `map` that fallback to returning `orElse`.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return orElse();
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnPublicationPage value)? $default,{required TResult orElse(),}){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnPublicationPage() when $default != null:
|
||||||
|
return $default(_that);case _:
|
||||||
|
return orElse();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A `switch`-like method, using callbacks.
|
||||||
|
///
|
||||||
|
/// Callbacks receives the raw object, upcasted.
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case final Subclass2 value:
|
||||||
|
/// return ...;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnPublicationPage value) $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnPublicationPage():
|
||||||
|
return $default(_that);}
|
||||||
|
}
|
||||||
|
/// A variant of `map` that fallback to returning `null`.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return null;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnPublicationPage value)? $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnPublicationPage() when $default != null:
|
||||||
|
return $default(_that);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A variant of `when` that fallback to an `orElse` callback.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return orElse();
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String? preset, String? path, Map<String, dynamic>? config, String siteId, DateTime createdAt, DateTime updatedAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnPublicationPage() when $default != null:
|
||||||
|
return $default(_that.id,_that.preset,_that.path,_that.config,_that.siteId,_that.createdAt,_that.updatedAt);case _:
|
||||||
|
return orElse();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A `switch`-like method, using callbacks.
|
||||||
|
///
|
||||||
|
/// As opposed to `map`, this offers destructuring.
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case Subclass2(:final field2):
|
||||||
|
/// return ...;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String? preset, String? path, Map<String, dynamic>? config, String siteId, DateTime createdAt, DateTime updatedAt) $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnPublicationPage():
|
||||||
|
return $default(_that.id,_that.preset,_that.path,_that.config,_that.siteId,_that.createdAt,_that.updatedAt);}
|
||||||
|
}
|
||||||
|
/// A variant of `when` that fallback to returning `null`
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return null;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String? preset, String? path, Map<String, dynamic>? config, String siteId, DateTime createdAt, DateTime updatedAt)? $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnPublicationPage() when $default != null:
|
||||||
|
return $default(_that.id,_that.preset,_that.path,_that.config,_that.siteId,_that.createdAt,_that.updatedAt);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
|
||||||
|
class _SnPublicationPage implements SnPublicationPage {
|
||||||
|
const _SnPublicationPage({required this.id, this.preset, this.path, final Map<String, dynamic>? config, required this.siteId, required this.createdAt, required this.updatedAt}): _config = config;
|
||||||
|
factory _SnPublicationPage.fromJson(Map<String, dynamic> json) => _$SnPublicationPageFromJson(json);
|
||||||
|
|
||||||
|
@override final String id;
|
||||||
|
@override final String? preset;
|
||||||
|
@override final String? path;
|
||||||
|
final Map<String, dynamic>? _config;
|
||||||
|
@override Map<String, dynamic>? get config {
|
||||||
|
final value = _config;
|
||||||
|
if (value == null) return null;
|
||||||
|
if (_config is EqualUnmodifiableMapView) return _config;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableMapView(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override final String siteId;
|
||||||
|
@override final DateTime createdAt;
|
||||||
|
@override final DateTime updatedAt;
|
||||||
|
|
||||||
|
/// Create a copy of SnPublicationPage
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$SnPublicationPageCopyWith<_SnPublicationPage> get copyWith => __$SnPublicationPageCopyWithImpl<_SnPublicationPage>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$SnPublicationPageToJson(this, );
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPublicationPage&&(identical(other.id, id) || other.id == id)&&(identical(other.preset, preset) || other.preset == preset)&&(identical(other.path, path) || other.path == path)&&const DeepCollectionEquality().equals(other._config, _config)&&(identical(other.siteId, siteId) || other.siteId == siteId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,id,preset,path,const DeepCollectionEquality().hash(_config),siteId,createdAt,updatedAt);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SnPublicationPage(id: $id, preset: $preset, path: $path, config: $config, siteId: $siteId, createdAt: $createdAt, updatedAt: $updatedAt)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class _$SnPublicationPageCopyWith<$Res> implements $SnPublicationPageCopyWith<$Res> {
|
||||||
|
factory _$SnPublicationPageCopyWith(_SnPublicationPage value, $Res Function(_SnPublicationPage) _then) = __$SnPublicationPageCopyWithImpl;
|
||||||
|
@override @useResult
|
||||||
|
$Res call({
|
||||||
|
String id, String? preset, String? path, Map<String, dynamic>? config, String siteId, DateTime createdAt, DateTime updatedAt
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class __$SnPublicationPageCopyWithImpl<$Res>
|
||||||
|
implements _$SnPublicationPageCopyWith<$Res> {
|
||||||
|
__$SnPublicationPageCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final _SnPublicationPage _self;
|
||||||
|
final $Res Function(_SnPublicationPage) _then;
|
||||||
|
|
||||||
|
/// Create a copy of SnPublicationPage
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? preset = freezed,Object? path = freezed,Object? config = freezed,Object? siteId = null,Object? createdAt = null,Object? updatedAt = null,}) {
|
||||||
|
return _then(_SnPublicationPage(
|
||||||
|
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,preset: freezed == preset ? _self.preset : preset // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,path: freezed == path ? _self.path : path // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,config: freezed == config ? _self._config : config // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<String, dynamic>?,siteId: null == siteId ? _self.siteId : siteId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// dart format on
|
||||||
60
lib/models/publication_site.g.dart
Normal file
60
lib/models/publication_site.g.dart
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'publication_site.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
_SnPublicationSite _$SnPublicationSiteFromJson(Map<String, dynamic> json) =>
|
||||||
|
_SnPublicationSite(
|
||||||
|
id: json['id'] as String,
|
||||||
|
slug: json['slug'] as String,
|
||||||
|
name: json['name'] as String,
|
||||||
|
description: json['description'] as String?,
|
||||||
|
mode: (json['mode'] as num?)?.toInt(),
|
||||||
|
publisherId: json['publisher_id'] as String,
|
||||||
|
accountId: json['account_id'] as String,
|
||||||
|
createdAt: DateTime.parse(json['created_at'] as String),
|
||||||
|
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||||
|
pages:
|
||||||
|
(json['pages'] as List<dynamic>)
|
||||||
|
.map((e) => SnPublicationPage.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$SnPublicationSiteToJson(_SnPublicationSite instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'slug': instance.slug,
|
||||||
|
'name': instance.name,
|
||||||
|
'description': instance.description,
|
||||||
|
'mode': instance.mode,
|
||||||
|
'publisher_id': instance.publisherId,
|
||||||
|
'account_id': instance.accountId,
|
||||||
|
'created_at': instance.createdAt.toIso8601String(),
|
||||||
|
'updated_at': instance.updatedAt.toIso8601String(),
|
||||||
|
'pages': instance.pages.map((e) => e.toJson()).toList(),
|
||||||
|
};
|
||||||
|
|
||||||
|
_SnPublicationPage _$SnPublicationPageFromJson(Map<String, dynamic> json) =>
|
||||||
|
_SnPublicationPage(
|
||||||
|
id: json['id'] as String,
|
||||||
|
preset: json['preset'] as String?,
|
||||||
|
path: json['path'] as String?,
|
||||||
|
config: json['config'] as Map<String, dynamic>?,
|
||||||
|
siteId: json['site_id'] as String,
|
||||||
|
createdAt: DateTime.parse(json['created_at'] as String),
|
||||||
|
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$SnPublicationPageToJson(_SnPublicationPage instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'preset': instance.preset,
|
||||||
|
'path': instance.path,
|
||||||
|
'config': instance.config,
|
||||||
|
'site_id': instance.siteId,
|
||||||
|
'created_at': instance.createdAt.toIso8601String(),
|
||||||
|
'updated_at': instance.updatedAt.toIso8601String(),
|
||||||
|
};
|
||||||
23
lib/models/reference.dart
Normal file
23
lib/models/reference.dart
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
import 'package:island/models/file.dart';
|
||||||
|
|
||||||
|
part 'reference.freezed.dart';
|
||||||
|
part 'reference.g.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
sealed class Reference with _$Reference {
|
||||||
|
const factory Reference({
|
||||||
|
required String id,
|
||||||
|
@JsonKey(name: 'file_id') required String fileId,
|
||||||
|
SnCloudFile? file,
|
||||||
|
required String usage,
|
||||||
|
@JsonKey(name: 'resource_id') required String resourceId,
|
||||||
|
@JsonKey(name: 'expired_at') DateTime? expiredAt,
|
||||||
|
@JsonKey(name: 'created_at') required DateTime createdAt,
|
||||||
|
@JsonKey(name: 'updated_at') required DateTime updatedAt,
|
||||||
|
@JsonKey(name: 'deleted_at') DateTime? deletedAt,
|
||||||
|
}) = _Reference;
|
||||||
|
|
||||||
|
factory Reference.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$ReferenceFromJson(json);
|
||||||
|
}
|
||||||
319
lib/models/reference.freezed.dart
Normal file
319
lib/models/reference.freezed.dart
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// coverage:ignore-file
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||||
|
|
||||||
|
part of 'reference.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// FreezedGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// dart format off
|
||||||
|
T _$identity<T>(T value) => value;
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$Reference {
|
||||||
|
|
||||||
|
String get id;@JsonKey(name: 'file_id') String get fileId; SnCloudFile? get file; String get usage;@JsonKey(name: 'resource_id') String get resourceId;@JsonKey(name: 'expired_at') DateTime? get expiredAt;@JsonKey(name: 'created_at') DateTime get createdAt;@JsonKey(name: 'updated_at') DateTime get updatedAt;@JsonKey(name: 'deleted_at') DateTime? get deletedAt;
|
||||||
|
/// Create a copy of Reference
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$ReferenceCopyWith<Reference> get copyWith => _$ReferenceCopyWithImpl<Reference>(this as Reference, _$identity);
|
||||||
|
|
||||||
|
/// Serializes this Reference to a JSON map.
|
||||||
|
Map<String, dynamic> toJson();
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is Reference&&(identical(other.id, id) || other.id == id)&&(identical(other.fileId, fileId) || other.fileId == fileId)&&(identical(other.file, file) || other.file == file)&&(identical(other.usage, usage) || other.usage == usage)&&(identical(other.resourceId, resourceId) || other.resourceId == resourceId)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,id,fileId,file,usage,resourceId,expiredAt,createdAt,updatedAt,deletedAt);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'Reference(id: $id, fileId: $fileId, file: $file, usage: $usage, resourceId: $resourceId, expiredAt: $expiredAt, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class $ReferenceCopyWith<$Res> {
|
||||||
|
factory $ReferenceCopyWith(Reference value, $Res Function(Reference) _then) = _$ReferenceCopyWithImpl;
|
||||||
|
@useResult
|
||||||
|
$Res call({
|
||||||
|
String id,@JsonKey(name: 'file_id') String fileId, SnCloudFile? file, String usage,@JsonKey(name: 'resource_id') String resourceId,@JsonKey(name: 'expired_at') DateTime? expiredAt,@JsonKey(name: 'created_at') DateTime createdAt,@JsonKey(name: 'updated_at') DateTime updatedAt,@JsonKey(name: 'deleted_at') DateTime? deletedAt
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
$SnCloudFileCopyWith<$Res>? get file;
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class _$ReferenceCopyWithImpl<$Res>
|
||||||
|
implements $ReferenceCopyWith<$Res> {
|
||||||
|
_$ReferenceCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final Reference _self;
|
||||||
|
final $Res Function(Reference) _then;
|
||||||
|
|
||||||
|
/// Create a copy of Reference
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? fileId = null,Object? file = freezed,Object? usage = null,Object? resourceId = null,Object? expiredAt = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||||
|
return _then(_self.copyWith(
|
||||||
|
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,fileId: null == fileId ? _self.fileId : fileId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,file: freezed == file ? _self.file : file // ignore: cast_nullable_to_non_nullable
|
||||||
|
as SnCloudFile?,usage: null == usage ? _self.usage : usage // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,resourceId: null == resourceId ? _self.resourceId : resourceId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
/// Create a copy of Reference
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnCloudFileCopyWith<$Res>? get file {
|
||||||
|
if (_self.file == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $SnCloudFileCopyWith<$Res>(_self.file!, (value) {
|
||||||
|
return _then(_self.copyWith(file: value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Adds pattern-matching-related methods to [Reference].
|
||||||
|
extension ReferencePatterns on Reference {
|
||||||
|
/// A variant of `map` that fallback to returning `orElse`.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return orElse();
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _Reference value)? $default,{required TResult orElse(),}){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _Reference() when $default != null:
|
||||||
|
return $default(_that);case _:
|
||||||
|
return orElse();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A `switch`-like method, using callbacks.
|
||||||
|
///
|
||||||
|
/// Callbacks receives the raw object, upcasted.
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case final Subclass2 value:
|
||||||
|
/// return ...;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _Reference value) $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _Reference():
|
||||||
|
return $default(_that);}
|
||||||
|
}
|
||||||
|
/// A variant of `map` that fallback to returning `null`.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return null;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _Reference value)? $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _Reference() when $default != null:
|
||||||
|
return $default(_that);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A variant of `when` that fallback to an `orElse` callback.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return orElse();
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, @JsonKey(name: 'file_id') String fileId, SnCloudFile? file, String usage, @JsonKey(name: 'resource_id') String resourceId, @JsonKey(name: 'expired_at') DateTime? expiredAt, @JsonKey(name: 'created_at') DateTime createdAt, @JsonKey(name: 'updated_at') DateTime updatedAt, @JsonKey(name: 'deleted_at') DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _Reference() when $default != null:
|
||||||
|
return $default(_that.id,_that.fileId,_that.file,_that.usage,_that.resourceId,_that.expiredAt,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||||
|
return orElse();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A `switch`-like method, using callbacks.
|
||||||
|
///
|
||||||
|
/// As opposed to `map`, this offers destructuring.
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case Subclass2(:final field2):
|
||||||
|
/// return ...;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, @JsonKey(name: 'file_id') String fileId, SnCloudFile? file, String usage, @JsonKey(name: 'resource_id') String resourceId, @JsonKey(name: 'expired_at') DateTime? expiredAt, @JsonKey(name: 'created_at') DateTime createdAt, @JsonKey(name: 'updated_at') DateTime updatedAt, @JsonKey(name: 'deleted_at') DateTime? deletedAt) $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _Reference():
|
||||||
|
return $default(_that.id,_that.fileId,_that.file,_that.usage,_that.resourceId,_that.expiredAt,_that.createdAt,_that.updatedAt,_that.deletedAt);}
|
||||||
|
}
|
||||||
|
/// A variant of `when` that fallback to returning `null`
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return null;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, @JsonKey(name: 'file_id') String fileId, SnCloudFile? file, String usage, @JsonKey(name: 'resource_id') String resourceId, @JsonKey(name: 'expired_at') DateTime? expiredAt, @JsonKey(name: 'created_at') DateTime createdAt, @JsonKey(name: 'updated_at') DateTime updatedAt, @JsonKey(name: 'deleted_at') DateTime? deletedAt)? $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _Reference() when $default != null:
|
||||||
|
return $default(_that.id,_that.fileId,_that.file,_that.usage,_that.resourceId,_that.expiredAt,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
|
||||||
|
class _Reference implements Reference {
|
||||||
|
const _Reference({required this.id, @JsonKey(name: 'file_id') required this.fileId, this.file, required this.usage, @JsonKey(name: 'resource_id') required this.resourceId, @JsonKey(name: 'expired_at') this.expiredAt, @JsonKey(name: 'created_at') required this.createdAt, @JsonKey(name: 'updated_at') required this.updatedAt, @JsonKey(name: 'deleted_at') this.deletedAt});
|
||||||
|
factory _Reference.fromJson(Map<String, dynamic> json) => _$ReferenceFromJson(json);
|
||||||
|
|
||||||
|
@override final String id;
|
||||||
|
@override@JsonKey(name: 'file_id') final String fileId;
|
||||||
|
@override final SnCloudFile? file;
|
||||||
|
@override final String usage;
|
||||||
|
@override@JsonKey(name: 'resource_id') final String resourceId;
|
||||||
|
@override@JsonKey(name: 'expired_at') final DateTime? expiredAt;
|
||||||
|
@override@JsonKey(name: 'created_at') final DateTime createdAt;
|
||||||
|
@override@JsonKey(name: 'updated_at') final DateTime updatedAt;
|
||||||
|
@override@JsonKey(name: 'deleted_at') final DateTime? deletedAt;
|
||||||
|
|
||||||
|
/// Create a copy of Reference
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$ReferenceCopyWith<_Reference> get copyWith => __$ReferenceCopyWithImpl<_Reference>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$ReferenceToJson(this, );
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is _Reference&&(identical(other.id, id) || other.id == id)&&(identical(other.fileId, fileId) || other.fileId == fileId)&&(identical(other.file, file) || other.file == file)&&(identical(other.usage, usage) || other.usage == usage)&&(identical(other.resourceId, resourceId) || other.resourceId == resourceId)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,id,fileId,file,usage,resourceId,expiredAt,createdAt,updatedAt,deletedAt);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'Reference(id: $id, fileId: $fileId, file: $file, usage: $usage, resourceId: $resourceId, expiredAt: $expiredAt, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class _$ReferenceCopyWith<$Res> implements $ReferenceCopyWith<$Res> {
|
||||||
|
factory _$ReferenceCopyWith(_Reference value, $Res Function(_Reference) _then) = __$ReferenceCopyWithImpl;
|
||||||
|
@override @useResult
|
||||||
|
$Res call({
|
||||||
|
String id,@JsonKey(name: 'file_id') String fileId, SnCloudFile? file, String usage,@JsonKey(name: 'resource_id') String resourceId,@JsonKey(name: 'expired_at') DateTime? expiredAt,@JsonKey(name: 'created_at') DateTime createdAt,@JsonKey(name: 'updated_at') DateTime updatedAt,@JsonKey(name: 'deleted_at') DateTime? deletedAt
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
@override $SnCloudFileCopyWith<$Res>? get file;
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class __$ReferenceCopyWithImpl<$Res>
|
||||||
|
implements _$ReferenceCopyWith<$Res> {
|
||||||
|
__$ReferenceCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final _Reference _self;
|
||||||
|
final $Res Function(_Reference) _then;
|
||||||
|
|
||||||
|
/// Create a copy of Reference
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? fileId = null,Object? file = freezed,Object? usage = null,Object? resourceId = null,Object? expiredAt = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||||
|
return _then(_Reference(
|
||||||
|
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,fileId: null == fileId ? _self.fileId : fileId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,file: freezed == file ? _self.file : file // ignore: cast_nullable_to_non_nullable
|
||||||
|
as SnCloudFile?,usage: null == usage ? _self.usage : usage // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,resourceId: null == resourceId ? _self.resourceId : resourceId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a copy of Reference
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnCloudFileCopyWith<$Res>? get file {
|
||||||
|
if (_self.file == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $SnCloudFileCopyWith<$Res>(_self.file!, (value) {
|
||||||
|
return _then(_self.copyWith(file: value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// dart format on
|
||||||
41
lib/models/reference.g.dart
Normal file
41
lib/models/reference.g.dart
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'reference.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
_Reference _$ReferenceFromJson(Map<String, dynamic> json) => _Reference(
|
||||||
|
id: json['id'] as String,
|
||||||
|
fileId: json['file_id'] as String,
|
||||||
|
file:
|
||||||
|
json['file'] == null
|
||||||
|
? null
|
||||||
|
: SnCloudFile.fromJson(json['file'] as Map<String, dynamic>),
|
||||||
|
usage: json['usage'] as String,
|
||||||
|
resourceId: json['resource_id'] as String,
|
||||||
|
expiredAt:
|
||||||
|
json['expired_at'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['expired_at'] as String),
|
||||||
|
createdAt: DateTime.parse(json['created_at'] as String),
|
||||||
|
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||||
|
deletedAt:
|
||||||
|
json['deleted_at'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['deleted_at'] as String),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$ReferenceToJson(_Reference instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'file_id': instance.fileId,
|
||||||
|
'file': instance.file?.toJson(),
|
||||||
|
'usage': instance.usage,
|
||||||
|
'resource_id': instance.resourceId,
|
||||||
|
'expired_at': instance.expiredAt?.toIso8601String(),
|
||||||
|
'created_at': instance.createdAt.toIso8601String(),
|
||||||
|
'updated_at': instance.updatedAt.toIso8601String(),
|
||||||
|
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||||
|
};
|
||||||
25
lib/models/site_file.dart
Normal file
25
lib/models/site_file.dart
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'site_file.freezed.dart';
|
||||||
|
part 'site_file.g.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
sealed class SnSiteFileEntry with _$SnSiteFileEntry {
|
||||||
|
const factory SnSiteFileEntry({
|
||||||
|
required bool isDirectory,
|
||||||
|
required String relativePath,
|
||||||
|
required int size, // Size in bytes (0 for directories)
|
||||||
|
required DateTime modified, // ISO 8601 timestamp
|
||||||
|
}) = _SnSiteFileEntry;
|
||||||
|
|
||||||
|
factory SnSiteFileEntry.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SnSiteFileEntryFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
sealed class SnFileContent with _$SnFileContent {
|
||||||
|
const factory SnFileContent({required String content}) = _SnFileContent;
|
||||||
|
|
||||||
|
factory SnFileContent.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SnFileContentFromJson(json);
|
||||||
|
}
|
||||||
539
lib/models/site_file.freezed.dart
Normal file
539
lib/models/site_file.freezed.dart
Normal file
@@ -0,0 +1,539 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// coverage:ignore-file
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||||
|
|
||||||
|
part of 'site_file.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// FreezedGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// dart format off
|
||||||
|
T _$identity<T>(T value) => value;
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$SnSiteFileEntry {
|
||||||
|
|
||||||
|
bool get isDirectory; String get relativePath; int get size;// Size in bytes (0 for directories)
|
||||||
|
DateTime get modified;
|
||||||
|
/// Create a copy of SnSiteFileEntry
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnSiteFileEntryCopyWith<SnSiteFileEntry> get copyWith => _$SnSiteFileEntryCopyWithImpl<SnSiteFileEntry>(this as SnSiteFileEntry, _$identity);
|
||||||
|
|
||||||
|
/// Serializes this SnSiteFileEntry to a JSON map.
|
||||||
|
Map<String, dynamic> toJson();
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnSiteFileEntry&&(identical(other.isDirectory, isDirectory) || other.isDirectory == isDirectory)&&(identical(other.relativePath, relativePath) || other.relativePath == relativePath)&&(identical(other.size, size) || other.size == size)&&(identical(other.modified, modified) || other.modified == modified));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,isDirectory,relativePath,size,modified);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SnSiteFileEntry(isDirectory: $isDirectory, relativePath: $relativePath, size: $size, modified: $modified)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class $SnSiteFileEntryCopyWith<$Res> {
|
||||||
|
factory $SnSiteFileEntryCopyWith(SnSiteFileEntry value, $Res Function(SnSiteFileEntry) _then) = _$SnSiteFileEntryCopyWithImpl;
|
||||||
|
@useResult
|
||||||
|
$Res call({
|
||||||
|
bool isDirectory, String relativePath, int size, DateTime modified
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class _$SnSiteFileEntryCopyWithImpl<$Res>
|
||||||
|
implements $SnSiteFileEntryCopyWith<$Res> {
|
||||||
|
_$SnSiteFileEntryCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final SnSiteFileEntry _self;
|
||||||
|
final $Res Function(SnSiteFileEntry) _then;
|
||||||
|
|
||||||
|
/// Create a copy of SnSiteFileEntry
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline') @override $Res call({Object? isDirectory = null,Object? relativePath = null,Object? size = null,Object? modified = null,}) {
|
||||||
|
return _then(_self.copyWith(
|
||||||
|
isDirectory: null == isDirectory ? _self.isDirectory : isDirectory // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,relativePath: null == relativePath ? _self.relativePath : relativePath // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,size: null == size ? _self.size : size // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,modified: null == modified ? _self.modified : modified // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Adds pattern-matching-related methods to [SnSiteFileEntry].
|
||||||
|
extension SnSiteFileEntryPatterns on SnSiteFileEntry {
|
||||||
|
/// A variant of `map` that fallback to returning `orElse`.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return orElse();
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnSiteFileEntry value)? $default,{required TResult orElse(),}){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnSiteFileEntry() when $default != null:
|
||||||
|
return $default(_that);case _:
|
||||||
|
return orElse();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A `switch`-like method, using callbacks.
|
||||||
|
///
|
||||||
|
/// Callbacks receives the raw object, upcasted.
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case final Subclass2 value:
|
||||||
|
/// return ...;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnSiteFileEntry value) $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnSiteFileEntry():
|
||||||
|
return $default(_that);}
|
||||||
|
}
|
||||||
|
/// A variant of `map` that fallback to returning `null`.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return null;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnSiteFileEntry value)? $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnSiteFileEntry() when $default != null:
|
||||||
|
return $default(_that);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A variant of `when` that fallback to an `orElse` callback.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return orElse();
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool isDirectory, String relativePath, int size, DateTime modified)? $default,{required TResult orElse(),}) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnSiteFileEntry() when $default != null:
|
||||||
|
return $default(_that.isDirectory,_that.relativePath,_that.size,_that.modified);case _:
|
||||||
|
return orElse();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A `switch`-like method, using callbacks.
|
||||||
|
///
|
||||||
|
/// As opposed to `map`, this offers destructuring.
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case Subclass2(:final field2):
|
||||||
|
/// return ...;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool isDirectory, String relativePath, int size, DateTime modified) $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnSiteFileEntry():
|
||||||
|
return $default(_that.isDirectory,_that.relativePath,_that.size,_that.modified);}
|
||||||
|
}
|
||||||
|
/// A variant of `when` that fallback to returning `null`
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return null;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool isDirectory, String relativePath, int size, DateTime modified)? $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnSiteFileEntry() when $default != null:
|
||||||
|
return $default(_that.isDirectory,_that.relativePath,_that.size,_that.modified);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
|
||||||
|
class _SnSiteFileEntry implements SnSiteFileEntry {
|
||||||
|
const _SnSiteFileEntry({required this.isDirectory, required this.relativePath, required this.size, required this.modified});
|
||||||
|
factory _SnSiteFileEntry.fromJson(Map<String, dynamic> json) => _$SnSiteFileEntryFromJson(json);
|
||||||
|
|
||||||
|
@override final bool isDirectory;
|
||||||
|
@override final String relativePath;
|
||||||
|
@override final int size;
|
||||||
|
// Size in bytes (0 for directories)
|
||||||
|
@override final DateTime modified;
|
||||||
|
|
||||||
|
/// Create a copy of SnSiteFileEntry
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$SnSiteFileEntryCopyWith<_SnSiteFileEntry> get copyWith => __$SnSiteFileEntryCopyWithImpl<_SnSiteFileEntry>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$SnSiteFileEntryToJson(this, );
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnSiteFileEntry&&(identical(other.isDirectory, isDirectory) || other.isDirectory == isDirectory)&&(identical(other.relativePath, relativePath) || other.relativePath == relativePath)&&(identical(other.size, size) || other.size == size)&&(identical(other.modified, modified) || other.modified == modified));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,isDirectory,relativePath,size,modified);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SnSiteFileEntry(isDirectory: $isDirectory, relativePath: $relativePath, size: $size, modified: $modified)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class _$SnSiteFileEntryCopyWith<$Res> implements $SnSiteFileEntryCopyWith<$Res> {
|
||||||
|
factory _$SnSiteFileEntryCopyWith(_SnSiteFileEntry value, $Res Function(_SnSiteFileEntry) _then) = __$SnSiteFileEntryCopyWithImpl;
|
||||||
|
@override @useResult
|
||||||
|
$Res call({
|
||||||
|
bool isDirectory, String relativePath, int size, DateTime modified
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class __$SnSiteFileEntryCopyWithImpl<$Res>
|
||||||
|
implements _$SnSiteFileEntryCopyWith<$Res> {
|
||||||
|
__$SnSiteFileEntryCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final _SnSiteFileEntry _self;
|
||||||
|
final $Res Function(_SnSiteFileEntry) _then;
|
||||||
|
|
||||||
|
/// Create a copy of SnSiteFileEntry
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @pragma('vm:prefer-inline') $Res call({Object? isDirectory = null,Object? relativePath = null,Object? size = null,Object? modified = null,}) {
|
||||||
|
return _then(_SnSiteFileEntry(
|
||||||
|
isDirectory: null == isDirectory ? _self.isDirectory : isDirectory // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,relativePath: null == relativePath ? _self.relativePath : relativePath // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,size: null == size ? _self.size : size // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,modified: null == modified ? _self.modified : modified // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$SnFileContent {
|
||||||
|
|
||||||
|
String get content;
|
||||||
|
/// Create a copy of SnFileContent
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnFileContentCopyWith<SnFileContent> get copyWith => _$SnFileContentCopyWithImpl<SnFileContent>(this as SnFileContent, _$identity);
|
||||||
|
|
||||||
|
/// Serializes this SnFileContent to a JSON map.
|
||||||
|
Map<String, dynamic> toJson();
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnFileContent&&(identical(other.content, content) || other.content == content));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,content);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SnFileContent(content: $content)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class $SnFileContentCopyWith<$Res> {
|
||||||
|
factory $SnFileContentCopyWith(SnFileContent value, $Res Function(SnFileContent) _then) = _$SnFileContentCopyWithImpl;
|
||||||
|
@useResult
|
||||||
|
$Res call({
|
||||||
|
String content
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class _$SnFileContentCopyWithImpl<$Res>
|
||||||
|
implements $SnFileContentCopyWith<$Res> {
|
||||||
|
_$SnFileContentCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final SnFileContent _self;
|
||||||
|
final $Res Function(SnFileContent) _then;
|
||||||
|
|
||||||
|
/// Create a copy of SnFileContent
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline') @override $Res call({Object? content = null,}) {
|
||||||
|
return _then(_self.copyWith(
|
||||||
|
content: null == content ? _self.content : content // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Adds pattern-matching-related methods to [SnFileContent].
|
||||||
|
extension SnFileContentPatterns on SnFileContent {
|
||||||
|
/// A variant of `map` that fallback to returning `orElse`.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return orElse();
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnFileContent value)? $default,{required TResult orElse(),}){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnFileContent() when $default != null:
|
||||||
|
return $default(_that);case _:
|
||||||
|
return orElse();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A `switch`-like method, using callbacks.
|
||||||
|
///
|
||||||
|
/// Callbacks receives the raw object, upcasted.
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case final Subclass2 value:
|
||||||
|
/// return ...;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnFileContent value) $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnFileContent():
|
||||||
|
return $default(_that);}
|
||||||
|
}
|
||||||
|
/// A variant of `map` that fallback to returning `null`.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return null;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnFileContent value)? $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnFileContent() when $default != null:
|
||||||
|
return $default(_that);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A variant of `when` that fallback to an `orElse` callback.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return orElse();
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String content)? $default,{required TResult orElse(),}) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnFileContent() when $default != null:
|
||||||
|
return $default(_that.content);case _:
|
||||||
|
return orElse();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A `switch`-like method, using callbacks.
|
||||||
|
///
|
||||||
|
/// As opposed to `map`, this offers destructuring.
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case Subclass2(:final field2):
|
||||||
|
/// return ...;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String content) $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnFileContent():
|
||||||
|
return $default(_that.content);}
|
||||||
|
}
|
||||||
|
/// A variant of `when` that fallback to returning `null`
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return null;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String content)? $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _SnFileContent() when $default != null:
|
||||||
|
return $default(_that.content);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
|
||||||
|
class _SnFileContent implements SnFileContent {
|
||||||
|
const _SnFileContent({required this.content});
|
||||||
|
factory _SnFileContent.fromJson(Map<String, dynamic> json) => _$SnFileContentFromJson(json);
|
||||||
|
|
||||||
|
@override final String content;
|
||||||
|
|
||||||
|
/// Create a copy of SnFileContent
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$SnFileContentCopyWith<_SnFileContent> get copyWith => __$SnFileContentCopyWithImpl<_SnFileContent>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$SnFileContentToJson(this, );
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnFileContent&&(identical(other.content, content) || other.content == content));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,content);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SnFileContent(content: $content)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class _$SnFileContentCopyWith<$Res> implements $SnFileContentCopyWith<$Res> {
|
||||||
|
factory _$SnFileContentCopyWith(_SnFileContent value, $Res Function(_SnFileContent) _then) = __$SnFileContentCopyWithImpl;
|
||||||
|
@override @useResult
|
||||||
|
$Res call({
|
||||||
|
String content
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class __$SnFileContentCopyWithImpl<$Res>
|
||||||
|
implements _$SnFileContentCopyWith<$Res> {
|
||||||
|
__$SnFileContentCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final _SnFileContent _self;
|
||||||
|
final $Res Function(_SnFileContent) _then;
|
||||||
|
|
||||||
|
/// Create a copy of SnFileContent
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @pragma('vm:prefer-inline') $Res call({Object? content = null,}) {
|
||||||
|
return _then(_SnFileContent(
|
||||||
|
content: null == content ? _self.content : content // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// dart format on
|
||||||
29
lib/models/site_file.g.dart
Normal file
29
lib/models/site_file.g.dart
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'site_file.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
_SnSiteFileEntry _$SnSiteFileEntryFromJson(Map<String, dynamic> json) =>
|
||||||
|
_SnSiteFileEntry(
|
||||||
|
isDirectory: json['is_directory'] as bool,
|
||||||
|
relativePath: json['relative_path'] as String,
|
||||||
|
size: (json['size'] as num).toInt(),
|
||||||
|
modified: DateTime.parse(json['modified'] as String),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$SnSiteFileEntryToJson(_SnSiteFileEntry instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'is_directory': instance.isDirectory,
|
||||||
|
'relative_path': instance.relativePath,
|
||||||
|
'size': instance.size,
|
||||||
|
'modified': instance.modified.toIso8601String(),
|
||||||
|
};
|
||||||
|
|
||||||
|
_SnFileContent _$SnFileContentFromJson(Map<String, dynamic> json) =>
|
||||||
|
_SnFileContent(content: json['content'] as String);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$SnFileContentToJson(_SnFileContent instance) =>
|
||||||
|
<String, dynamic>{'content': instance.content};
|
||||||
@@ -212,8 +212,14 @@ class CallNotifier extends _$CallNotifier {
|
|||||||
String? _roomId;
|
String? _roomId;
|
||||||
String? get roomId => _roomId;
|
String? get roomId => _roomId;
|
||||||
|
|
||||||
Future<void> joinRoom(String roomId) async {
|
SnChatRoom? _chatRoom;
|
||||||
if (_roomId == roomId && _room != null) {
|
SnChatRoom? get chatRoom => _chatRoom;
|
||||||
|
|
||||||
|
Future<void> joinRoom(SnChatRoom room) async {
|
||||||
|
var roomId = room.id;
|
||||||
|
if (_roomId == roomId &&
|
||||||
|
_room != null &&
|
||||||
|
_room?.connectionState == lk.ConnectionState.connected) {
|
||||||
talker.info('[Call] Call skipped. Already has data');
|
talker.info('[Call] Call skipped. Already has data');
|
||||||
return;
|
return;
|
||||||
} else if (_room != null) {
|
} else if (_room != null) {
|
||||||
@@ -223,6 +229,7 @@ class CallNotifier extends _$CallNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
_roomId = roomId;
|
_roomId = roomId;
|
||||||
|
_chatRoom = room;
|
||||||
if (_room != null) {
|
if (_room != null) {
|
||||||
await _room!.disconnect();
|
await _room!.disconnect();
|
||||||
await _room!.dispose();
|
await _room!.dispose();
|
||||||
@@ -355,6 +362,7 @@ class CallNotifier extends _$CallNotifier {
|
|||||||
sourceId: source.id,
|
sourceId: source.id,
|
||||||
maxFrameRate: 30.0,
|
maxFrameRate: 30.0,
|
||||||
captureScreenAudio: true,
|
captureScreenAudio: true,
|
||||||
|
useiOSBroadcastExtension: true,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
await _localParticipant!.publishVideoTrack(track);
|
await _localParticipant!.publishVideoTrack(track);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ part of 'call.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$callNotifierHash() => r'a8ca3f625c0db3ad9992033ae70864ce15efc281';
|
String _$callNotifierHash() => r'2caee30f42315e539cb4df17c0d464ceed41ffa0';
|
||||||
|
|
||||||
/// See also [CallNotifier].
|
/// See also [CallNotifier].
|
||||||
@ProviderFor(CallNotifier)
|
@ProviderFor(CallNotifier)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import "package:flutter/material.dart";
|
|||||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||||
import "package:island/database/drift_db.dart";
|
import "package:island/database/drift_db.dart";
|
||||||
import "package:island/database/message.dart";
|
import "package:island/database/message.dart";
|
||||||
|
import "package:island/models/account.dart";
|
||||||
import "package:island/models/chat.dart";
|
import "package:island/models/chat.dart";
|
||||||
import "package:island/models/file.dart";
|
import "package:island/models/file.dart";
|
||||||
import "package:island/models/poll.dart";
|
import "package:island/models/poll.dart";
|
||||||
@@ -20,6 +21,7 @@ import "package:riverpod_annotation/riverpod_annotation.dart";
|
|||||||
import "package:uuid/uuid.dart";
|
import "package:uuid/uuid.dart";
|
||||||
import "package:island/screens/chat/chat.dart";
|
import "package:island/screens/chat/chat.dart";
|
||||||
import "package:island/pods/chat/chat_rooms.dart";
|
import "package:island/pods/chat/chat_rooms.dart";
|
||||||
|
import "package:island/screens/account/profile.dart";
|
||||||
|
|
||||||
part 'messages_notifier.g.dart';
|
part 'messages_notifier.g.dart';
|
||||||
|
|
||||||
@@ -45,6 +47,8 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
bool _isUpdatingState = false;
|
bool _isUpdatingState = false;
|
||||||
DateTime? _lastPauseTime;
|
DateTime? _lastPauseTime;
|
||||||
|
|
||||||
|
late final Future<SnAccount?> Function(String) _fetchAccount;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<List<LocalChatMessage>> build(String roomId) async {
|
FutureOr<List<LocalChatMessage>> build(String roomId) async {
|
||||||
_roomId = roomId;
|
_roomId = roomId;
|
||||||
@@ -53,6 +57,15 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
final room = await ref.watch(chatroomProvider(roomId).future);
|
final room = await ref.watch(chatroomProvider(roomId).future);
|
||||||
final identity = await ref.watch(chatroomIdentityProvider(roomId).future);
|
final identity = await ref.watch(chatroomIdentityProvider(roomId).future);
|
||||||
|
|
||||||
|
// Initialize fetch account method for corrupted data recovery
|
||||||
|
_fetchAccount = (String accountId) async {
|
||||||
|
try {
|
||||||
|
return await ref.watch(accountProvider(accountId).future);
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (room == null) {
|
if (room == null) {
|
||||||
throw Exception('Room not found');
|
throw Exception('Room not found');
|
||||||
}
|
}
|
||||||
@@ -133,6 +146,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
_roomId,
|
_roomId,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
withAttachments: withAttachments,
|
withAttachments: withAttachments,
|
||||||
|
fetchAccount: _fetchAccount,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
final chatMessagesFromDb = await _database.getMessagesForRoom(
|
final chatMessagesFromDb = await _database.getMessagesForRoom(
|
||||||
@@ -140,8 +154,16 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
offset: offset,
|
offset: offset,
|
||||||
limit: take,
|
limit: take,
|
||||||
);
|
);
|
||||||
dbMessages =
|
dbMessages = await Future.wait(
|
||||||
chatMessagesFromDb.map(_database.companionToMessage).toList();
|
chatMessagesFromDb
|
||||||
|
.map(
|
||||||
|
(msg) => _database.companionToMessage(
|
||||||
|
msg,
|
||||||
|
fetchAccount: _fetchAccount,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<LocalChatMessage> filteredMessages = dbMessages;
|
List<LocalChatMessage> filteredMessages = dbMessages;
|
||||||
@@ -202,8 +224,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>[];
|
||||||
@@ -272,6 +300,9 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
|
|
||||||
for (final message in messages) {
|
for (final message in messages) {
|
||||||
await _database.saveMessage(_database.messageToCompanion(message));
|
await _database.saveMessage(_database.messageToCompanion(message));
|
||||||
|
if (message.sender != null) {
|
||||||
|
await _database.saveMember(message.sender!); // Save/update member data
|
||||||
|
}
|
||||||
if (message.nonce != null) {
|
if (message.nonce != null) {
|
||||||
_pendingMessages.removeWhere(
|
_pendingMessages.removeWhere(
|
||||||
(_, pendingMsg) => pendingMsg.nonce == message.nonce,
|
(_, pendingMsg) => pendingMsg.nonce == message.nonce,
|
||||||
@@ -300,7 +331,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');
|
||||||
@@ -468,6 +502,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.saveMessage(_database.messageToCompanion(localMessage));
|
||||||
|
await _database.saveMember(mockMessage.sender);
|
||||||
|
|
||||||
final currentMessages = state.value ?? [];
|
final currentMessages = state.value ?? [];
|
||||||
state = AsyncValue.data([localMessage, ...currentMessages]);
|
state = AsyncValue.data([localMessage, ...currentMessages]);
|
||||||
@@ -888,7 +923,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(
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ part of 'messages_notifier.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$messagesNotifierHash() => r'fc9c99024a0801efa4894f250aea8bdc6127a0b6';
|
String _$messagesNotifierHash() => r'27e5d686d9204ba39adbd1838cf4a6eaea0ac85f';
|
||||||
|
|
||||||
/// Copied from Dart SDK
|
/// Copied from Dart SDK
|
||||||
class _SystemHash {
|
class _SystemHash {
|
||||||
|
|||||||
@@ -11,12 +11,36 @@ part 'file_list.g.dart';
|
|||||||
class CloudFileListNotifier extends _$CloudFileListNotifier
|
class CloudFileListNotifier extends _$CloudFileListNotifier
|
||||||
with CursorPagingNotifierMixin<FileListItem> {
|
with CursorPagingNotifierMixin<FileListItem> {
|
||||||
String _currentPath = '/';
|
String _currentPath = '/';
|
||||||
|
String? _poolId;
|
||||||
|
String? _query;
|
||||||
|
String? _order;
|
||||||
|
bool _orderDesc = false;
|
||||||
|
|
||||||
void setPath(String path) {
|
void setPath(String path) {
|
||||||
_currentPath = path;
|
_currentPath = path;
|
||||||
ref.invalidateSelf();
|
ref.invalidateSelf();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setPool(String? poolId) {
|
||||||
|
_poolId = poolId;
|
||||||
|
ref.invalidateSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setQuery(String? query) {
|
||||||
|
_query = query;
|
||||||
|
ref.invalidateSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setOrder(String? order) {
|
||||||
|
_order = order;
|
||||||
|
ref.invalidateSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setOrderDesc(bool orderDesc) {
|
||||||
|
_orderDesc = orderDesc;
|
||||||
|
ref.invalidateSelf();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<CursorPagingData<FileListItem>> build() => fetch(cursor: null);
|
Future<CursorPagingData<FileListItem>> build() => fetch(cursor: null);
|
||||||
|
|
||||||
@@ -26,9 +50,25 @@ class CloudFileListNotifier extends _$CloudFileListNotifier
|
|||||||
}) async {
|
}) async {
|
||||||
final client = ref.read(apiClientProvider);
|
final client = ref.read(apiClientProvider);
|
||||||
|
|
||||||
|
final queryParameters = <String, String>{'path': _currentPath};
|
||||||
|
|
||||||
|
if (_poolId != null) {
|
||||||
|
queryParameters['pool'] = _poolId!;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_query != null) {
|
||||||
|
queryParameters['query'] = _query!;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_order != null) {
|
||||||
|
queryParameters['order'] = _order!;
|
||||||
|
}
|
||||||
|
|
||||||
|
queryParameters['orderDesc'] = _orderDesc.toString();
|
||||||
|
|
||||||
final response = await client.get(
|
final response = await client.get(
|
||||||
'/drive/index/browse',
|
'/drive/index/browse',
|
||||||
queryParameters: {'path': _currentPath},
|
queryParameters: queryParameters,
|
||||||
);
|
);
|
||||||
|
|
||||||
final List<String> folders =
|
final List<String> folders =
|
||||||
@@ -58,6 +98,37 @@ Future<Map<String, dynamic>?> billingUsage(Ref ref) async {
|
|||||||
@riverpod
|
@riverpod
|
||||||
class UnindexedFileListNotifier extends _$UnindexedFileListNotifier
|
class UnindexedFileListNotifier extends _$UnindexedFileListNotifier
|
||||||
with CursorPagingNotifierMixin<FileListItem> {
|
with CursorPagingNotifierMixin<FileListItem> {
|
||||||
|
String? _poolId;
|
||||||
|
bool _recycled = false;
|
||||||
|
String? _query;
|
||||||
|
String? _order;
|
||||||
|
bool _orderDesc = false;
|
||||||
|
|
||||||
|
void setPool(String? poolId) {
|
||||||
|
_poolId = poolId;
|
||||||
|
ref.invalidateSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setRecycled(bool recycled) {
|
||||||
|
_recycled = recycled;
|
||||||
|
ref.invalidateSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setQuery(String? query) {
|
||||||
|
_query = query;
|
||||||
|
ref.invalidateSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setOrder(String? order) {
|
||||||
|
_order = order;
|
||||||
|
ref.invalidateSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setOrderDesc(bool orderDesc) {
|
||||||
|
_orderDesc = orderDesc;
|
||||||
|
ref.invalidateSelf();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<CursorPagingData<FileListItem>> build() => fetch(cursor: null);
|
Future<CursorPagingData<FileListItem>> build() => fetch(cursor: null);
|
||||||
|
|
||||||
@@ -70,9 +141,32 @@ class UnindexedFileListNotifier extends _$UnindexedFileListNotifier
|
|||||||
final offset = cursor != null ? int.tryParse(cursor) ?? 0 : 0;
|
final offset = cursor != null ? int.tryParse(cursor) ?? 0 : 0;
|
||||||
const take = 50; // Default page size
|
const take = 50; // Default page size
|
||||||
|
|
||||||
|
final queryParameters = <String, String>{
|
||||||
|
'take': take.toString(),
|
||||||
|
'offset': offset.toString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (_poolId != null) {
|
||||||
|
queryParameters['pool'] = _poolId!;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_recycled) {
|
||||||
|
queryParameters['recycled'] = _recycled.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_query != null) {
|
||||||
|
queryParameters['query'] = _query!;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_order != null) {
|
||||||
|
queryParameters['order'] = _order!;
|
||||||
|
}
|
||||||
|
|
||||||
|
queryParameters['orderDesc'] = _orderDesc.toString();
|
||||||
|
|
||||||
final response = await client.get(
|
final response = await client.get(
|
||||||
'/drive/index/unindexed',
|
'/drive/index/unindexed',
|
||||||
queryParameters: {'take': take.toString(), 'offset': offset.toString()},
|
queryParameters: queryParameters,
|
||||||
);
|
);
|
||||||
|
|
||||||
final total = int.tryParse(response.headers.value('x-total') ?? '0') ?? 0;
|
final total = int.tryParse(response.headers.value('x-total') ?? '0') ?? 0;
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ final billingQuotaProvider =
|
|||||||
// ignore: unused_element
|
// ignore: unused_element
|
||||||
typedef BillingQuotaRef = AutoDisposeFutureProviderRef<Map<String, dynamic>?>;
|
typedef BillingQuotaRef = AutoDisposeFutureProviderRef<Map<String, dynamic>?>;
|
||||||
String _$cloudFileListNotifierHash() =>
|
String _$cloudFileListNotifierHash() =>
|
||||||
r'5f2f80357cb31ac6473df5ac2101f9a462004f81';
|
r'533dfa86f920b60cf7491fb4aeb95ece19e428af';
|
||||||
|
|
||||||
/// See also [CloudFileListNotifier].
|
/// See also [CloudFileListNotifier].
|
||||||
@ProviderFor(CloudFileListNotifier)
|
@ProviderFor(CloudFileListNotifier)
|
||||||
@@ -66,7 +66,7 @@ final cloudFileListNotifierProvider = AutoDisposeAsyncNotifierProvider<
|
|||||||
typedef _$CloudFileListNotifier =
|
typedef _$CloudFileListNotifier =
|
||||||
AutoDisposeAsyncNotifier<CursorPagingData<FileListItem>>;
|
AutoDisposeAsyncNotifier<CursorPagingData<FileListItem>>;
|
||||||
String _$unindexedFileListNotifierHash() =>
|
String _$unindexedFileListNotifierHash() =>
|
||||||
r'48fc92432a50a562190da5fe8ed0920d171b07b6';
|
r'afa487d7b956b71b21ca1b073a01364a34ede1d5';
|
||||||
|
|
||||||
/// See also [UnindexedFileListNotifier].
|
/// See also [UnindexedFileListNotifier].
|
||||||
@ProviderFor(UnindexedFileListNotifier)
|
@ProviderFor(UnindexedFileListNotifier)
|
||||||
|
|||||||
16
lib/pods/file_references.dart
Normal file
16
lib/pods/file_references.dart
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
import 'package:island/models/reference.dart';
|
||||||
|
import 'package:island/pods/network.dart';
|
||||||
|
|
||||||
|
part 'file_references.g.dart';
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
Future<List<Reference>> fileReferences(Ref ref, String fileId) async {
|
||||||
|
final client = ref.read(apiClientProvider);
|
||||||
|
final response = await client.get('/drive/files/$fileId/references');
|
||||||
|
final list = response.data as List<dynamic>;
|
||||||
|
return list
|
||||||
|
.map((json) => Reference.fromJson(json as Map<String, dynamic>))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
153
lib/pods/file_references.g.dart
Normal file
153
lib/pods/file_references.g.dart
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'file_references.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
String _$fileReferencesHash() => r'd66c678c221f61978bdb242b98e6dbe31d0c204b';
|
||||||
|
|
||||||
|
/// Copied from Dart SDK
|
||||||
|
class _SystemHash {
|
||||||
|
_SystemHash._();
|
||||||
|
|
||||||
|
static int combine(int hash, int value) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + value);
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||||
|
return hash ^ (hash >> 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int finish(int hash) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = hash ^ (hash >> 11);
|
||||||
|
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [fileReferences].
|
||||||
|
@ProviderFor(fileReferences)
|
||||||
|
const fileReferencesProvider = FileReferencesFamily();
|
||||||
|
|
||||||
|
/// See also [fileReferences].
|
||||||
|
class FileReferencesFamily extends Family<AsyncValue<List<Reference>>> {
|
||||||
|
/// See also [fileReferences].
|
||||||
|
const FileReferencesFamily();
|
||||||
|
|
||||||
|
/// See also [fileReferences].
|
||||||
|
FileReferencesProvider call(String fileId) {
|
||||||
|
return FileReferencesProvider(fileId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
FileReferencesProvider getProviderOverride(
|
||||||
|
covariant FileReferencesProvider provider,
|
||||||
|
) {
|
||||||
|
return call(provider.fileId);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||||
|
_allTransitiveDependencies;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get name => r'fileReferencesProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [fileReferences].
|
||||||
|
class FileReferencesProvider
|
||||||
|
extends AutoDisposeFutureProvider<List<Reference>> {
|
||||||
|
/// See also [fileReferences].
|
||||||
|
FileReferencesProvider(String fileId)
|
||||||
|
: this._internal(
|
||||||
|
(ref) => fileReferences(ref as FileReferencesRef, fileId),
|
||||||
|
from: fileReferencesProvider,
|
||||||
|
name: r'fileReferencesProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$fileReferencesHash,
|
||||||
|
dependencies: FileReferencesFamily._dependencies,
|
||||||
|
allTransitiveDependencies:
|
||||||
|
FileReferencesFamily._allTransitiveDependencies,
|
||||||
|
fileId: fileId,
|
||||||
|
);
|
||||||
|
|
||||||
|
FileReferencesProvider._internal(
|
||||||
|
super._createNotifier, {
|
||||||
|
required super.name,
|
||||||
|
required super.dependencies,
|
||||||
|
required super.allTransitiveDependencies,
|
||||||
|
required super.debugGetCreateSourceHash,
|
||||||
|
required super.from,
|
||||||
|
required this.fileId,
|
||||||
|
}) : super.internal();
|
||||||
|
|
||||||
|
final String fileId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Override overrideWith(
|
||||||
|
FutureOr<List<Reference>> Function(FileReferencesRef provider) create,
|
||||||
|
) {
|
||||||
|
return ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
override: FileReferencesProvider._internal(
|
||||||
|
(ref) => create(ref as FileReferencesRef),
|
||||||
|
from: from,
|
||||||
|
name: null,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
debugGetCreateSourceHash: null,
|
||||||
|
fileId: fileId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AutoDisposeFutureProviderElement<List<Reference>> createElement() {
|
||||||
|
return _FileReferencesProviderElement(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is FileReferencesProvider && other.fileId == fileId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, fileId.hashCode);
|
||||||
|
|
||||||
|
return _SystemHash.finish(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
mixin FileReferencesRef on AutoDisposeFutureProviderRef<List<Reference>> {
|
||||||
|
/// The parameter `fileId` of this provider.
|
||||||
|
String get fileId;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FileReferencesProviderElement
|
||||||
|
extends AutoDisposeFutureProviderElement<List<Reference>>
|
||||||
|
with FileReferencesRef {
|
||||||
|
_FileReferencesProviderElement(super.provider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get fileId => (origin as FileReferencesProvider).fileId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
159
lib/pods/site_files.dart
Normal file
159
lib/pods/site_files.dart
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:http_parser/http_parser.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:island/models/site_file.dart';
|
||||||
|
import 'package:island/pods/network.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
|
part 'site_files.g.dart';
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
Future<List<SnSiteFileEntry>> siteFiles(
|
||||||
|
Ref ref, {
|
||||||
|
required String siteId,
|
||||||
|
String? path,
|
||||||
|
}) async {
|
||||||
|
final apiClient = ref.watch(apiClientProvider);
|
||||||
|
final queryParams = path != null ? {'path': path} : null;
|
||||||
|
final resp = await apiClient.get(
|
||||||
|
'/zone/sites/$siteId/files',
|
||||||
|
queryParameters: queryParams,
|
||||||
|
);
|
||||||
|
final data = resp.data as List<dynamic>;
|
||||||
|
return data.map((json) => SnSiteFileEntry.fromJson(json)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
Future<SnFileContent> siteFileContent(
|
||||||
|
Ref ref, {
|
||||||
|
required String siteId,
|
||||||
|
required String relativePath,
|
||||||
|
}) async {
|
||||||
|
final apiClient = ref.watch(apiClientProvider);
|
||||||
|
final resp = await apiClient.get(
|
||||||
|
'/zone/sites/$siteId/files/content/$relativePath',
|
||||||
|
);
|
||||||
|
final content =
|
||||||
|
resp.data is String
|
||||||
|
? resp.data
|
||||||
|
: SnFileContent.fromJson(resp.data).content;
|
||||||
|
return SnFileContent(content: content);
|
||||||
|
}
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
Future<String> siteFileContentRaw(
|
||||||
|
Ref ref, {
|
||||||
|
required String siteId,
|
||||||
|
required String relativePath,
|
||||||
|
}) async {
|
||||||
|
final apiClient = ref.watch(apiClientProvider);
|
||||||
|
final resp = await apiClient.get(
|
||||||
|
'/zone/sites/$siteId/files/content/$relativePath',
|
||||||
|
);
|
||||||
|
return resp.data is String ? resp.data : resp.data['content'] as String;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SiteFilesNotifier
|
||||||
|
extends
|
||||||
|
AutoDisposeFamilyAsyncNotifier<
|
||||||
|
List<SnSiteFileEntry>,
|
||||||
|
({String siteId, String? path})
|
||||||
|
> {
|
||||||
|
@override
|
||||||
|
Future<List<SnSiteFileEntry>> build(
|
||||||
|
({String siteId, String? path}) arg,
|
||||||
|
) async {
|
||||||
|
return fetchFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<SnSiteFileEntry>> fetchFiles() async {
|
||||||
|
try {
|
||||||
|
final apiClient = ref.read(apiClientProvider);
|
||||||
|
final queryParams = arg.path != null ? {'path': arg.path} : null;
|
||||||
|
final resp = await apiClient.get(
|
||||||
|
'/zone/sites/${arg.siteId}/files',
|
||||||
|
queryParameters: queryParams,
|
||||||
|
);
|
||||||
|
final data = resp.data as List<dynamic>;
|
||||||
|
return data.map((json) => SnSiteFileEntry.fromJson(json)).toList();
|
||||||
|
} catch (e) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> uploadFile(File file, String filePath) async {
|
||||||
|
state = const AsyncValue.loading();
|
||||||
|
try {
|
||||||
|
final apiClient = ref.read(apiClientProvider);
|
||||||
|
|
||||||
|
// Create multipart form data
|
||||||
|
final formData = FormData.fromMap({
|
||||||
|
'filePath': filePath,
|
||||||
|
'file': await MultipartFile.fromFile(
|
||||||
|
file.path,
|
||||||
|
filename: file.path.split('/').last,
|
||||||
|
contentType: MediaType('application', 'octet-stream'),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
await apiClient.post(
|
||||||
|
'/zone/sites/${arg.siteId}/files/upload',
|
||||||
|
data: formData,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Refresh the files list
|
||||||
|
ref.invalidate(siteFilesProvider(siteId: arg.siteId, path: arg.path));
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
state = AsyncValue.error(error, stackTrace);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateFileContent(String relativePath, String newContent) async {
|
||||||
|
state = const AsyncValue.loading();
|
||||||
|
try {
|
||||||
|
final apiClient = ref.read(apiClientProvider);
|
||||||
|
await apiClient.put(
|
||||||
|
'/zone/sites/${arg.siteId}/files/edit/$relativePath',
|
||||||
|
data: {'new_content': newContent},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Refresh the files list
|
||||||
|
ref.invalidate(siteFilesProvider(siteId: arg.siteId, path: arg.path));
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
state = AsyncValue.error(error, stackTrace);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteFile(String relativePath) async {
|
||||||
|
state = const AsyncValue.loading();
|
||||||
|
try {
|
||||||
|
final apiClient = ref.read(apiClientProvider);
|
||||||
|
await apiClient.delete(
|
||||||
|
'/zone/sites/${arg.siteId}/files/delete/$relativePath',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Refresh the files list
|
||||||
|
ref.invalidate(siteFilesProvider(siteId: arg.siteId, path: arg.path));
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
state = AsyncValue.error(error, stackTrace);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> createDirectory(String directoryPath) async {
|
||||||
|
// For directories, we upload a dummy file first then delete it or create through upload
|
||||||
|
// Actually, according to API docs, directories are created when uploading files to them
|
||||||
|
// So we'll just invalidate to refresh the list
|
||||||
|
ref.invalidate(siteFilesProvider(siteId: arg.siteId, path: arg.path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final siteFilesNotifierProvider = AsyncNotifierProvider.autoDispose.family<
|
||||||
|
SiteFilesNotifier,
|
||||||
|
List<SnSiteFileEntry>,
|
||||||
|
({String siteId, String? path})
|
||||||
|
>(SiteFilesNotifier.new);
|
||||||
451
lib/pods/site_files.g.dart
Normal file
451
lib/pods/site_files.g.dart
Normal file
@@ -0,0 +1,451 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'site_files.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
String _$siteFilesHash() => r'd4029e6c160edcd454eb39ef1c19427b7f95a8d8';
|
||||||
|
|
||||||
|
/// Copied from Dart SDK
|
||||||
|
class _SystemHash {
|
||||||
|
_SystemHash._();
|
||||||
|
|
||||||
|
static int combine(int hash, int value) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + value);
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||||
|
return hash ^ (hash >> 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int finish(int hash) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = hash ^ (hash >> 11);
|
||||||
|
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [siteFiles].
|
||||||
|
@ProviderFor(siteFiles)
|
||||||
|
const siteFilesProvider = SiteFilesFamily();
|
||||||
|
|
||||||
|
/// See also [siteFiles].
|
||||||
|
class SiteFilesFamily extends Family<AsyncValue<List<SnSiteFileEntry>>> {
|
||||||
|
/// See also [siteFiles].
|
||||||
|
const SiteFilesFamily();
|
||||||
|
|
||||||
|
/// See also [siteFiles].
|
||||||
|
SiteFilesProvider call({required String siteId, String? path}) {
|
||||||
|
return SiteFilesProvider(siteId: siteId, path: path);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
SiteFilesProvider getProviderOverride(covariant SiteFilesProvider provider) {
|
||||||
|
return call(siteId: provider.siteId, path: provider.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||||
|
_allTransitiveDependencies;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get name => r'siteFilesProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [siteFiles].
|
||||||
|
class SiteFilesProvider
|
||||||
|
extends AutoDisposeFutureProvider<List<SnSiteFileEntry>> {
|
||||||
|
/// See also [siteFiles].
|
||||||
|
SiteFilesProvider({required String siteId, String? path})
|
||||||
|
: this._internal(
|
||||||
|
(ref) => siteFiles(ref as SiteFilesRef, siteId: siteId, path: path),
|
||||||
|
from: siteFilesProvider,
|
||||||
|
name: r'siteFilesProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$siteFilesHash,
|
||||||
|
dependencies: SiteFilesFamily._dependencies,
|
||||||
|
allTransitiveDependencies: SiteFilesFamily._allTransitiveDependencies,
|
||||||
|
siteId: siteId,
|
||||||
|
path: path,
|
||||||
|
);
|
||||||
|
|
||||||
|
SiteFilesProvider._internal(
|
||||||
|
super._createNotifier, {
|
||||||
|
required super.name,
|
||||||
|
required super.dependencies,
|
||||||
|
required super.allTransitiveDependencies,
|
||||||
|
required super.debugGetCreateSourceHash,
|
||||||
|
required super.from,
|
||||||
|
required this.siteId,
|
||||||
|
required this.path,
|
||||||
|
}) : super.internal();
|
||||||
|
|
||||||
|
final String siteId;
|
||||||
|
final String? path;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Override overrideWith(
|
||||||
|
FutureOr<List<SnSiteFileEntry>> Function(SiteFilesRef provider) create,
|
||||||
|
) {
|
||||||
|
return ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
override: SiteFilesProvider._internal(
|
||||||
|
(ref) => create(ref as SiteFilesRef),
|
||||||
|
from: from,
|
||||||
|
name: null,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
debugGetCreateSourceHash: null,
|
||||||
|
siteId: siteId,
|
||||||
|
path: path,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AutoDisposeFutureProviderElement<List<SnSiteFileEntry>> createElement() {
|
||||||
|
return _SiteFilesProviderElement(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is SiteFilesProvider &&
|
||||||
|
other.siteId == siteId &&
|
||||||
|
other.path == path;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, siteId.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, path.hashCode);
|
||||||
|
|
||||||
|
return _SystemHash.finish(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
mixin SiteFilesRef on AutoDisposeFutureProviderRef<List<SnSiteFileEntry>> {
|
||||||
|
/// The parameter `siteId` of this provider.
|
||||||
|
String get siteId;
|
||||||
|
|
||||||
|
/// The parameter `path` of this provider.
|
||||||
|
String? get path;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SiteFilesProviderElement
|
||||||
|
extends AutoDisposeFutureProviderElement<List<SnSiteFileEntry>>
|
||||||
|
with SiteFilesRef {
|
||||||
|
_SiteFilesProviderElement(super.provider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get siteId => (origin as SiteFilesProvider).siteId;
|
||||||
|
@override
|
||||||
|
String? get path => (origin as SiteFilesProvider).path;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$siteFileContentHash() => r'b594ad4f8c54555e742ece94ee001092cb2f83d1';
|
||||||
|
|
||||||
|
/// See also [siteFileContent].
|
||||||
|
@ProviderFor(siteFileContent)
|
||||||
|
const siteFileContentProvider = SiteFileContentFamily();
|
||||||
|
|
||||||
|
/// See also [siteFileContent].
|
||||||
|
class SiteFileContentFamily extends Family<AsyncValue<SnFileContent>> {
|
||||||
|
/// See also [siteFileContent].
|
||||||
|
const SiteFileContentFamily();
|
||||||
|
|
||||||
|
/// See also [siteFileContent].
|
||||||
|
SiteFileContentProvider call({
|
||||||
|
required String siteId,
|
||||||
|
required String relativePath,
|
||||||
|
}) {
|
||||||
|
return SiteFileContentProvider(siteId: siteId, relativePath: relativePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
SiteFileContentProvider getProviderOverride(
|
||||||
|
covariant SiteFileContentProvider provider,
|
||||||
|
) {
|
||||||
|
return call(siteId: provider.siteId, relativePath: provider.relativePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||||
|
_allTransitiveDependencies;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get name => r'siteFileContentProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [siteFileContent].
|
||||||
|
class SiteFileContentProvider extends AutoDisposeFutureProvider<SnFileContent> {
|
||||||
|
/// See also [siteFileContent].
|
||||||
|
SiteFileContentProvider({
|
||||||
|
required String siteId,
|
||||||
|
required String relativePath,
|
||||||
|
}) : this._internal(
|
||||||
|
(ref) => siteFileContent(
|
||||||
|
ref as SiteFileContentRef,
|
||||||
|
siteId: siteId,
|
||||||
|
relativePath: relativePath,
|
||||||
|
),
|
||||||
|
from: siteFileContentProvider,
|
||||||
|
name: r'siteFileContentProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$siteFileContentHash,
|
||||||
|
dependencies: SiteFileContentFamily._dependencies,
|
||||||
|
allTransitiveDependencies:
|
||||||
|
SiteFileContentFamily._allTransitiveDependencies,
|
||||||
|
siteId: siteId,
|
||||||
|
relativePath: relativePath,
|
||||||
|
);
|
||||||
|
|
||||||
|
SiteFileContentProvider._internal(
|
||||||
|
super._createNotifier, {
|
||||||
|
required super.name,
|
||||||
|
required super.dependencies,
|
||||||
|
required super.allTransitiveDependencies,
|
||||||
|
required super.debugGetCreateSourceHash,
|
||||||
|
required super.from,
|
||||||
|
required this.siteId,
|
||||||
|
required this.relativePath,
|
||||||
|
}) : super.internal();
|
||||||
|
|
||||||
|
final String siteId;
|
||||||
|
final String relativePath;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Override overrideWith(
|
||||||
|
FutureOr<SnFileContent> Function(SiteFileContentRef provider) create,
|
||||||
|
) {
|
||||||
|
return ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
override: SiteFileContentProvider._internal(
|
||||||
|
(ref) => create(ref as SiteFileContentRef),
|
||||||
|
from: from,
|
||||||
|
name: null,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
debugGetCreateSourceHash: null,
|
||||||
|
siteId: siteId,
|
||||||
|
relativePath: relativePath,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AutoDisposeFutureProviderElement<SnFileContent> createElement() {
|
||||||
|
return _SiteFileContentProviderElement(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is SiteFileContentProvider &&
|
||||||
|
other.siteId == siteId &&
|
||||||
|
other.relativePath == relativePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, siteId.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, relativePath.hashCode);
|
||||||
|
|
||||||
|
return _SystemHash.finish(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
mixin SiteFileContentRef on AutoDisposeFutureProviderRef<SnFileContent> {
|
||||||
|
/// The parameter `siteId` of this provider.
|
||||||
|
String get siteId;
|
||||||
|
|
||||||
|
/// The parameter `relativePath` of this provider.
|
||||||
|
String get relativePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SiteFileContentProviderElement
|
||||||
|
extends AutoDisposeFutureProviderElement<SnFileContent>
|
||||||
|
with SiteFileContentRef {
|
||||||
|
_SiteFileContentProviderElement(super.provider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get siteId => (origin as SiteFileContentProvider).siteId;
|
||||||
|
@override
|
||||||
|
String get relativePath => (origin as SiteFileContentProvider).relativePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$siteFileContentRawHash() =>
|
||||||
|
r'd0331c30698a9f4b90fe9b79273ff5914fa46616';
|
||||||
|
|
||||||
|
/// See also [siteFileContentRaw].
|
||||||
|
@ProviderFor(siteFileContentRaw)
|
||||||
|
const siteFileContentRawProvider = SiteFileContentRawFamily();
|
||||||
|
|
||||||
|
/// See also [siteFileContentRaw].
|
||||||
|
class SiteFileContentRawFamily extends Family<AsyncValue<String>> {
|
||||||
|
/// See also [siteFileContentRaw].
|
||||||
|
const SiteFileContentRawFamily();
|
||||||
|
|
||||||
|
/// See also [siteFileContentRaw].
|
||||||
|
SiteFileContentRawProvider call({
|
||||||
|
required String siteId,
|
||||||
|
required String relativePath,
|
||||||
|
}) {
|
||||||
|
return SiteFileContentRawProvider(
|
||||||
|
siteId: siteId,
|
||||||
|
relativePath: relativePath,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
SiteFileContentRawProvider getProviderOverride(
|
||||||
|
covariant SiteFileContentRawProvider provider,
|
||||||
|
) {
|
||||||
|
return call(siteId: provider.siteId, relativePath: provider.relativePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||||
|
_allTransitiveDependencies;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get name => r'siteFileContentRawProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [siteFileContentRaw].
|
||||||
|
class SiteFileContentRawProvider extends AutoDisposeFutureProvider<String> {
|
||||||
|
/// See also [siteFileContentRaw].
|
||||||
|
SiteFileContentRawProvider({
|
||||||
|
required String siteId,
|
||||||
|
required String relativePath,
|
||||||
|
}) : this._internal(
|
||||||
|
(ref) => siteFileContentRaw(
|
||||||
|
ref as SiteFileContentRawRef,
|
||||||
|
siteId: siteId,
|
||||||
|
relativePath: relativePath,
|
||||||
|
),
|
||||||
|
from: siteFileContentRawProvider,
|
||||||
|
name: r'siteFileContentRawProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$siteFileContentRawHash,
|
||||||
|
dependencies: SiteFileContentRawFamily._dependencies,
|
||||||
|
allTransitiveDependencies:
|
||||||
|
SiteFileContentRawFamily._allTransitiveDependencies,
|
||||||
|
siteId: siteId,
|
||||||
|
relativePath: relativePath,
|
||||||
|
);
|
||||||
|
|
||||||
|
SiteFileContentRawProvider._internal(
|
||||||
|
super._createNotifier, {
|
||||||
|
required super.name,
|
||||||
|
required super.dependencies,
|
||||||
|
required super.allTransitiveDependencies,
|
||||||
|
required super.debugGetCreateSourceHash,
|
||||||
|
required super.from,
|
||||||
|
required this.siteId,
|
||||||
|
required this.relativePath,
|
||||||
|
}) : super.internal();
|
||||||
|
|
||||||
|
final String siteId;
|
||||||
|
final String relativePath;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Override overrideWith(
|
||||||
|
FutureOr<String> Function(SiteFileContentRawRef provider) create,
|
||||||
|
) {
|
||||||
|
return ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
override: SiteFileContentRawProvider._internal(
|
||||||
|
(ref) => create(ref as SiteFileContentRawRef),
|
||||||
|
from: from,
|
||||||
|
name: null,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
debugGetCreateSourceHash: null,
|
||||||
|
siteId: siteId,
|
||||||
|
relativePath: relativePath,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AutoDisposeFutureProviderElement<String> createElement() {
|
||||||
|
return _SiteFileContentRawProviderElement(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is SiteFileContentRawProvider &&
|
||||||
|
other.siteId == siteId &&
|
||||||
|
other.relativePath == relativePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, siteId.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, relativePath.hashCode);
|
||||||
|
|
||||||
|
return _SystemHash.finish(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
mixin SiteFileContentRawRef on AutoDisposeFutureProviderRef<String> {
|
||||||
|
/// The parameter `siteId` of this provider.
|
||||||
|
String get siteId;
|
||||||
|
|
||||||
|
/// The parameter `relativePath` of this provider.
|
||||||
|
String get relativePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SiteFileContentRawProviderElement
|
||||||
|
extends AutoDisposeFutureProviderElement<String>
|
||||||
|
with SiteFileContentRawRef {
|
||||||
|
_SiteFileContentRawProviderElement(super.provider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get siteId => (origin as SiteFileContentRawProvider).siteId;
|
||||||
|
@override
|
||||||
|
String get relativePath =>
|
||||||
|
(origin as SiteFileContentRawProvider).relativePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
116
lib/pods/site_pages.dart
Normal file
116
lib/pods/site_pages.dart
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:island/models/publication_site.dart';
|
||||||
|
import 'package:island/pods/network.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
|
part 'site_pages.g.dart';
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
Future<List<SnPublicationPage>> sitePages(
|
||||||
|
Ref ref,
|
||||||
|
String pubName,
|
||||||
|
String siteSlug,
|
||||||
|
) async {
|
||||||
|
final apiClient = ref.watch(apiClientProvider);
|
||||||
|
final resp = await apiClient.get('/zone/sites/$pubName/$siteSlug/pages');
|
||||||
|
final data = resp.data as List<dynamic>;
|
||||||
|
return data.map((json) => SnPublicationPage.fromJson(json)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
Future<SnPublicationPage> sitePage(Ref ref, String pageId) async {
|
||||||
|
final apiClient = ref.watch(apiClientProvider);
|
||||||
|
final resp = await apiClient.get('/zone/sites/pages/$pageId');
|
||||||
|
return SnPublicationPage.fromJson(resp.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
class SitePagesNotifier
|
||||||
|
extends
|
||||||
|
AutoDisposeFamilyAsyncNotifier<
|
||||||
|
List<SnPublicationPage>,
|
||||||
|
({String pubName, String siteSlug})
|
||||||
|
> {
|
||||||
|
@override
|
||||||
|
Future<List<SnPublicationPage>> build(
|
||||||
|
({String pubName, String siteSlug}) arg,
|
||||||
|
) async {
|
||||||
|
return fetchPages();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<SnPublicationPage>> fetchPages() async {
|
||||||
|
try {
|
||||||
|
final apiClient = ref.read(apiClientProvider);
|
||||||
|
final resp = await apiClient.get(
|
||||||
|
'/zone/sites/${arg.pubName}/${arg.siteSlug}/pages',
|
||||||
|
);
|
||||||
|
final data = resp.data as List<dynamic>;
|
||||||
|
return data.map((json) => SnPublicationPage.fromJson(json)).toList();
|
||||||
|
} catch (e) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<SnPublicationPage?> createPage(Map<String, dynamic> pageData) async {
|
||||||
|
state = const AsyncValue.loading();
|
||||||
|
try {
|
||||||
|
final apiClient = ref.read(apiClientProvider);
|
||||||
|
final resp = await apiClient.post(
|
||||||
|
'/zone/sites/${arg.pubName}/${arg.siteSlug}/pages',
|
||||||
|
data: pageData,
|
||||||
|
);
|
||||||
|
final newPage = SnPublicationPage.fromJson(resp.data);
|
||||||
|
|
||||||
|
// Refresh the pages list
|
||||||
|
ref.invalidate(sitePagesProvider(arg.pubName, arg.siteSlug));
|
||||||
|
|
||||||
|
return newPage;
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
state = AsyncValue.error(error, stackTrace);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<SnPublicationPage?> updatePage(
|
||||||
|
String pageId,
|
||||||
|
Map<String, dynamic> pageData,
|
||||||
|
) async {
|
||||||
|
state = const AsyncValue.loading();
|
||||||
|
try {
|
||||||
|
final apiClient = ref.read(apiClientProvider);
|
||||||
|
final resp = await apiClient.patch(
|
||||||
|
'/zone/sites/pages/$pageId',
|
||||||
|
data: pageData,
|
||||||
|
);
|
||||||
|
final updatedPage = SnPublicationPage.fromJson(resp.data);
|
||||||
|
|
||||||
|
// Refresh the pages list
|
||||||
|
ref.invalidate(sitePagesProvider(arg.pubName, arg.siteSlug));
|
||||||
|
|
||||||
|
return updatedPage;
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
state = AsyncValue.error(error, stackTrace);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deletePage(String pageId) async {
|
||||||
|
state = const AsyncValue.loading();
|
||||||
|
try {
|
||||||
|
final apiClient = ref.read(apiClientProvider);
|
||||||
|
await apiClient.delete('/zone/sites/pages/$pageId');
|
||||||
|
|
||||||
|
// Refresh the pages list
|
||||||
|
ref.invalidate(sitePagesProvider(arg.pubName, arg.siteSlug));
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
state = AsyncValue.error(error, stackTrace);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final sitePagesNotifierProvider = AsyncNotifierProvider.autoDispose.family<
|
||||||
|
SitePagesNotifier,
|
||||||
|
List<SnPublicationPage>,
|
||||||
|
({String pubName, String siteSlug})
|
||||||
|
>(SitePagesNotifier.new);
|
||||||
280
lib/pods/site_pages.g.dart
Normal file
280
lib/pods/site_pages.g.dart
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'site_pages.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
String _$sitePagesHash() => r'5e084e9694ad665e9b238c6a747c6c6e99c5eb03';
|
||||||
|
|
||||||
|
/// Copied from Dart SDK
|
||||||
|
class _SystemHash {
|
||||||
|
_SystemHash._();
|
||||||
|
|
||||||
|
static int combine(int hash, int value) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + value);
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||||
|
return hash ^ (hash >> 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int finish(int hash) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = hash ^ (hash >> 11);
|
||||||
|
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [sitePages].
|
||||||
|
@ProviderFor(sitePages)
|
||||||
|
const sitePagesProvider = SitePagesFamily();
|
||||||
|
|
||||||
|
/// See also [sitePages].
|
||||||
|
class SitePagesFamily extends Family<AsyncValue<List<SnPublicationPage>>> {
|
||||||
|
/// See also [sitePages].
|
||||||
|
const SitePagesFamily();
|
||||||
|
|
||||||
|
/// See also [sitePages].
|
||||||
|
SitePagesProvider call(String pubName, String siteSlug) {
|
||||||
|
return SitePagesProvider(pubName, siteSlug);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
SitePagesProvider getProviderOverride(covariant SitePagesProvider provider) {
|
||||||
|
return call(provider.pubName, provider.siteSlug);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||||
|
_allTransitiveDependencies;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get name => r'sitePagesProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [sitePages].
|
||||||
|
class SitePagesProvider
|
||||||
|
extends AutoDisposeFutureProvider<List<SnPublicationPage>> {
|
||||||
|
/// See also [sitePages].
|
||||||
|
SitePagesProvider(String pubName, String siteSlug)
|
||||||
|
: this._internal(
|
||||||
|
(ref) => sitePages(ref as SitePagesRef, pubName, siteSlug),
|
||||||
|
from: sitePagesProvider,
|
||||||
|
name: r'sitePagesProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$sitePagesHash,
|
||||||
|
dependencies: SitePagesFamily._dependencies,
|
||||||
|
allTransitiveDependencies: SitePagesFamily._allTransitiveDependencies,
|
||||||
|
pubName: pubName,
|
||||||
|
siteSlug: siteSlug,
|
||||||
|
);
|
||||||
|
|
||||||
|
SitePagesProvider._internal(
|
||||||
|
super._createNotifier, {
|
||||||
|
required super.name,
|
||||||
|
required super.dependencies,
|
||||||
|
required super.allTransitiveDependencies,
|
||||||
|
required super.debugGetCreateSourceHash,
|
||||||
|
required super.from,
|
||||||
|
required this.pubName,
|
||||||
|
required this.siteSlug,
|
||||||
|
}) : super.internal();
|
||||||
|
|
||||||
|
final String pubName;
|
||||||
|
final String siteSlug;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Override overrideWith(
|
||||||
|
FutureOr<List<SnPublicationPage>> Function(SitePagesRef provider) create,
|
||||||
|
) {
|
||||||
|
return ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
override: SitePagesProvider._internal(
|
||||||
|
(ref) => create(ref as SitePagesRef),
|
||||||
|
from: from,
|
||||||
|
name: null,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
debugGetCreateSourceHash: null,
|
||||||
|
pubName: pubName,
|
||||||
|
siteSlug: siteSlug,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AutoDisposeFutureProviderElement<List<SnPublicationPage>> createElement() {
|
||||||
|
return _SitePagesProviderElement(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is SitePagesProvider &&
|
||||||
|
other.pubName == pubName &&
|
||||||
|
other.siteSlug == siteSlug;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, pubName.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, siteSlug.hashCode);
|
||||||
|
|
||||||
|
return _SystemHash.finish(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
mixin SitePagesRef on AutoDisposeFutureProviderRef<List<SnPublicationPage>> {
|
||||||
|
/// The parameter `pubName` of this provider.
|
||||||
|
String get pubName;
|
||||||
|
|
||||||
|
/// The parameter `siteSlug` of this provider.
|
||||||
|
String get siteSlug;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SitePagesProviderElement
|
||||||
|
extends AutoDisposeFutureProviderElement<List<SnPublicationPage>>
|
||||||
|
with SitePagesRef {
|
||||||
|
_SitePagesProviderElement(super.provider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pubName => (origin as SitePagesProvider).pubName;
|
||||||
|
@override
|
||||||
|
String get siteSlug => (origin as SitePagesProvider).siteSlug;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$sitePageHash() => r'542f70c5b103fe34d7cf7eb0821d52f017022efc';
|
||||||
|
|
||||||
|
/// See also [sitePage].
|
||||||
|
@ProviderFor(sitePage)
|
||||||
|
const sitePageProvider = SitePageFamily();
|
||||||
|
|
||||||
|
/// See also [sitePage].
|
||||||
|
class SitePageFamily extends Family<AsyncValue<SnPublicationPage>> {
|
||||||
|
/// See also [sitePage].
|
||||||
|
const SitePageFamily();
|
||||||
|
|
||||||
|
/// See also [sitePage].
|
||||||
|
SitePageProvider call(String pageId) {
|
||||||
|
return SitePageProvider(pageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
SitePageProvider getProviderOverride(covariant SitePageProvider provider) {
|
||||||
|
return call(provider.pageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||||
|
_allTransitiveDependencies;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get name => r'sitePageProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [sitePage].
|
||||||
|
class SitePageProvider extends AutoDisposeFutureProvider<SnPublicationPage> {
|
||||||
|
/// See also [sitePage].
|
||||||
|
SitePageProvider(String pageId)
|
||||||
|
: this._internal(
|
||||||
|
(ref) => sitePage(ref as SitePageRef, pageId),
|
||||||
|
from: sitePageProvider,
|
||||||
|
name: r'sitePageProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$sitePageHash,
|
||||||
|
dependencies: SitePageFamily._dependencies,
|
||||||
|
allTransitiveDependencies: SitePageFamily._allTransitiveDependencies,
|
||||||
|
pageId: pageId,
|
||||||
|
);
|
||||||
|
|
||||||
|
SitePageProvider._internal(
|
||||||
|
super._createNotifier, {
|
||||||
|
required super.name,
|
||||||
|
required super.dependencies,
|
||||||
|
required super.allTransitiveDependencies,
|
||||||
|
required super.debugGetCreateSourceHash,
|
||||||
|
required super.from,
|
||||||
|
required this.pageId,
|
||||||
|
}) : super.internal();
|
||||||
|
|
||||||
|
final String pageId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Override overrideWith(
|
||||||
|
FutureOr<SnPublicationPage> Function(SitePageRef provider) create,
|
||||||
|
) {
|
||||||
|
return ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
override: SitePageProvider._internal(
|
||||||
|
(ref) => create(ref as SitePageRef),
|
||||||
|
from: from,
|
||||||
|
name: null,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
debugGetCreateSourceHash: null,
|
||||||
|
pageId: pageId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AutoDisposeFutureProviderElement<SnPublicationPage> createElement() {
|
||||||
|
return _SitePageProviderElement(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is SitePageProvider && other.pageId == pageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, pageId.hashCode);
|
||||||
|
|
||||||
|
return _SystemHash.finish(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
mixin SitePageRef on AutoDisposeFutureProviderRef<SnPublicationPage> {
|
||||||
|
/// The parameter `pageId` of this provider.
|
||||||
|
String get pageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SitePageProviderElement
|
||||||
|
extends AutoDisposeFutureProviderElement<SnPublicationPage>
|
||||||
|
with SitePageRef {
|
||||||
|
_SitePageProviderElement(super.provider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pageId => (origin as SitePageProvider).pageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
88
lib/pods/sites.dart
Normal file
88
lib/pods/sites.dart
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:island/models/publication_site.dart';
|
||||||
|
import 'package:island/pods/network.dart';
|
||||||
|
|
||||||
|
class SiteNotifier
|
||||||
|
extends
|
||||||
|
AutoDisposeFamilyAsyncNotifier<
|
||||||
|
SnPublicationSite,
|
||||||
|
({String pubName, String? siteId})
|
||||||
|
> {
|
||||||
|
@override
|
||||||
|
FutureOr<SnPublicationSite> build(
|
||||||
|
({String pubName, String? siteId}) arg,
|
||||||
|
) async {
|
||||||
|
if (arg.siteId == null || arg.siteId!.isEmpty) {
|
||||||
|
return SnPublicationSite(
|
||||||
|
id: '',
|
||||||
|
slug: '',
|
||||||
|
name: '',
|
||||||
|
publisherId: arg.pubName,
|
||||||
|
accountId: '',
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
pages: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final client = ref.read(apiClientProvider);
|
||||||
|
final response = await client.get('/sphere/sites/${arg.siteId}');
|
||||||
|
return SnPublicationSite.fromJson(response.data);
|
||||||
|
} catch (e) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> saveSite(SnPublicationSite site) async {
|
||||||
|
state = const AsyncValue.loading();
|
||||||
|
try {
|
||||||
|
final client = ref.read(apiClientProvider);
|
||||||
|
final url = '/sphere/sites';
|
||||||
|
|
||||||
|
final response =
|
||||||
|
site.id.isEmpty
|
||||||
|
? await client.post(url, data: site.toJson())
|
||||||
|
: await client.patch('$url/${site.id}', data: site.toJson());
|
||||||
|
|
||||||
|
state = AsyncValue.data(SnPublicationSite.fromJson(response.data));
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
state = AsyncValue.error(error, stackTrace);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteSite() async {
|
||||||
|
final siteId = arg.siteId;
|
||||||
|
if (siteId == null || siteId.isEmpty) return;
|
||||||
|
|
||||||
|
state = const AsyncValue.loading();
|
||||||
|
try {
|
||||||
|
final client = ref.read(apiClientProvider);
|
||||||
|
await client.delete('/sphere/sites/$siteId');
|
||||||
|
state = AsyncValue.data(
|
||||||
|
SnPublicationSite(
|
||||||
|
id: '',
|
||||||
|
slug: '',
|
||||||
|
name: '',
|
||||||
|
publisherId: arg.pubName,
|
||||||
|
accountId: '',
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
pages: [],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
state = AsyncValue.error(error, stackTrace);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final siteNotifierProvider = AsyncNotifierProvider.autoDispose.family<
|
||||||
|
SiteNotifier,
|
||||||
|
SnPublicationSite,
|
||||||
|
({String pubName, String? siteId})
|
||||||
|
>(SiteNotifier.new);
|
||||||
@@ -258,6 +258,24 @@ class UploadTasksNotifier extends StateNotifier<List<DriveTask>> {
|
|||||||
}).toList();
|
}).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void updateDownloadProgress(
|
||||||
|
String taskId,
|
||||||
|
int downloadedBytes,
|
||||||
|
int totalBytes,
|
||||||
|
) {
|
||||||
|
state =
|
||||||
|
state.map((task) {
|
||||||
|
if (task.taskId == taskId) {
|
||||||
|
return task.copyWith(
|
||||||
|
fileSize: totalBytes,
|
||||||
|
uploadedBytes: downloadedBytes,
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return task;
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
void removeTask(String taskId) {
|
void removeTask(String taskId) {
|
||||||
state = state.where((task) => task.taskId != taskId).toList();
|
state = state.where((task) => task.taskId != taskId).toList();
|
||||||
}
|
}
|
||||||
@@ -275,6 +293,10 @@ class UploadTasksNotifier extends StateNotifier<List<DriveTask>> {
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void clearAllTasks() {
|
||||||
|
state = [];
|
||||||
|
}
|
||||||
|
|
||||||
DriveTask? getTask(String taskId) {
|
DriveTask? getTask(String taskId) {
|
||||||
return state.where((task) => task.taskId == taskId).firstOrNull;
|
return state.where((task) => task.taskId == taskId).firstOrNull;
|
||||||
}
|
}
|
||||||
@@ -291,6 +313,27 @@ class UploadTasksNotifier extends StateNotifier<List<DriveTask>> {
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String addLocalDownloadTask(SnCloudFile item) {
|
||||||
|
final taskId =
|
||||||
|
'download-${item.id}-${DateTime.now().millisecondsSinceEpoch}';
|
||||||
|
final task = DriveTask(
|
||||||
|
id: taskId,
|
||||||
|
taskId: taskId,
|
||||||
|
fileName: item.name,
|
||||||
|
contentType: item.mimeType ?? '',
|
||||||
|
fileSize: 0,
|
||||||
|
uploadedBytes: 0,
|
||||||
|
totalChunks: 1,
|
||||||
|
uploadedChunks: 0,
|
||||||
|
status: DriveTaskStatus.inProgress,
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
type: 'FileDownload',
|
||||||
|
);
|
||||||
|
state = [...state, task];
|
||||||
|
return taskId;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_websocketSubscription?.cancel();
|
_websocketSubscription?.cancel();
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import 'package:dio/dio.dart';
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:firebase_analytics/firebase_analytics.dart';
|
import 'package:firebase_analytics/firebase_analytics.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_platform_alert/flutter_platform_alert.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:island/widgets/alert.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/account.dart';
|
import 'package:island/models/account.dart';
|
||||||
@@ -36,9 +37,12 @@ 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(
|
||||||
|
title: Text('failedToLoadUserInfo'.tr()),
|
||||||
|
content: Text(
|
||||||
|
[
|
||||||
(error.response?.statusCode == 401
|
(error.response?.statusCode == 401
|
||||||
? 'failedToLoadUserInfoUnauthorized'
|
? 'failedToLoadUserInfoUnauthorized'
|
||||||
: 'failedToLoadUserInfoNetwork')
|
: 'failedToLoadUserInfoNetwork')
|
||||||
@@ -46,31 +50,52 @@ class UserInfoNotifier extends StateNotifier<AsyncValue<SnAccount?>> {
|
|||||||
.trim(),
|
.trim(),
|
||||||
'',
|
'',
|
||||||
'${error.response?.statusCode ?? 'Network Error'}',
|
'${error.response?.statusCode ?? 'Network Error'}',
|
||||||
if (error.response?.headers != null) error.response?.headers,
|
if (error.response?.headers != null)
|
||||||
|
error.response?.headers,
|
||||||
if (error.response?.data != null)
|
if (error.response?.data != null)
|
||||||
jsonEncode(error.response?.data),
|
jsonEncode(error.response?.data),
|
||||||
].join('\n'),
|
].join('\n'),
|
||||||
iconStyle: IconStyle.error,
|
),
|
||||||
neutralButtonTitle: 'retry'.tr(),
|
actions: [
|
||||||
negativeButtonTitle: 'okay'.tr(),
|
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()),
|
||||||
|
content: Text(
|
||||||
[
|
[
|
||||||
'failedToLoadUserInfoNetwork'.tr(),
|
'failedToLoadUserInfoNetwork'.tr(),
|
||||||
error.toString(),
|
error.toString(),
|
||||||
].join('\n\n').trim(),
|
].join('\n\n').trim(),
|
||||||
iconStyle: IconStyle.error,
|
),
|
||||||
neutralButtonTitle: 'retry'.tr(),
|
actions: [
|
||||||
negativeButtonTitle: 'okay'.tr(),
|
TextButton(
|
||||||
|
onPressed: () => close(false),
|
||||||
|
child: Text('okay'.tr()),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => close(true),
|
||||||
|
child: Text('retry'.tr()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
).then((value) {
|
).then((value) {
|
||||||
if (value == CustomButton.neutralButton) {
|
if (value == true) {
|
||||||
fetchUser();
|
fetchUser();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ import 'package:island/screens/account/me/account_settings.dart';
|
|||||||
import 'package:island/screens/chat/chat.dart';
|
import 'package:island/screens/chat/chat.dart';
|
||||||
import 'package:island/screens/chat/room.dart';
|
import 'package:island/screens/chat/room.dart';
|
||||||
import 'package:island/screens/chat/room_detail.dart';
|
import 'package:island/screens/chat/room_detail.dart';
|
||||||
import 'package:island/screens/chat/call.dart';
|
|
||||||
import 'package:island/screens/chat/search_messages.dart';
|
import 'package:island/screens/chat/search_messages.dart';
|
||||||
import 'package:island/screens/thought/think.dart';
|
import 'package:island/screens/thought/think.dart';
|
||||||
import 'package:island/screens/creators/hub.dart';
|
import 'package:island/screens/creators/hub.dart';
|
||||||
@@ -43,6 +42,8 @@ import 'package:island/screens/stickers/pack_detail.dart';
|
|||||||
import 'package:island/screens/discovery/feeds/feed_marketplace.dart';
|
import 'package:island/screens/discovery/feeds/feed_marketplace.dart';
|
||||||
import 'package:island/screens/discovery/feeds/feed_detail.dart';
|
import 'package:island/screens/discovery/feeds/feed_detail.dart';
|
||||||
import 'package:island/screens/creators/poll/poll_list.dart';
|
import 'package:island/screens/creators/poll/poll_list.dart';
|
||||||
|
import 'package:island/screens/creators/sites/site_detail.dart';
|
||||||
|
import 'package:island/screens/creators/sites/site_list.dart';
|
||||||
import 'package:island/screens/creators/webfeed/webfeed_list.dart';
|
import 'package:island/screens/creators/webfeed/webfeed_list.dart';
|
||||||
import 'package:island/screens/posts/compose.dart';
|
import 'package:island/screens/posts/compose.dart';
|
||||||
import 'package:island/screens/posts/compose_article.dart';
|
import 'package:island/screens/posts/compose_article.dart';
|
||||||
@@ -117,14 +118,6 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
return ArticleEditScreen(id: id);
|
return ArticleEditScreen(id: id);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
GoRoute(
|
|
||||||
name: 'chatCall',
|
|
||||||
path: '/chat/:id/call',
|
|
||||||
builder: (context, state) {
|
|
||||||
final id = state.pathParameters['id']!;
|
|
||||||
return CallScreen(roomId: id);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
GoRoute(
|
GoRoute(
|
||||||
name: 'logs',
|
name: 'logs',
|
||||||
path: '/logs',
|
path: '/logs',
|
||||||
@@ -170,6 +163,22 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
builder: (context, state) => const AboutScreen(),
|
builder: (context, state) => const AboutScreen(),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
GoRoute(
|
||||||
|
name: 'fileDetail',
|
||||||
|
path: '/files/:id',
|
||||||
|
builder: (context, state) {
|
||||||
|
// For now, we'll need to pass the file object through extra
|
||||||
|
// This will be updated when we modify the file list navigation
|
||||||
|
final file = state.extra as SnCloudFile?;
|
||||||
|
if (file != null) {
|
||||||
|
return FileDetailScreen(item: file);
|
||||||
|
}
|
||||||
|
// Fallback - this shouldn't happen in normal flow
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
// Main tabs with TabsScreen shell
|
// Main tabs with TabsScreen shell
|
||||||
ShellRoute(
|
ShellRoute(
|
||||||
navigatorKey: _tabsShellKey,
|
navigatorKey: _tabsShellKey,
|
||||||
@@ -427,23 +436,6 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
name: 'files',
|
name: 'files',
|
||||||
path: '/files',
|
path: '/files',
|
||||||
builder: (context, state) => const FileListScreen(),
|
builder: (context, state) => const FileListScreen(),
|
||||||
routes: [
|
|
||||||
GoRoute(
|
|
||||||
name: 'fileDetail',
|
|
||||||
path: ':id',
|
|
||||||
builder: (context, state) {
|
|
||||||
// For now, we'll need to pass the file object through extra
|
|
||||||
// This will be updated when we modify the file list navigation
|
|
||||||
final file = state.extra as SnCloudFile?;
|
|
||||||
if (file != null) {
|
|
||||||
return FileDetailScreen(item: file);
|
|
||||||
}
|
|
||||||
// Fallback - this shouldn't happen in normal flow
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
|
|
||||||
// SN-chan tab
|
// SN-chan tab
|
||||||
@@ -485,6 +477,29 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
return CreatorPollListScreen(pubName: name);
|
return CreatorPollListScreen(pubName: name);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
// Site list route
|
||||||
|
GoRoute(
|
||||||
|
name: 'creatorSites',
|
||||||
|
path: ':name/sites',
|
||||||
|
builder: (context, state) {
|
||||||
|
final name = state.pathParameters['name']!;
|
||||||
|
return CreatorSiteListScreen(pubName: name);
|
||||||
|
},
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
name: 'creatorSiteDetail',
|
||||||
|
path: ':siteSlug',
|
||||||
|
builder: (context, state) {
|
||||||
|
final name = state.pathParameters['name']!;
|
||||||
|
final siteSlug = state.pathParameters['siteSlug']!;
|
||||||
|
return PublicationSiteDetailScreen(
|
||||||
|
siteSlug: siteSlug,
|
||||||
|
pubName: name,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
GoRoute(
|
GoRoute(
|
||||||
name: 'creatorStickers',
|
name: 'creatorStickers',
|
||||||
|
|||||||
@@ -384,9 +384,7 @@ class _AboutScreenState extends ConsumerState<AboutScreen> {
|
|||||||
icon: const Icon(Symbols.content_copy, size: 16),
|
icon: const Icon(Symbols.content_copy, size: 16),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Clipboard.setData(ClipboardData(text: value));
|
Clipboard.setData(ClipboardData(text: value));
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
showSnackBar('copiedToClipboard'.tr());
|
||||||
SnackBar(content: Text('copiedToClipboard'.tr())),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
constraints: const BoxConstraints(),
|
constraints: const BoxConstraints(),
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ class CaptchaScreen extends ConsumerWidget {
|
|||||||
return showModalBottomSheet<String>(
|
return showModalBottomSheet<String>(
|
||||||
context: context,
|
context: context,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
|
isDismissible: false,
|
||||||
builder: (context) => const CaptchaScreen(),
|
builder: (context) => const CaptchaScreen(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ class CaptchaScreen extends ConsumerStatefulWidget {
|
|||||||
static Future<String?> show(BuildContext context) {
|
static Future<String?> show(BuildContext context) {
|
||||||
return showModalBottomSheet<String>(
|
return showModalBottomSheet<String>(
|
||||||
context: context,
|
context: context,
|
||||||
|
isDismissible: false,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
builder: (context) => const CaptchaScreen(),
|
builder: (context) => const CaptchaScreen(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 client = ref.watch(apiClientProvider);
|
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 resp = await client.get('/sphere/chat');
|
||||||
return resp.data
|
final remoteRooms =
|
||||||
|
resp.data
|
||||||
.map((e) => SnChatRoom.fromJson(e))
|
.map((e) => SnChatRoom.fromJson(e))
|
||||||
.cast<SnChatRoom>()
|
.cast<SnChatRoom>()
|
||||||
.toList();
|
.toList();
|
||||||
|
await db.saveChatRooms(remoteRooms);
|
||||||
|
ref.invalidateSelf();
|
||||||
|
} catch (_) {}
|
||||||
|
}).ignore();
|
||||||
|
|
||||||
|
return localRooms;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
// Fallback to API
|
||||||
|
final client = ref.watch(apiClientProvider);
|
||||||
|
final resp = await client.get('/sphere/chat');
|
||||||
|
final rooms =
|
||||||
|
resp.data.map((e) => SnChatRoom.fromJson(e)).cast<SnChatRoom>().toList();
|
||||||
|
await db.saveChatRooms(rooms);
|
||||||
|
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)
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import "package:island/widgets/chat/chat_input.dart";
|
|||||||
import "package:island/widgets/chat/chat_link_attachments.dart";
|
import "package:island/widgets/chat/chat_link_attachments.dart";
|
||||||
import "package:island/widgets/chat/public_room_preview.dart";
|
import "package:island/widgets/chat/public_room_preview.dart";
|
||||||
import "package:island/screens/thought/think_sheet.dart";
|
import "package:island/screens/thought/think_sheet.dart";
|
||||||
|
import "package:island/screens/chat/widgets/message_item_wrapper.dart";
|
||||||
|
|
||||||
class ChatRoomScreen extends HookConsumerWidget {
|
class ChatRoomScreen extends HookConsumerWidget {
|
||||||
final String id;
|
final String id;
|
||||||
@@ -148,9 +149,6 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
final inputKey = useMemoized(() => GlobalKey());
|
final inputKey = useMemoized(() => GlobalKey());
|
||||||
final inputHeight = useState<double>(80.0);
|
final inputHeight = useState<double>(80.0);
|
||||||
|
|
||||||
// Track previous height for smooth animations
|
|
||||||
final previousInputHeight = usePrevious<double>(inputHeight.value);
|
|
||||||
|
|
||||||
// Periodic height measurement for dynamic sizing
|
// Periodic height measurement for dynamic sizing
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
final timer = Timer.periodic(const Duration(milliseconds: 50), (_) {
|
final timer = Timer.periodic(const Duration(milliseconds: 50), (_) {
|
||||||
@@ -181,6 +179,38 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
final isSelectionMode = useState<bool>(false);
|
final isSelectionMode = useState<bool>(false);
|
||||||
final selectedMessages = useState<Set<String>>({});
|
final selectedMessages = useState<Set<String>>({});
|
||||||
|
|
||||||
|
final roomOpenTime = useMemoized(() => DateTime.now());
|
||||||
|
|
||||||
|
final onMessageAction = useCallback(
|
||||||
|
(String action, LocalChatMessage message) {
|
||||||
|
switch (action) {
|
||||||
|
case MessageItemAction.delete:
|
||||||
|
messagesNotifier.deleteMessage(message.id);
|
||||||
|
case MessageItemAction.edit:
|
||||||
|
messageEditingTo.value = message.toRemoteMessage();
|
||||||
|
messageController.text = messageEditingTo.value?.content ?? '';
|
||||||
|
attachments.value =
|
||||||
|
messageEditingTo.value!.attachments
|
||||||
|
.map((e) => UniversalFile.fromAttachment(e))
|
||||||
|
.toList();
|
||||||
|
case MessageItemAction.forward:
|
||||||
|
messageForwardingTo.value = message.toRemoteMessage();
|
||||||
|
case MessageItemAction.reply:
|
||||||
|
messageReplyingTo.value = message.toRemoteMessage();
|
||||||
|
case MessageItemAction.resend:
|
||||||
|
messagesNotifier.retryMessage(message.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
messagesNotifier,
|
||||||
|
messageEditingTo,
|
||||||
|
messageController,
|
||||||
|
attachments,
|
||||||
|
messageForwardingTo,
|
||||||
|
messageReplyingTo,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
var isLoading = false;
|
var isLoading = false;
|
||||||
var isScrollingToMessage = false; // Flag to prevent scroll conflicts
|
var isScrollingToMessage = false; // Flag to prevent scroll conflicts
|
||||||
|
|
||||||
@@ -624,239 +654,16 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget chatMessageListWidget(List<LocalChatMessage> messageList) =>
|
Widget chatMessageListWidget(
|
||||||
previousInputHeight != null && previousInputHeight != inputHeight.value
|
List<LocalChatMessage> messageList,
|
||||||
? TweenAnimationBuilder<double>(
|
) => AnimatedPadding(
|
||||||
tween: Tween<double>(
|
|
||||||
begin: previousInputHeight,
|
|
||||||
end: inputHeight.value,
|
|
||||||
),
|
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
curve: Curves.easeOut,
|
curve: Curves.easeOut,
|
||||||
builder:
|
|
||||||
(context, height, child) => SuperListView.builder(
|
|
||||||
listController: listController,
|
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
top: 16,
|
bottom: MediaQuery.of(context).padding.bottom + 8 + inputHeight.value,
|
||||||
bottom:
|
|
||||||
MediaQuery.of(context).padding.bottom + 8 + height,
|
|
||||||
),
|
),
|
||||||
controller: scrollController,
|
child: SuperListView.builder(
|
||||||
reverse: true, // Show newest messages at the bottom
|
|
||||||
itemCount: messageList.length,
|
|
||||||
findChildIndexCallback: (key) {
|
|
||||||
if (key is! ValueKey<String>) return null;
|
|
||||||
final messageId = key.value.substring(
|
|
||||||
messageKeyPrefix.length,
|
|
||||||
);
|
|
||||||
final index = messageList.indexWhere(
|
|
||||||
(m) => (m.nonce ?? m.id) == messageId,
|
|
||||||
);
|
|
||||||
// Return null for invalid indices to let SuperListView handle it properly
|
|
||||||
return index >= 0 ? index : null;
|
|
||||||
},
|
|
||||||
extentEstimation: (_, _) => 40,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final message = messageList[index];
|
|
||||||
final nextMessage =
|
|
||||||
index < messageList.length - 1
|
|
||||||
? messageList[index + 1]
|
|
||||||
: null;
|
|
||||||
final isLastInGroup =
|
|
||||||
nextMessage == null ||
|
|
||||||
nextMessage.senderId != message.senderId ||
|
|
||||||
nextMessage.createdAt
|
|
||||||
.difference(message.createdAt)
|
|
||||||
.inMinutes
|
|
||||||
.abs() >
|
|
||||||
3;
|
|
||||||
|
|
||||||
// Use a stable animation key that doesn't change during message lifecycle
|
|
||||||
final key = Key(
|
|
||||||
'$messageKeyPrefix${message.nonce ?? message.id}',
|
|
||||||
);
|
|
||||||
|
|
||||||
final messageWidget = chatIdentity.when(
|
|
||||||
skipError: true,
|
|
||||||
data:
|
|
||||||
(identity) => GestureDetector(
|
|
||||||
onLongPress: () {
|
|
||||||
if (!isSelectionMode.value) {
|
|
||||||
toggleSelectionMode();
|
|
||||||
toggleMessageSelection(message.id);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onTap: () {
|
|
||||||
if (isSelectionMode.value) {
|
|
||||||
toggleMessageSelection(message.id);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Container(
|
|
||||||
color:
|
|
||||||
selectedMessages.value.contains(message.id)
|
|
||||||
? Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.primaryContainer
|
|
||||||
.withOpacity(0.3)
|
|
||||||
: null,
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
MessageItem(
|
|
||||||
key:
|
|
||||||
settings.disableAnimation
|
|
||||||
? key
|
|
||||||
: null,
|
|
||||||
message: message,
|
|
||||||
isCurrentUser:
|
|
||||||
identity?.id == message.senderId,
|
|
||||||
onAction:
|
|
||||||
isSelectionMode.value
|
|
||||||
? null
|
|
||||||
: (action) {
|
|
||||||
switch (action) {
|
|
||||||
case MessageItemAction.delete:
|
|
||||||
messagesNotifier
|
|
||||||
.deleteMessage(
|
|
||||||
message.id,
|
|
||||||
);
|
|
||||||
case MessageItemAction.edit:
|
|
||||||
messageEditingTo.value =
|
|
||||||
message
|
|
||||||
.toRemoteMessage();
|
|
||||||
messageController.text =
|
|
||||||
messageEditingTo
|
|
||||||
.value
|
|
||||||
?.content ??
|
|
||||||
'';
|
|
||||||
attachments.value =
|
|
||||||
messageEditingTo
|
|
||||||
.value!
|
|
||||||
.attachments
|
|
||||||
.map(
|
|
||||||
(e) =>
|
|
||||||
UniversalFile.fromAttachment(
|
|
||||||
e,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
case MessageItemAction
|
|
||||||
.forward:
|
|
||||||
messageForwardingTo.value =
|
|
||||||
message
|
|
||||||
.toRemoteMessage();
|
|
||||||
case MessageItemAction.reply:
|
|
||||||
messageReplyingTo.value =
|
|
||||||
message
|
|
||||||
.toRemoteMessage();
|
|
||||||
case MessageItemAction.resend:
|
|
||||||
messagesNotifier
|
|
||||||
.retryMessage(
|
|
||||||
message.id,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onJump: (messageId) {
|
|
||||||
scrollToMessage(
|
|
||||||
messageId: messageId,
|
|
||||||
messageList: messageList,
|
|
||||||
messagesNotifier: messagesNotifier,
|
|
||||||
listController: listController,
|
listController: listController,
|
||||||
scrollController: scrollController,
|
|
||||||
ref: ref,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
progress:
|
|
||||||
attachmentProgress.value[message.id],
|
|
||||||
showAvatar: isLastInGroup,
|
|
||||||
isSelectionMode: isSelectionMode.value,
|
|
||||||
isSelected: selectedMessages.value
|
|
||||||
.contains(message.id),
|
|
||||||
onToggleSelection: toggleMessageSelection,
|
|
||||||
onEnterSelectionMode: () {
|
|
||||||
if (!isSelectionMode.value) {
|
|
||||||
toggleSelectionMode();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (selectedMessages.value.contains(
|
|
||||||
message.id,
|
|
||||||
))
|
|
||||||
...([
|
|
||||||
Positioned(
|
|
||||||
top: 8,
|
|
||||||
right: 8,
|
|
||||||
child: Container(
|
|
||||||
width: 16,
|
|
||||||
height: 16,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color:
|
|
||||||
Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.primary,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
child: Icon(
|
|
||||||
Icons.check,
|
|
||||||
size: 12,
|
|
||||||
color:
|
|
||||||
Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.onPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
loading:
|
|
||||||
() => MessageItem(
|
|
||||||
message: message,
|
|
||||||
isCurrentUser: false,
|
|
||||||
onAction: null,
|
|
||||||
progress: null,
|
|
||||||
showAvatar: false,
|
|
||||||
onJump: (_) {},
|
|
||||||
),
|
|
||||||
error: (_, _) => const SizedBox.shrink(),
|
|
||||||
);
|
|
||||||
|
|
||||||
return settings.disableAnimation
|
|
||||||
? messageWidget
|
|
||||||
: TweenAnimationBuilder<double>(
|
|
||||||
key: key,
|
|
||||||
tween: Tween<double>(begin: 0.0, end: 1.0),
|
|
||||||
duration: Duration(
|
|
||||||
milliseconds: 400 + (index % 5) * 50,
|
|
||||||
), // Staggered delay
|
|
||||||
curve: Curves.easeOutCubic,
|
|
||||||
builder: (context, animationValue, child) {
|
|
||||||
return Transform.translate(
|
|
||||||
offset: Offset(
|
|
||||||
0,
|
|
||||||
20 * (1 - animationValue),
|
|
||||||
), // Slide up from bottom
|
|
||||||
child: Opacity(
|
|
||||||
opacity: animationValue,
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: messageWidget,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: SuperListView.builder(
|
|
||||||
listController: listController,
|
|
||||||
padding: EdgeInsets.only(
|
|
||||||
top: 16,
|
|
||||||
bottom:
|
|
||||||
MediaQuery.of(context).padding.bottom +
|
|
||||||
8 +
|
|
||||||
inputHeight.value,
|
|
||||||
),
|
|
||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
reverse: true, // Show newest messages at the bottom
|
reverse: true, // Show newest messages at the bottom
|
||||||
itemCount: messageList.length,
|
itemCount: messageList.length,
|
||||||
@@ -866,16 +673,13 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
final index = messageList.indexWhere(
|
final index = messageList.indexWhere(
|
||||||
(m) => (m.nonce ?? m.id) == messageId,
|
(m) => (m.nonce ?? m.id) == messageId,
|
||||||
);
|
);
|
||||||
// Return null for invalid indices to let SuperListView handle it properly
|
|
||||||
return index >= 0 ? index : null;
|
return index >= 0 ? index : null;
|
||||||
},
|
},
|
||||||
extentEstimation: (_, _) => 40,
|
extentEstimation: (_, _) => 40,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final message = messageList[index];
|
final message = messageList[index];
|
||||||
final nextMessage =
|
final nextMessage =
|
||||||
index < messageList.length - 1
|
index < messageList.length - 1 ? messageList[index + 1] : null;
|
||||||
? messageList[index + 1]
|
|
||||||
: null;
|
|
||||||
final isLastInGroup =
|
final isLastInGroup =
|
||||||
nextMessage == null ||
|
nextMessage == null ||
|
||||||
nextMessage.senderId != message.senderId ||
|
nextMessage.senderId != message.senderId ||
|
||||||
@@ -885,166 +689,34 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
.abs() >
|
.abs() >
|
||||||
3;
|
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:
|
|
||||||
(identity) => GestureDetector(
|
|
||||||
onLongPress: () {
|
|
||||||
if (!isSelectionMode.value) {
|
|
||||||
toggleSelectionMode();
|
|
||||||
toggleMessageSelection(message.id);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onTap: () {
|
|
||||||
if (isSelectionMode.value) {
|
|
||||||
toggleMessageSelection(message.id);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Container(
|
|
||||||
color:
|
|
||||||
selectedMessages.value.contains(message.id)
|
|
||||||
? Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.primaryContainer
|
|
||||||
.withOpacity(0.3)
|
|
||||||
: null,
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
MessageItem(
|
|
||||||
key: settings.disableAnimation ? key : null,
|
|
||||||
message: message,
|
message: message,
|
||||||
isCurrentUser: identity?.id == message.senderId,
|
index: index,
|
||||||
onAction:
|
isLastInGroup: isLastInGroup,
|
||||||
isSelectionMode.value
|
isSelectionMode: isSelectionMode.value,
|
||||||
? null
|
selectedMessages: selectedMessages.value,
|
||||||
: (action) {
|
chatIdentity: chatIdentity,
|
||||||
switch (action) {
|
toggleSelectionMode: toggleSelectionMode,
|
||||||
case MessageItemAction.delete:
|
toggleMessageSelection: toggleMessageSelection,
|
||||||
messagesNotifier.deleteMessage(
|
onMessageAction: onMessageAction,
|
||||||
message.id,
|
onJump:
|
||||||
);
|
(messageId) => scrollToMessage(
|
||||||
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,
|
messageId: messageId,
|
||||||
messageList: messageList,
|
messageList: messageList,
|
||||||
messagesNotifier: messagesNotifier,
|
messagesNotifier: messagesNotifier,
|
||||||
listController: listController,
|
listController: listController,
|
||||||
scrollController: scrollController,
|
scrollController: scrollController,
|
||||||
ref: ref,
|
ref: ref,
|
||||||
|
),
|
||||||
|
attachmentProgress: attachmentProgress.value,
|
||||||
|
disableAnimation: settings.disableAnimation,
|
||||||
|
roomOpenTime: roomOpenTime,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
progress: attachmentProgress.value[message.id],
|
|
||||||
showAvatar: isLastInGroup,
|
|
||||||
isSelectionMode: isSelectionMode.value,
|
|
||||||
isSelected: selectedMessages.value.contains(
|
|
||||||
message.id,
|
|
||||||
),
|
),
|
||||||
onToggleSelection: toggleMessageSelection,
|
|
||||||
onEnterSelectionMode: () {
|
|
||||||
if (!isSelectionMode.value) {
|
|
||||||
toggleSelectionMode();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (selectedMessages.value.contains(message.id))
|
|
||||||
...([
|
|
||||||
Positioned(
|
|
||||||
top: 8,
|
|
||||||
right: 8,
|
|
||||||
child: Container(
|
|
||||||
width: 16,
|
|
||||||
height: 16,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color:
|
|
||||||
Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.primary,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
child: Icon(
|
|
||||||
Icons.check,
|
|
||||||
size: 12,
|
|
||||||
color:
|
|
||||||
Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.onPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
loading:
|
|
||||||
() => MessageItem(
|
|
||||||
message: message,
|
|
||||||
isCurrentUser: false,
|
|
||||||
onAction: null,
|
|
||||||
progress: null,
|
|
||||||
showAvatar: false,
|
|
||||||
onJump: (_) {},
|
|
||||||
),
|
|
||||||
error: (_, _) => const SizedBox.shrink(),
|
|
||||||
);
|
|
||||||
|
|
||||||
return settings.disableAnimation
|
|
||||||
? messageWidget
|
|
||||||
: TweenAnimationBuilder<double>(
|
|
||||||
key: key,
|
|
||||||
tween: Tween<double>(begin: 0.0, end: 1.0),
|
|
||||||
duration: Duration(
|
|
||||||
milliseconds: 400 + (index % 5) * 50,
|
|
||||||
), // Staggered delay
|
|
||||||
curve: Curves.easeOutCubic,
|
|
||||||
builder: (context, animationValue, child) {
|
|
||||||
return Transform.translate(
|
|
||||||
offset: Offset(
|
|
||||||
0,
|
|
||||||
20 * (1 - animationValue),
|
|
||||||
), // Slide up from bottom
|
|
||||||
child: Opacity(opacity: animationValue, child: child),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: messageWidget,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
@@ -1066,7 +738,11 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
AudioCallButton(roomId: id),
|
chatRoom.when(
|
||||||
|
data: (data) => AudioCallButton(room: data!),
|
||||||
|
error: (err, _) => const SizedBox.shrink(),
|
||||||
|
loading: () => const SizedBox.shrink(),
|
||||||
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.more_vert),
|
icon: const Icon(Icons.more_vert),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
@@ -1167,7 +843,14 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
top: 0,
|
top: 0,
|
||||||
child: CallOverlayBar().padding(horizontal: 8, top: 12),
|
child: chatRoom.when(
|
||||||
|
data:
|
||||||
|
(data) => CallOverlayBar(
|
||||||
|
room: data!,
|
||||||
|
).padding(horizontal: 8, top: 12),
|
||||||
|
error: (_, _) => const SizedBox.shrink(),
|
||||||
|
loading: () => const SizedBox.shrink(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (isSyncing)
|
if (isSyncing)
|
||||||
Positioned(
|
Positioned(
|
||||||
|
|||||||
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: (_) {},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -403,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)),
|
||||||
@@ -585,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: [
|
||||||
@@ -831,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(
|
||||||
'/sphere/publishers/$publisherUname/invites',
|
'/sphere/publishers/invites/$publisherUname',
|
||||||
data: {'related_user_id': result.id, 'role': 0},
|
data: {'related_user_id': result.id, 'role': 0},
|
||||||
);
|
);
|
||||||
// Refresh both providers
|
// Refresh both providers
|
||||||
@@ -1119,7 +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);
|
||||||
@@ -1132,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) {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:island/models/poll.dart';
|
import 'package:island/models/poll.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
import 'package:island/screens/poll/poll_editor.dart';
|
import 'package:island/screens/poll/poll_editor.dart';
|
||||||
|
import 'package:island/widgets/alert.dart';
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
import 'package:island/widgets/poll/poll_feedback.dart';
|
import 'package:island/widgets/poll/poll_feedback.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
@@ -234,19 +235,9 @@ class _CreatorPollItem extends HookConsumerWidget {
|
|||||||
'/sphere/polls/${pollWithStats.id}',
|
'/sphere/polls/${pollWithStats.id}',
|
||||||
);
|
);
|
||||||
ref.invalidate(pollListNotifierProvider(pubName));
|
ref.invalidate(pollListNotifierProvider(pubName));
|
||||||
if (context.mounted) {
|
showSnackBar('Poll deleted successfully');
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text('Poll deleted successfully'),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (context.mounted) {
|
showErrorAlert(e);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(content: Text('Failed to delete poll')),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
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
|
||||||
@@ -545,6 +545,7 @@ class ExploreScreen extends HookConsumerWidget {
|
|||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: FriendsOverviewWidget(
|
child: FriendsOverviewWidget(
|
||||||
padding: const EdgeInsets.only(bottom: 8),
|
padding: const EdgeInsets.only(bottom: 8),
|
||||||
|
hideWhenEmpty: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (notificationCount.value != null &&
|
if (notificationCount.value != null &&
|
||||||
|
|||||||
@@ -6,17 +6,24 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:gal/gal.dart';
|
import 'package:gal/gal.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/file.dart';
|
import 'package:island/models/file.dart';
|
||||||
import 'package:island/pods/config.dart';
|
import 'package:island/pods/config.dart';
|
||||||
|
import 'package:island/pods/file_references.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
|
import 'package:island/pods/upload_tasks.dart';
|
||||||
|
import 'package:island/models/drive_task.dart';
|
||||||
import 'package:island/services/responsive.dart';
|
import 'package:island/services/responsive.dart';
|
||||||
|
import 'package:island/services/time.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
import 'package:island/widgets/content/file_info_sheet.dart';
|
import 'package:island/widgets/content/file_info_sheet.dart';
|
||||||
import 'package:island/widgets/content/file_viewer_contents.dart';
|
import 'package:island/widgets/content/file_viewer_contents.dart';
|
||||||
|
import 'package:island/widgets/content/sheet.dart';
|
||||||
import 'package:path/path.dart' show extension;
|
import 'package:path/path.dart' show extension;
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
|
||||||
class FileDetailScreen extends HookConsumerWidget {
|
class FileDetailScreen extends HookConsumerWidget {
|
||||||
final SnCloudFile item;
|
final SnCloudFile item;
|
||||||
@@ -76,7 +83,7 @@ class FileDetailScreen extends HookConsumerWidget {
|
|||||||
}, [animationController]);
|
}, [animationController]);
|
||||||
|
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
isNoBackground: true,
|
isNoBackground: false,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
@@ -86,28 +93,49 @@ class FileDetailScreen extends HookConsumerWidget {
|
|||||||
title: Text(item.name.isEmpty ? 'File Details' : item.name),
|
title: Text(item.name.isEmpty ? 'File Details' : item.name),
|
||||||
actions: _buildAppBarActions(context, ref, showInfoSheet),
|
actions: _buildAppBarActions(context, ref, showInfoSheet),
|
||||||
),
|
),
|
||||||
body: AnimatedBuilder(
|
body: LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
return AnimatedBuilder(
|
||||||
animation: animation,
|
animation: animation,
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
return Row(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
// Main content area
|
// Main content area - resizes with animation
|
||||||
Expanded(child: _buildContent(context, ref, serverUrl)),
|
Positioned(
|
||||||
// Animated drawer panel
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
width: constraints.maxWidth - animation.value * 400,
|
||||||
|
child: _buildContent(context, ref, serverUrl),
|
||||||
|
),
|
||||||
|
// Animated drawer panel - overlays
|
||||||
if (isWide)
|
if (isWide)
|
||||||
SizedBox(
|
Positioned(
|
||||||
height: double.infinity,
|
right: 0,
|
||||||
width: animation.value * 400, // Max width of 400px
|
top: 0,
|
||||||
child: Container(
|
bottom: 0,
|
||||||
child:
|
width: 400,
|
||||||
animation.value > 0.1
|
child: Transform.translate(
|
||||||
? FileInfoSheet(item: item, onClose: showInfoSheet)
|
offset: Offset((1 - animation.value) * 400, 0),
|
||||||
: const SizedBox.shrink(),
|
child: SizedBox(
|
||||||
|
width: 400,
|
||||||
|
child: Material(
|
||||||
|
color:
|
||||||
|
Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
elevation: 8,
|
||||||
|
child: FileInfoSheet(
|
||||||
|
item: item,
|
||||||
|
onClose: showInfoSheet,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -144,6 +172,24 @@ class FileDetailScreen extends HookConsumerWidget {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add references button
|
||||||
|
actions.add(
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.link),
|
||||||
|
onPressed:
|
||||||
|
() => showModalBottomSheet(
|
||||||
|
useRootNavigator: true,
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
builder:
|
||||||
|
(context) => SheetScaffold(
|
||||||
|
titleText: 'File References',
|
||||||
|
child: ReferencesList(fileId: item.id),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
// Always add info button
|
// Always add info button
|
||||||
actions.add(
|
actions.add(
|
||||||
IconButton(icon: Icon(Icons.info_outline), onPressed: showInfoSheet),
|
IconButton(icon: Icon(Icons.info_outline), onPressed: showInfoSheet),
|
||||||
@@ -187,6 +233,8 @@ class FileDetailScreen extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _downloadFile(WidgetRef ref) async {
|
Future<void> _downloadFile(WidgetRef ref) async {
|
||||||
|
final taskNotifier = ref.read(uploadTasksProvider.notifier);
|
||||||
|
final taskId = taskNotifier.addLocalDownloadTask(item);
|
||||||
try {
|
try {
|
||||||
showSnackBar('Downloading file...');
|
showSnackBar('Downloading file...');
|
||||||
|
|
||||||
@@ -202,14 +250,26 @@ class FileDetailScreen extends HookConsumerWidget {
|
|||||||
'/drive/files/${item.id}',
|
'/drive/files/${item.id}',
|
||||||
filePath,
|
filePath,
|
||||||
queryParameters: {'original': true},
|
queryParameters: {'original': true},
|
||||||
|
onReceiveProgress: (count, total) {
|
||||||
|
if (total > 0) {
|
||||||
|
taskNotifier.updateDownloadProgress(taskId, count, total);
|
||||||
|
taskNotifier.updateTransmissionProgress(taskId, count / total);
|
||||||
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
await FileSaver.instance.saveFile(
|
await FileSaver.instance.saveFile(
|
||||||
name: item.name.isEmpty ? '${item.id}.$extName' : item.name,
|
name: item.name.isEmpty ? '${item.id}.$extName' : item.name,
|
||||||
file: File(filePath),
|
file: File(filePath),
|
||||||
);
|
);
|
||||||
|
taskNotifier.updateTaskStatus(taskId, DriveTaskStatus.completed);
|
||||||
showSnackBar('File saved to downloads');
|
showSnackBar('File saved to downloads');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
taskNotifier.updateTaskStatus(
|
||||||
|
taskId,
|
||||||
|
DriveTaskStatus.failed,
|
||||||
|
errorMessage: e.toString(),
|
||||||
|
);
|
||||||
showErrorAlert(e);
|
showErrorAlert(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -229,3 +289,54 @@ class FileDetailScreen extends HookConsumerWidget {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ReferencesList extends ConsumerWidget {
|
||||||
|
const ReferencesList({super.key, required this.fileId});
|
||||||
|
|
||||||
|
final String fileId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final asyncReferences = ref.watch(fileReferencesProvider(fileId));
|
||||||
|
|
||||||
|
return asyncReferences.when(
|
||||||
|
data:
|
||||||
|
(references) => ListView.builder(
|
||||||
|
itemCount: references.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final reference = references[index];
|
||||||
|
return ListTile(
|
||||||
|
leading: const Icon(Icons.link),
|
||||||
|
title: Row(
|
||||||
|
spacing: 6,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
reference.usage,
|
||||||
|
style: GoogleFonts.robotoMono(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 13,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
reference.id,
|
||||||
|
style: GoogleFonts.robotoMono(fontSize: 13),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
subtitle: Row(
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
Text(reference.createdAt.formatRelative(context)),
|
||||||
|
const VerticalDivider(width: 1, thickness: 1).height(12),
|
||||||
|
Text(reference.createdAt.formatSystem()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
|
error:
|
||||||
|
(error, _) => Center(child: Text('Error loading references: $error')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import 'package:cross_file/cross_file.dart';
|
import 'package:cross_file/cross_file.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/file.dart';
|
import 'package:island/models/file.dart';
|
||||||
|
import 'package:island/models/file_pool.dart';
|
||||||
import 'package:island/pods/file_list.dart';
|
import 'package:island/pods/file_list.dart';
|
||||||
import 'package:island/services/file_uploader.dart';
|
import 'package:island/services/file_uploader.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
@@ -23,6 +25,7 @@ class FileListScreen extends HookConsumerWidget {
|
|||||||
// Path navigation state
|
// Path navigation state
|
||||||
final currentPath = useState<String>('/');
|
final currentPath = useState<String>('/');
|
||||||
final mode = useState<FileListMode>(FileListMode.normal);
|
final mode = useState<FileListMode>(FileListMode.normal);
|
||||||
|
final selectedPool = useState<SnFilePool?>(null);
|
||||||
|
|
||||||
final usageAsync = ref.watch(billingUsageProvider);
|
final usageAsync = ref.watch(billingUsageProvider);
|
||||||
final quotaAsync = ref.watch(billingQuotaProvider);
|
final quotaAsync = ref.watch(billingQuotaProvider);
|
||||||
@@ -32,7 +35,7 @@ class FileListScreen extends HookConsumerWidget {
|
|||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
isNoBackground: false,
|
isNoBackground: false,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text('Files'),
|
title: Text('files').tr(),
|
||||||
leading: const PageBackButton(),
|
leading: const PageBackButton(),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
@@ -55,8 +58,13 @@ class FileListScreen extends HookConsumerWidget {
|
|||||||
usage: usage,
|
usage: usage,
|
||||||
quota: quota,
|
quota: quota,
|
||||||
currentPath: currentPath,
|
currentPath: currentPath,
|
||||||
|
selectedPool: selectedPool,
|
||||||
onPickAndUpload:
|
onPickAndUpload:
|
||||||
() => _pickAndUploadFile(ref, currentPath.value),
|
() => _pickAndUploadFile(
|
||||||
|
ref,
|
||||||
|
currentPath.value,
|
||||||
|
selectedPool.value?.id,
|
||||||
|
),
|
||||||
onShowCreateDirectory: _showCreateDirectoryDialog,
|
onShowCreateDirectory: _showCreateDirectoryDialog,
|
||||||
mode: mode,
|
mode: mode,
|
||||||
viewMode: viewMode,
|
viewMode: viewMode,
|
||||||
@@ -70,7 +78,11 @@ class FileListScreen extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _pickAndUploadFile(WidgetRef ref, String currentPath) async {
|
Future<void> _pickAndUploadFile(
|
||||||
|
WidgetRef ref,
|
||||||
|
String currentPath,
|
||||||
|
String? poolId,
|
||||||
|
) async {
|
||||||
try {
|
try {
|
||||||
final result = await FilePicker.platform.pickFiles(
|
final result = await FilePicker.platform.pickFiles(
|
||||||
allowMultiple: true,
|
allowMultiple: true,
|
||||||
@@ -92,6 +104,7 @@ class FileListScreen extends HookConsumerWidget {
|
|||||||
fileData: universalFile,
|
fileData: universalFile,
|
||||||
ref: ref,
|
ref: ref,
|
||||||
path: currentPath,
|
path: currentPath,
|
||||||
|
poolId: poolId,
|
||||||
onProgress: (progress, _) {
|
onProgress: (progress, _) {
|
||||||
// Progress is handled by the upload tasks system
|
// Progress is handled by the upload tasks system
|
||||||
if (progress != null) {
|
if (progress != null) {
|
||||||
|
|||||||
@@ -534,7 +534,7 @@ class _LotteryPurchaseSheetState extends State<LotteryPurchaseSheet> {
|
|||||||
),
|
),
|
||||||
const Gap(4),
|
const Gap(4),
|
||||||
Text(
|
Text(
|
||||||
'The last selected number will be your special number.',
|
'lotteryLastNumberSpecial'.tr(),
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
color: Theme.of(context).colorScheme.secondary,
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
@@ -738,11 +738,11 @@ class _LotteryPurchaseSheetState extends State<LotteryPurchaseSheet> {
|
|||||||
},
|
},
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value == null || value.isEmpty) {
|
if (value == null || value.isEmpty) {
|
||||||
return 'Please enter a multiplier';
|
return 'lotteryMultiplierRequired'.tr();
|
||||||
}
|
}
|
||||||
final parsed = int.tryParse(value);
|
final parsed = int.tryParse(value);
|
||||||
if (parsed == null || parsed < 1 || parsed > 10) {
|
if (parsed == null || parsed < 1 || parsed > 10) {
|
||||||
return 'Multiplier must be between 1 and 10';
|
return 'lotteryMultiplierRange'.tr();
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -451,7 +451,7 @@ class PollEditorScreen extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (confirmed == true) {
|
if (confirmed == true) {
|
||||||
Navigator.of(context).pop();
|
if (context.mounted) Navigator.of(context).pop();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|||||||
@@ -82,12 +82,32 @@ class _ParsedVersion implements Comparable<_ParsedVersion> {
|
|||||||
return _ParsedVersion(major, minor, patch, build);
|
return _ParsedVersion(major, minor, patch, build);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Normalize Android build numbers by removing architecture-based offsets
|
||||||
|
/// Android adds 1000 for x86, 2000 for ARMv7, 4000 for ARMv8
|
||||||
|
int get normalizedBuild {
|
||||||
|
// Check if build number has an architecture offset
|
||||||
|
// We detect this by checking if the build % 1000 is the base build
|
||||||
|
if (build >= 4000) {
|
||||||
|
// Likely ARMv8 (arm64-v8a) with +4000 offset
|
||||||
|
return build % 4000;
|
||||||
|
} else if (build >= 2000) {
|
||||||
|
// Likely ARMv7 (armeabi-v7a) with +2000 offset
|
||||||
|
return build % 2000;
|
||||||
|
} else if (build >= 1000) {
|
||||||
|
// Likely x86/x86_64 with +1000 offset
|
||||||
|
return build % 1000;
|
||||||
|
}
|
||||||
|
// No offset, return as-is
|
||||||
|
return build;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int compareTo(_ParsedVersion other) {
|
int compareTo(_ParsedVersion other) {
|
||||||
if (major != other.major) return major.compareTo(other.major);
|
if (major != other.major) return major.compareTo(other.major);
|
||||||
if (minor != other.minor) return minor.compareTo(other.minor);
|
if (minor != other.minor) return minor.compareTo(other.minor);
|
||||||
if (patch != other.patch) return patch.compareTo(other.patch);
|
if (patch != other.patch) return patch.compareTo(other.patch);
|
||||||
return build.compareTo(other.build);
|
// Use normalized build numbers for comparison to handle Android arch offsets
|
||||||
|
return normalizedBuild.compareTo(other.normalizedBuild);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -244,7 +264,8 @@ class UpdateService {
|
|||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
builder: (context) => _WindowsUpdateDialog(
|
builder:
|
||||||
|
(context) => _WindowsUpdateDialog(
|
||||||
updateUrl: url,
|
updateUrl: url,
|
||||||
onComplete: () {
|
onComplete: () {
|
||||||
// Close the update sheet
|
// Close the update sheet
|
||||||
@@ -321,7 +342,9 @@ class _WindowsUpdateDialog extends StatefulWidget {
|
|||||||
|
|
||||||
class _WindowsUpdateDialogState extends State<_WindowsUpdateDialog> {
|
class _WindowsUpdateDialogState extends State<_WindowsUpdateDialog> {
|
||||||
final ValueNotifier<double?> progressNotifier = ValueNotifier<double?>(null);
|
final ValueNotifier<double?> progressNotifier = ValueNotifier<double?>(null);
|
||||||
final ValueNotifier<String> messageNotifier = ValueNotifier<String>('Downloading installer...');
|
final ValueNotifier<String> messageNotifier = ValueNotifier<String>(
|
||||||
|
'Downloading installer...',
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -392,7 +415,8 @@ class _WindowsUpdateDialogState extends State<_WindowsUpdateDialog> {
|
|||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder:
|
||||||
|
(context) => AlertDialog(
|
||||||
title: const Text('Update Failed'),
|
title: const Text('Update Failed'),
|
||||||
content: Text(message),
|
content: Text(message),
|
||||||
actions: [
|
actions: [
|
||||||
@@ -458,7 +482,9 @@ class _WindowsUpdateDialogState extends State<_WindowsUpdateDialog> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
talker.info('[Update] Windows installer downloaded successfully to: $filePath');
|
talker.info(
|
||||||
|
'[Update] Windows installer downloaded successfully to: $filePath',
|
||||||
|
);
|
||||||
return filePath;
|
return filePath;
|
||||||
} else {
|
} else {
|
||||||
talker.error(
|
talker.error(
|
||||||
@@ -500,7 +526,9 @@ class _WindowsUpdateDialogState extends State<_WindowsUpdateDialog> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
talker.info('[Update] Windows installer extracted successfully to: $extractDir');
|
talker.info(
|
||||||
|
'[Update] Windows installer extracted successfully to: $extractDir',
|
||||||
|
);
|
||||||
return extractDir;
|
return extractDir;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
talker.error('[Update] Error extracting Windows installer: $e');
|
talker.error('[Update] Error extracting Windows installer: $e');
|
||||||
@@ -514,7 +542,8 @@ class _WindowsUpdateDialogState extends State<_WindowsUpdateDialog> {
|
|||||||
talker.info('[Update] Running Windows installer from: $extractDir');
|
talker.info('[Update] Running Windows installer from: $extractDir');
|
||||||
|
|
||||||
final dir = Directory(extractDir);
|
final dir = Directory(extractDir);
|
||||||
final exeFiles = dir
|
final exeFiles =
|
||||||
|
dir
|
||||||
.listSync()
|
.listSync()
|
||||||
.where((f) => f is File && f.path.endsWith('.exe'))
|
.where((f) => f is File && f.path.endsWith('.exe'))
|
||||||
.toList();
|
.toList();
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ class AccountName extends StatelessWidget {
|
|||||||
final String? textOverride;
|
final String? textOverride;
|
||||||
final bool ignorePermissions;
|
final bool ignorePermissions;
|
||||||
final bool hideVerificationMark;
|
final bool hideVerificationMark;
|
||||||
|
final bool hideOverlay;
|
||||||
const AccountName({
|
const AccountName({
|
||||||
super.key,
|
super.key,
|
||||||
required this.account,
|
required this.account,
|
||||||
@@ -46,6 +47,7 @@ class AccountName extends StatelessWidget {
|
|||||||
this.textOverride,
|
this.textOverride,
|
||||||
this.ignorePermissions = false,
|
this.ignorePermissions = false,
|
||||||
this.hideVerificationMark = false,
|
this.hideVerificationMark = false,
|
||||||
|
this.hideOverlay = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
Alignment _parseGradientDirection(String direction) {
|
Alignment _parseGradientDirection(String direction) {
|
||||||
@@ -189,12 +191,25 @@ class AccountName extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (account.perkSubscription != null)
|
if (account.perkSubscription != null)
|
||||||
StellarMembershipMark(membership: account.perkSubscription!),
|
StellarMembershipMark(
|
||||||
|
membership: account.perkSubscription!,
|
||||||
|
hideOverlay: hideOverlay,
|
||||||
|
),
|
||||||
if (account.profile.verification != null &&
|
if (account.profile.verification != null &&
|
||||||
!hideVerificationMark)
|
!hideVerificationMark)
|
||||||
VerificationMark(mark: account.profile.verification!),
|
VerificationMark(
|
||||||
|
mark: account.profile.verification!,
|
||||||
|
hideOverlay: hideOverlay,
|
||||||
|
),
|
||||||
if (account.automatedId != null)
|
if (account.automatedId != null)
|
||||||
Tooltip(
|
hideOverlay
|
||||||
|
? Icon(
|
||||||
|
Symbols.smart_toy,
|
||||||
|
size: 16,
|
||||||
|
color: nameStyle.color,
|
||||||
|
fill: 1,
|
||||||
|
)
|
||||||
|
: Tooltip(
|
||||||
message: 'accountAutomated'.tr(),
|
message: 'accountAutomated'.tr(),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Symbols.smart_toy,
|
Symbols.smart_toy,
|
||||||
@@ -226,18 +241,31 @@ class AccountName extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Text(
|
child: Text(
|
||||||
account.nick,
|
textOverride ?? account.nick,
|
||||||
style: nameStyle,
|
style: nameStyle,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (account.perkSubscription != null)
|
if (account.perkSubscription != null)
|
||||||
StellarMembershipMark(membership: account.perkSubscription!),
|
StellarMembershipMark(
|
||||||
|
membership: account.perkSubscription!,
|
||||||
|
hideOverlay: hideOverlay,
|
||||||
|
),
|
||||||
if (account.profile.verification != null)
|
if (account.profile.verification != null)
|
||||||
VerificationMark(mark: account.profile.verification!),
|
VerificationMark(
|
||||||
|
mark: account.profile.verification!,
|
||||||
|
hideOverlay: hideOverlay,
|
||||||
|
),
|
||||||
if (account.automatedId != null)
|
if (account.automatedId != null)
|
||||||
Tooltip(
|
hideOverlay
|
||||||
|
? Icon(
|
||||||
|
Symbols.smart_toy,
|
||||||
|
size: 16,
|
||||||
|
color: nameStyle.color,
|
||||||
|
fill: 1,
|
||||||
|
)
|
||||||
|
: Tooltip(
|
||||||
message: 'accountAutomated'.tr(),
|
message: 'accountAutomated'.tr(),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Symbols.smart_toy,
|
Symbols.smart_toy,
|
||||||
@@ -253,11 +281,29 @@ class AccountName extends StatelessWidget {
|
|||||||
|
|
||||||
class VerificationMark extends StatelessWidget {
|
class VerificationMark extends StatelessWidget {
|
||||||
final SnVerificationMark mark;
|
final SnVerificationMark mark;
|
||||||
const VerificationMark({super.key, required this.mark});
|
final bool hideOverlay;
|
||||||
|
const VerificationMark({
|
||||||
|
super.key,
|
||||||
|
required this.mark,
|
||||||
|
this.hideOverlay = false,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Tooltip(
|
final icon = Icon(
|
||||||
|
mark.type == 4
|
||||||
|
? Symbols.play_circle
|
||||||
|
: mark.type == 0
|
||||||
|
? Symbols.build_circle
|
||||||
|
: Symbols.verified,
|
||||||
|
size: 16,
|
||||||
|
color: kVerificationMarkColors[mark.type],
|
||||||
|
fill: 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
return hideOverlay
|
||||||
|
? icon
|
||||||
|
: Tooltip(
|
||||||
richMessage: TextSpan(
|
richMessage: TextSpan(
|
||||||
text: mark.title ?? 'No title',
|
text: mark.title ?? 'No title',
|
||||||
children: [
|
children: [
|
||||||
@@ -269,23 +315,19 @@ class VerificationMark extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
child: Icon(
|
child: icon,
|
||||||
mark.type == 4
|
|
||||||
? Symbols.play_circle
|
|
||||||
: mark.type == 0
|
|
||||||
? Symbols.build_circle
|
|
||||||
: Symbols.verified,
|
|
||||||
size: 16,
|
|
||||||
color: kVerificationMarkColors[mark.type],
|
|
||||||
fill: 1,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class StellarMembershipMark extends StatelessWidget {
|
class StellarMembershipMark extends StatelessWidget {
|
||||||
final SnWalletSubscriptionRef membership;
|
final SnWalletSubscriptionRef membership;
|
||||||
const StellarMembershipMark({super.key, required this.membership});
|
final bool hideOverlay;
|
||||||
|
const StellarMembershipMark({
|
||||||
|
super.key,
|
||||||
|
required this.membership,
|
||||||
|
this.hideOverlay = false,
|
||||||
|
});
|
||||||
|
|
||||||
String _getMembershipTierName(String identifier) {
|
String _getMembershipTierName(String identifier) {
|
||||||
switch (identifier) {
|
switch (identifier) {
|
||||||
@@ -321,7 +363,11 @@ class StellarMembershipMark extends StatelessWidget {
|
|||||||
final tierColor = _getMembershipTierColor(membership.identifier);
|
final tierColor = _getMembershipTierColor(membership.identifier);
|
||||||
final tierIcon = Symbols.kid_star;
|
final tierIcon = Symbols.kid_star;
|
||||||
|
|
||||||
return Tooltip(
|
final icon = Icon(tierIcon, size: 16, color: tierColor, fill: 1);
|
||||||
|
|
||||||
|
return hideOverlay
|
||||||
|
? icon
|
||||||
|
: Tooltip(
|
||||||
richMessage: TextSpan(
|
richMessage: TextSpan(
|
||||||
text: 'stellarMembership'.tr(),
|
text: 'stellarMembership'.tr(),
|
||||||
children: [
|
children: [
|
||||||
@@ -333,7 +379,7 @@ class StellarMembershipMark extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
child: Icon(tierIcon, size: 16, color: tierColor, fill: 1),
|
child: icon,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,24 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:fl_heatmap/fl_heatmap.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/heatmap.dart';
|
import 'package:island/models/heatmap.dart';
|
||||||
import '../services/responsive.dart';
|
import 'package:island/services/responsive.dart';
|
||||||
|
|
||||||
|
/// Custom data class for selected heatmap item
|
||||||
|
class SelectedHeatmapItem {
|
||||||
|
final double value;
|
||||||
|
final String unit;
|
||||||
|
final String dateString;
|
||||||
|
final String dayLabel;
|
||||||
|
|
||||||
|
SelectedHeatmapItem({
|
||||||
|
required this.value,
|
||||||
|
required this.unit,
|
||||||
|
required this.dateString,
|
||||||
|
required this.dayLabel,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// A reusable heatmap widget for displaying activity data in GitHub-style layout.
|
/// A reusable heatmap widget for displaying activity data in GitHub-style layout.
|
||||||
/// Shows exactly 365 days (wide screen) or 90 days (non-wide screen) of data ending at the current date.
|
/// Shows exactly 365 days (wide screen) or 90 days (non-wide screen) of data ending at the current date.
|
||||||
@@ -21,7 +34,7 @@ class ActivityHeatmapWidget extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final selectedItem = useState<HeatmapItem?>(null);
|
final selectedItem = useState<SelectedHeatmapItem?>(null);
|
||||||
|
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
|
|
||||||
@@ -101,48 +114,18 @@ class ActivityHeatmapWidget extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final heatmapData = HeatmapData(
|
// Find maximum value for color scaling
|
||||||
rows: [
|
final maxValue =
|
||||||
'Mon',
|
dataMap.values.isNotEmpty
|
||||||
'Tue',
|
? dataMap.values.reduce((a, b) => a > b ? a : b)
|
||||||
'Wed',
|
: 1.0;
|
||||||
'Thu',
|
|
||||||
'Fri',
|
// Helper function to get color based on activity level
|
||||||
'Sat',
|
Color getActivityColor(double value) {
|
||||||
'Sun',
|
if (value == 0) return Colors.grey.withOpacity(0.1);
|
||||||
], // Days of week vertically
|
final intensity = value / maxValue;
|
||||||
columns:
|
return Colors.green.withOpacity(0.2 + (intensity * 0.8));
|
||||||
weeks
|
}
|
||||||
.map(
|
|
||||||
(w) =>
|
|
||||||
'${w.year}-${w.month.toString().padLeft(2, '0')}-${w.day.toString().padLeft(2, '0')}',
|
|
||||||
)
|
|
||||||
.toList(), // Weeks horizontally
|
|
||||||
items: [
|
|
||||||
for (int day = 0; day < 7; day++) // For each day of week (Mon-Sun)
|
|
||||||
for (final week in weeks) // For each week
|
|
||||||
HeatmapItem(
|
|
||||||
value: dataMap[week.add(Duration(days: day))] ?? 0.0,
|
|
||||||
unit: heatmap.unit,
|
|
||||||
xAxisLabel:
|
|
||||||
'${week.year}-${week.month.toString().padLeft(2, '0')}-${week.day.toString().padLeft(2, '0')}',
|
|
||||||
yAxisLabel:
|
|
||||||
day == 0
|
|
||||||
? 'Mon'
|
|
||||||
: day == 1
|
|
||||||
? 'Tue'
|
|
||||||
: day == 2
|
|
||||||
? 'Wed'
|
|
||||||
: day == 3
|
|
||||||
? 'Thu'
|
|
||||||
: day == 4
|
|
||||||
? 'Fri'
|
|
||||||
: day == 5
|
|
||||||
? 'Sat'
|
|
||||||
: 'Sun',
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
@@ -151,39 +134,103 @@ class ActivityHeatmapWidget extends HookConsumerWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
// Month labels row - aligned with month start positions
|
||||||
'activityHeatmap',
|
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
|
||||||
).tr(),
|
|
||||||
const Gap(8),
|
|
||||||
// Month labels row
|
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(width: 30), // Space for day labels
|
const SizedBox(width: 30), // Space for day labels
|
||||||
...monthLabels.asMap().entries.map((entry) {
|
...List.generate(weeks.length, (weekIndex) {
|
||||||
final month = entry.value;
|
// Check if this week is the start of a month
|
||||||
|
final monthIndex = monthPositions.indexOf(weekIndex);
|
||||||
|
final monthText =
|
||||||
|
monthIndex != -1 ? monthLabels[monthIndex] : null;
|
||||||
|
|
||||||
return Expanded(
|
return monthText != null
|
||||||
child: Container(
|
? Expanded(
|
||||||
alignment: Alignment.center,
|
|
||||||
child: Text(
|
child: Text(
|
||||||
month,
|
monthText,
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
);
|
: SizedBox.shrink();
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const Gap(4),
|
const Gap(4),
|
||||||
Heatmap(
|
// Custom heatmap grid
|
||||||
heatmapData: heatmapData,
|
Column(
|
||||||
rowsVisible: 7,
|
children: List.generate(7, (dayIndex) {
|
||||||
showXAxisLabels: false,
|
final dayLabels = [
|
||||||
onItemSelectedListener: (item) {
|
'Mon',
|
||||||
selectedItem.value = item;
|
'Tue',
|
||||||
|
'Wed',
|
||||||
|
'Thu',
|
||||||
|
'Fri',
|
||||||
|
'Sat',
|
||||||
|
'Sun',
|
||||||
|
];
|
||||||
|
final dayLabel = dayLabels[dayIndex];
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
// Day label
|
||||||
|
SizedBox(
|
||||||
|
width: 30,
|
||||||
|
child: Text(
|
||||||
|
dayLabel,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Activity squares for each week - evenly distributed
|
||||||
|
Expanded(
|
||||||
|
child: Row(
|
||||||
|
children: List.generate(weeks.length, (weekIndex) {
|
||||||
|
final week = weeks[weekIndex];
|
||||||
|
final date = week.add(Duration(days: dayIndex));
|
||||||
|
final value = dataMap[date] ?? 0.0;
|
||||||
|
final dateString =
|
||||||
|
'${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
||||||
|
|
||||||
|
return Expanded(
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
selectedItem.value = SelectedHeatmapItem(
|
||||||
|
value: value,
|
||||||
|
unit: heatmap.unit,
|
||||||
|
dateString: dateString,
|
||||||
|
dayLabel: dayLabel,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
child: Container(
|
||||||
|
height: 12,
|
||||||
|
margin: const EdgeInsets.all(1),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: getActivityColor(value),
|
||||||
|
borderRadius: BorderRadius.circular(2),
|
||||||
|
border:
|
||||||
|
selectedItem.value != null &&
|
||||||
|
selectedItem.value!.dateString ==
|
||||||
|
dateString &&
|
||||||
|
selectedItem.value!.dayLabel ==
|
||||||
|
dayLabel
|
||||||
|
? Border.all(
|
||||||
|
color: Colors.blue,
|
||||||
|
width: 1,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
// Legend
|
// Legend
|
||||||
@@ -203,9 +250,7 @@ class ActivityHeatmapWidget extends HookConsumerWidget {
|
|||||||
style: Theme.of(context).textTheme.bodySmall,
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
),
|
),
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: _formatDate(
|
text: _formatDate(selectedItem.value!.dateString),
|
||||||
selectedItem.value!.xAxisLabel ?? '',
|
|
||||||
),
|
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:island/main.dart';
|
import 'package:island/main.dart';
|
||||||
|
import 'package:island/talker.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:top_snackbar_flutter/top_snack_bar.dart';
|
import 'package:top_snackbar_flutter/top_snack_bar.dart';
|
||||||
|
|
||||||
export 'content/alert.native.dart'
|
|
||||||
if (dart.library.html) 'content/alert.web.dart';
|
|
||||||
|
|
||||||
void showSnackBar(String message, {SnackBarAction? action}) {
|
void showSnackBar(String message, {SnackBarAction? action}) {
|
||||||
final context = globalOverlay.currentState!.context;
|
final context = globalOverlay.currentState!.context;
|
||||||
final screenWidth = MediaQuery.of(context).size.width;
|
final screenWidth = MediaQuery.of(context).size.width;
|
||||||
@@ -29,43 +30,60 @@ void showSnackBar(String message, {SnackBarAction? action}) {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
snackBarPosition: SnackBarPosition.bottom,
|
snackBarPosition: SnackBarPosition.bottom,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void clearSnackBar(BuildContext context) {
|
|
||||||
ScaffoldMessenger.of(context).clearSnackBars();
|
|
||||||
}
|
|
||||||
|
|
||||||
OverlayEntry? _loadingOverlay;
|
OverlayEntry? _loadingOverlay;
|
||||||
GlobalKey<_FadeOverlayState> _loadingOverlayKey = GlobalKey();
|
GlobalKey<_FadeOverlayState> _loadingOverlayKey = GlobalKey();
|
||||||
|
|
||||||
class _FadeOverlay extends StatefulWidget {
|
class _FadeOverlay extends StatefulWidget {
|
||||||
const _FadeOverlay({super.key, required this.child});
|
const _FadeOverlay({
|
||||||
final Widget child;
|
super.key,
|
||||||
|
this.child,
|
||||||
|
this.builder,
|
||||||
|
this.duration = const Duration(milliseconds: 200),
|
||||||
|
this.curve = Curves.linear,
|
||||||
|
}) : assert(child != null || builder != null);
|
||||||
|
|
||||||
|
final Widget? child;
|
||||||
|
final Widget Function(BuildContext, Animation<double>)? builder;
|
||||||
|
final Duration duration;
|
||||||
|
final Curve curve;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_FadeOverlay> createState() => _FadeOverlayState();
|
State<_FadeOverlay> createState() => _FadeOverlayState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FadeOverlayState extends State<_FadeOverlay> {
|
class _FadeOverlayState extends State<_FadeOverlay>
|
||||||
bool _visible = false;
|
with SingleTickerProviderStateMixin {
|
||||||
|
late AnimationController _controller;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
_controller = AnimationController(vsync: this, duration: widget.duration);
|
||||||
setState(() => _visible = true);
|
_controller.forward();
|
||||||
});
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> animateOut() async {
|
||||||
|
await _controller.reverse();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AnimatedOpacity(
|
final animation = CurvedAnimation(parent: _controller, curve: widget.curve);
|
||||||
opacity: _visible ? 1.0 : 0.0,
|
if (widget.builder != null) {
|
||||||
duration: const Duration(milliseconds: 200),
|
return widget.builder!(context, animation);
|
||||||
child: widget.child,
|
}
|
||||||
);
|
return FadeTransition(opacity: animation, child: widget.child);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,10 +127,156 @@ void hideLoadingModal(BuildContext context) async {
|
|||||||
final state = entry.mounted ? _loadingOverlayKey.currentState : null;
|
final state = entry.mounted ? _loadingOverlayKey.currentState : null;
|
||||||
|
|
||||||
if (state != null) {
|
if (state != null) {
|
||||||
// ignore: invalid_use_of_protected_member
|
await state.animateOut();
|
||||||
state.setState(() => state._visible = false);
|
|
||||||
await Future.delayed(const Duration(milliseconds: 200));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
entry.remove();
|
entry.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _parseRemoteError(DioException err) {
|
||||||
|
String? message;
|
||||||
|
if (err.response?.data is String) {
|
||||||
|
message = err.response?.data;
|
||||||
|
} else if (err.response?.data?['message'] != null) {
|
||||||
|
message = <String?>[
|
||||||
|
err.response?.data?['message']?.toString(),
|
||||||
|
err.response?.data?['detail']?.toString(),
|
||||||
|
].where((e) => e != null).cast<String>().map((e) => e.trim()).join('\n');
|
||||||
|
} else if (err.response?.data?['errors'] != null) {
|
||||||
|
final errors = err.response?.data['errors'] as Map<String, dynamic>;
|
||||||
|
message = errors.values
|
||||||
|
.map(
|
||||||
|
(ele) =>
|
||||||
|
(ele as List<dynamic>).map((ele) => ele.toString()).join('\n'),
|
||||||
|
)
|
||||||
|
.join('\n');
|
||||||
|
}
|
||||||
|
if (message == null || message.isEmpty) message = err.response?.statusMessage;
|
||||||
|
message ??= err.message;
|
||||||
|
return message ?? err.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<T?> showOverlayDialog<T>({
|
||||||
|
required Widget Function(BuildContext context, void Function(T? result) close)
|
||||||
|
builder,
|
||||||
|
bool barrierDismissible = true,
|
||||||
|
}) {
|
||||||
|
final completer = Completer<T?>();
|
||||||
|
final key = GlobalKey<_FadeOverlayState>();
|
||||||
|
late OverlayEntry entry;
|
||||||
|
|
||||||
|
void close(T? result) async {
|
||||||
|
if (completer.isCompleted) return;
|
||||||
|
|
||||||
|
final state = key.currentState;
|
||||||
|
if (state != null) {
|
||||||
|
await state.animateOut();
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.remove();
|
||||||
|
completer.complete(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
entry = OverlayEntry(
|
||||||
|
builder:
|
||||||
|
(context) => _FadeOverlay(
|
||||||
|
key: key,
|
||||||
|
duration: const Duration(milliseconds: 150),
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
builder: (context, animation) {
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
Positioned.fill(
|
||||||
|
child: FadeTransition(
|
||||||
|
opacity: animation,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: barrierDismissible ? () => close(null) : null,
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
child: const ColoredBox(color: Colors.black54),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Center(
|
||||||
|
child: SlideTransition(
|
||||||
|
position: Tween<Offset>(
|
||||||
|
begin: const Offset(0, 0.05),
|
||||||
|
end: Offset.zero,
|
||||||
|
).animate(animation),
|
||||||
|
child: FadeTransition(
|
||||||
|
opacity: animation,
|
||||||
|
child: builder(context, close),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
globalOverlay.currentState!.insert(entry);
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
void showErrorAlert(dynamic err) {
|
||||||
|
if (err is Error) {
|
||||||
|
talker.error('Something went wrong...', err, err.stackTrace);
|
||||||
|
}
|
||||||
|
final text = switch (err) {
|
||||||
|
String _ => err,
|
||||||
|
DioException _ => _parseRemoteError(err),
|
||||||
|
Exception _ => err.toString(),
|
||||||
|
_ => err.toString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
showOverlayDialog<void>(
|
||||||
|
builder:
|
||||||
|
(context, close) => AlertDialog(
|
||||||
|
title: Text('somethingWentWrong'.tr()),
|
||||||
|
content: Text(text),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => close(null),
|
||||||
|
child: Text(MaterialLocalizations.of(context).okButtonLabel),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void showInfoAlert(String message, String title) {
|
||||||
|
showOverlayDialog<void>(
|
||||||
|
builder:
|
||||||
|
(context, close) => AlertDialog(
|
||||||
|
title: Text(title),
|
||||||
|
content: Text(message),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => close(null),
|
||||||
|
child: Text(MaterialLocalizations.of(context).okButtonLabel),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> showConfirmAlert(String message, String title) async {
|
||||||
|
final result = await showOverlayDialog<bool>(
|
||||||
|
builder:
|
||||||
|
(context, close) => AlertDialog(
|
||||||
|
title: Text(title),
|
||||||
|
content: Text(message),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => close(false),
|
||||||
|
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => close(true),
|
||||||
|
child: Text(MaterialLocalizations.of(context).okButtonLabel),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return result ?? false;
|
||||||
|
}
|
||||||
|
|||||||
@@ -130,6 +130,17 @@ class _AppWrapperState extends ConsumerState<AppWrapper>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Special handling for share intent deep links
|
||||||
|
// Share intents are handled by SharingIntentService showing a modal,
|
||||||
|
// not by routing to a page
|
||||||
|
if (path == '/share') {
|
||||||
|
if (!kIsWeb &&
|
||||||
|
(Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
|
||||||
|
windowManager.show();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final router = ref.read(routerProvider);
|
final router = ref.read(routerProvider);
|
||||||
if (uri.queryParameters.isNotEmpty) {
|
if (uri.queryParameters.isNotEmpty) {
|
||||||
path =
|
path =
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/chat.dart';
|
import 'package:island/models/chat.dart';
|
||||||
@@ -28,12 +28,12 @@ Future<SnRealtimeCall?> ongoingCall(Ref ref, String roomId) async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class AudioCallButton extends HookConsumerWidget {
|
class AudioCallButton extends HookConsumerWidget {
|
||||||
final String roomId;
|
final SnChatRoom room;
|
||||||
const AudioCallButton({super.key, required this.roomId});
|
const AudioCallButton({super.key, required this.room});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final ongoingCall = ref.watch(ongoingCallProvider(roomId));
|
final ongoingCall = ref.watch(ongoingCallProvider(room.id));
|
||||||
final callState = ref.watch(callNotifierProvider);
|
final callState = ref.watch(callNotifierProvider);
|
||||||
final callNotifier = ref.read(callNotifierProvider.notifier);
|
final callNotifier = ref.read(callNotifierProvider.notifier);
|
||||||
final isLoading = useState(false);
|
final isLoading = useState(false);
|
||||||
@@ -42,10 +42,9 @@ class AudioCallButton extends HookConsumerWidget {
|
|||||||
Future<void> handleJoin() async {
|
Future<void> handleJoin() async {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
try {
|
try {
|
||||||
await apiClient.post('/sphere/chat/realtime/$roomId');
|
await apiClient.post('/sphere/chat/realtime/${room.id}');
|
||||||
if (context.mounted) {
|
// Just join the room, the overlay will handle the UI
|
||||||
context.pushNamed('chatCall', pathParameters: {'id': roomId});
|
await callNotifier.joinRoom(room);
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showErrorAlert(e);
|
showErrorAlert(e);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -56,7 +55,7 @@ class AudioCallButton extends HookConsumerWidget {
|
|||||||
Future<void> handleEnd() async {
|
Future<void> handleEnd() async {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
try {
|
try {
|
||||||
await apiClient.delete('/sphere/chat/realtime/$roomId');
|
await apiClient.delete('/sphere/chat/realtime/${room.id}');
|
||||||
callNotifier.dispose(); // Clean up call resources
|
callNotifier.dispose(); // Clean up call resources
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showErrorAlert(e);
|
showErrorAlert(e);
|
||||||
@@ -94,9 +93,14 @@ class AudioCallButton extends HookConsumerWidget {
|
|||||||
return IconButton(
|
return IconButton(
|
||||||
icon: const Icon(Icons.call),
|
icon: const Icon(Icons.call),
|
||||||
tooltip: 'Join Ongoing Call',
|
tooltip: 'Join Ongoing Call',
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
if (context.mounted) {
|
isLoading.value = true;
|
||||||
context.pushNamed('chatCall', pathParameters: {'id': roomId});
|
try {
|
||||||
|
await callNotifier.joinRoom(room);
|
||||||
|
} catch (e) {
|
||||||
|
showErrorAlert(e);
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -105,7 +109,7 @@ class AudioCallButton extends HookConsumerWidget {
|
|||||||
// Show join/start call button
|
// Show join/start call button
|
||||||
return IconButton(
|
return IconButton(
|
||||||
icon: const Icon(Icons.call),
|
icon: const Icon(Icons.call),
|
||||||
tooltip: 'Start/Join Call',
|
tooltip: 'Start Call',
|
||||||
onPressed: handleJoin,
|
onPressed: handleJoin,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
80
lib/widgets/chat/call_content.dart
Normal file
80
lib/widgets/chat/call_content.dart
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:island/pods/chat/call.dart';
|
||||||
|
import 'package:island/widgets/chat/call_participant_tile.dart';
|
||||||
|
import 'package:livekit_client/livekit_client.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
|
||||||
|
class CallContent extends HookConsumerWidget {
|
||||||
|
const CallContent({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final callState = ref.watch(callNotifierProvider);
|
||||||
|
final callNotifier = ref.watch(callNotifierProvider.notifier);
|
||||||
|
|
||||||
|
if (!callState.isConnected) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
if (callNotifier.participants.isEmpty) {
|
||||||
|
return const Center(child: Text('No participants in call'));
|
||||||
|
}
|
||||||
|
|
||||||
|
final participants = callNotifier.participants;
|
||||||
|
final allAudioOnly = participants.every(
|
||||||
|
(p) =>
|
||||||
|
!(p.hasVideo &&
|
||||||
|
p.remoteParticipant.trackPublications.values.any(
|
||||||
|
(pub) =>
|
||||||
|
pub.track != null &&
|
||||||
|
pub.kind == TrackType.VIDEO &&
|
||||||
|
!pub.muted &&
|
||||||
|
!pub.isDisposed,
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (allAudioOnly) {
|
||||||
|
// Audio-only: show avatars in a compact row
|
||||||
|
return Center(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: Wrap(
|
||||||
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
|
alignment: WrapAlignment.center,
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 8,
|
||||||
|
children: [
|
||||||
|
for (final live in participants)
|
||||||
|
SpeakingRippleAvatar(
|
||||||
|
live: live,
|
||||||
|
size: 72,
|
||||||
|
).padding(horizontal: 4),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show all participants in a responsive grid
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
// Calculate width for responsive 2-column layout
|
||||||
|
final itemWidth = (constraints.maxWidth / 2) - 16;
|
||||||
|
|
||||||
|
return Wrap(
|
||||||
|
alignment: WrapAlignment.center,
|
||||||
|
runAlignment: WrapAlignment.center,
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 8,
|
||||||
|
children: [
|
||||||
|
for (final participant in participants)
|
||||||
|
SizedBox(
|
||||||
|
width: itemWidth,
|
||||||
|
child: CallParticipantTile(live: participant),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,18 @@
|
|||||||
|
import 'package:animations/animations.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:island/models/account.dart';
|
||||||
|
import 'package:island/models/chat.dart';
|
||||||
import 'package:island/pods/chat/call.dart';
|
import 'package:island/pods/chat/call.dart';
|
||||||
|
import 'package:island/pods/userinfo.dart';
|
||||||
|
import 'package:island/screens/chat/call.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
|
import 'package:island/widgets/chat/call_button.dart';
|
||||||
|
import 'package:island/widgets/chat/call_content.dart';
|
||||||
import 'package:island/widgets/chat/call_participant_tile.dart';
|
import 'package:island/widgets/chat/call_participant_tile.dart';
|
||||||
import 'package:island/widgets/content/sheet.dart';
|
import 'package:island/widgets/content/sheet.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
@@ -13,7 +20,8 @@ import 'package:styled_widget/styled_widget.dart';
|
|||||||
import 'package:livekit_client/livekit_client.dart';
|
import 'package:livekit_client/livekit_client.dart';
|
||||||
|
|
||||||
class CallControlsBar extends HookConsumerWidget {
|
class CallControlsBar extends HookConsumerWidget {
|
||||||
const CallControlsBar({super.key});
|
final bool isCompact;
|
||||||
|
const CallControlsBar({super.key, this.isCompact = false});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@@ -21,11 +29,14 @@ class CallControlsBar extends HookConsumerWidget {
|
|||||||
final callNotifier = ref.read(callNotifierProvider.notifier);
|
final callNotifier = ref.read(callNotifierProvider.notifier);
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: isCompact ? 12 : 20,
|
||||||
|
vertical: isCompact ? 8 : 16,
|
||||||
|
),
|
||||||
child: Wrap(
|
child: Wrap(
|
||||||
alignment: WrapAlignment.center,
|
alignment: WrapAlignment.center,
|
||||||
runSpacing: 16,
|
runSpacing: isCompact ? 12 : 16,
|
||||||
spacing: 16,
|
spacing: isCompact ? 12 : 16,
|
||||||
children: [
|
children: [
|
||||||
_buildCircularButtonWithDropdown(
|
_buildCircularButtonWithDropdown(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -73,12 +84,15 @@ class CallControlsBar extends HookConsumerWidget {
|
|||||||
(innerContext) => Column(
|
(innerContext) => Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
|
const Gap(24),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Symbols.logout, fill: 1),
|
leading: const Icon(Symbols.logout, fill: 1),
|
||||||
title: Text('callLeave').tr(),
|
title: Text('callLeave').tr(),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
callNotifier.disconnect();
|
callNotifier.disconnect();
|
||||||
|
if (Navigator.of(context).canPop()) {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
Navigator.of(innerContext).pop();
|
Navigator.of(innerContext).pop();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -96,7 +110,9 @@ class CallControlsBar extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
callNotifier.dispose();
|
callNotifier.dispose();
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
|
if (Navigator.of(context).canPop()) {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
Navigator.of(innerContext).pop();
|
Navigator.of(innerContext).pop();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -124,12 +140,14 @@ class CallControlsBar extends HookConsumerWidget {
|
|||||||
required Color backgroundColor,
|
required Color backgroundColor,
|
||||||
Color? iconColor,
|
Color? iconColor,
|
||||||
}) {
|
}) {
|
||||||
|
final size = isCompact ? 40.0 : 56.0;
|
||||||
|
final iconSize = isCompact ? 20.0 : 24.0;
|
||||||
return Container(
|
return Container(
|
||||||
width: 56,
|
width: size,
|
||||||
height: 56,
|
height: size,
|
||||||
decoration: BoxDecoration(color: backgroundColor, shape: BoxShape.circle),
|
decoration: BoxDecoration(color: backgroundColor, shape: BoxShape.circle),
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
icon: Icon(icon, color: iconColor ?? Colors.white, size: 24),
|
icon: Icon(icon, color: iconColor ?? Colors.white, size: iconSize),
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -145,29 +163,38 @@ class CallControlsBar extends HookConsumerWidget {
|
|||||||
Color? iconColor,
|
Color? iconColor,
|
||||||
String? deviceType, // 'videoinput' or 'audioinput'
|
String? deviceType, // 'videoinput' or 'audioinput'
|
||||||
}) {
|
}) {
|
||||||
|
final size = isCompact ? 40.0 : 56.0;
|
||||||
|
final iconSize = isCompact ? 20.0 : 24.0;
|
||||||
return Stack(
|
return Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
width: 56,
|
width: size,
|
||||||
height: 56,
|
height: size,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: backgroundColor,
|
color: backgroundColor,
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
icon: Icon(icon, color: iconColor ?? Colors.white, size: 24),
|
icon: Icon(icon, color: iconColor ?? Colors.white, size: iconSize),
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (hasDropdown && deviceType != null)
|
if (hasDropdown && deviceType != null)
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 4,
|
bottom: 0,
|
||||||
right: 4,
|
right: isCompact ? 0 : -4,
|
||||||
child: GestureDetector(
|
child: Material(
|
||||||
onTap: () => _showDeviceSelectionDialog(context, ref, deviceType),
|
color:
|
||||||
|
Colors
|
||||||
|
.transparent, // Make Material transparent to show underlying color
|
||||||
|
child: InkWell(
|
||||||
|
onTap:
|
||||||
|
() => _showDeviceSelectionDialog(context, ref, deviceType),
|
||||||
|
borderRadius: BorderRadius.circular((isCompact ? 16 : 24) / 2),
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 16,
|
width: isCompact ? 16 : 24,
|
||||||
height: 16,
|
height: isCompact ? 16 : 24,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: backgroundColor.withOpacity(0.8),
|
color: backgroundColor.withOpacity(0.8),
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
@@ -179,7 +206,8 @@ class CallControlsBar extends HookConsumerWidget {
|
|||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.arrow_drop_down,
|
Icons.arrow_drop_down,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
size: 12,
|
size: isCompact ? 12 : 20,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -279,34 +307,150 @@ class CallControlsBar extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class CallOverlayBar extends HookConsumerWidget {
|
class CallOverlayBar extends HookConsumerWidget {
|
||||||
const CallOverlayBar({super.key});
|
final SnChatRoom room;
|
||||||
|
const CallOverlayBar({super.key, required this.room});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final callState = ref.watch(callNotifierProvider);
|
final callState = ref.watch(callNotifierProvider);
|
||||||
final callNotifier = ref.read(callNotifierProvider.notifier);
|
final callNotifier = ref.read(callNotifierProvider.notifier);
|
||||||
// Only show if connected and not on the call screen
|
final ongoingCall = ref.watch(ongoingCallProvider(room.id));
|
||||||
if (!callState.isConnected) return const SizedBox.shrink();
|
|
||||||
|
|
||||||
|
// State for overlay mode: compact or preview
|
||||||
|
// Default to true (preview mode) so user sees video immediately after joining
|
||||||
|
final isExpanded = useState(true);
|
||||||
|
|
||||||
|
Widget child;
|
||||||
|
if (callState.isConnected) {
|
||||||
|
child = _buildActiveCallOverlay(
|
||||||
|
context,
|
||||||
|
ref,
|
||||||
|
callState,
|
||||||
|
callNotifier,
|
||||||
|
isExpanded,
|
||||||
|
);
|
||||||
|
} else if (ongoingCall.value != null) {
|
||||||
|
child = _buildJoinPrompt(context, ref);
|
||||||
|
} else {
|
||||||
|
child = const SizedBox.shrink(key: ValueKey('empty'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return AnimatedSize(
|
||||||
|
duration: const Duration(milliseconds: 150),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
child: AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 150),
|
||||||
|
layoutBuilder: (currentChild, previousChildren) {
|
||||||
|
return Stack(
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
children: <Widget>[
|
||||||
|
...previousChildren,
|
||||||
|
if (currentChild != null) currentChild,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildJoinPrompt(BuildContext context, WidgetRef ref) {
|
||||||
|
final isLoading = useState(false);
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
key: const ValueKey('join_prompt'),
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.videocam,
|
||||||
|
color: Theme.of(context).colorScheme.onPrimary,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(12),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text('Call in progress').bold(),
|
||||||
|
Text('Tap to join', style: Theme.of(context).textTheme.bodySmall),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
if (isLoading.value)
|
||||||
|
const SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
).padding(right: 8)
|
||||||
|
else
|
||||||
|
FilledButton.icon(
|
||||||
|
onPressed: () async {
|
||||||
|
isLoading.value = true;
|
||||||
|
try {
|
||||||
|
// Just join the room, don't navigate
|
||||||
|
await ref.read(callNotifierProvider.notifier).joinRoom(room);
|
||||||
|
} catch (e) {
|
||||||
|
showErrorAlert(e);
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.call, size: 18),
|
||||||
|
label: const Text('Join'),
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(all: 12),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getChatRoomName(SnChatRoom? room, SnAccount currentUser) {
|
||||||
|
if (room == null) return 'unnamed'.tr();
|
||||||
|
return room.name ??
|
||||||
|
(room.members ?? [])
|
||||||
|
.where((element) => element.id != currentUser.id)
|
||||||
|
.map((element) => element.account.nick)
|
||||||
|
.first;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildActiveCallOverlay(
|
||||||
|
BuildContext context,
|
||||||
|
WidgetRef ref,
|
||||||
|
CallState callState,
|
||||||
|
CallNotifier callNotifier,
|
||||||
|
ValueNotifier<bool> isExpanded,
|
||||||
|
) {
|
||||||
final lastSpeaker =
|
final lastSpeaker =
|
||||||
callNotifier.participants
|
callNotifier.participants
|
||||||
.where(
|
.where(
|
||||||
(element) => element.remoteParticipant.lastSpokeAt != null,
|
(element) => element.remoteParticipant.lastSpokeAt != null,
|
||||||
)
|
)
|
||||||
.isEmpty
|
.isEmpty
|
||||||
? callNotifier.participants.first
|
? callNotifier.participants.firstOrNull
|
||||||
: callNotifier.participants
|
: callNotifier.participants
|
||||||
.where(
|
.where(
|
||||||
(element) => element.remoteParticipant.lastSpokeAt != null,
|
(element) => element.remoteParticipant.lastSpokeAt != null,
|
||||||
)
|
)
|
||||||
.fold(
|
.fold(
|
||||||
callNotifier.participants.first,
|
callNotifier.participants.firstOrNull,
|
||||||
(value, element) =>
|
(value, element) =>
|
||||||
element.remoteParticipant.lastSpokeAt != null &&
|
element.remoteParticipant.lastSpokeAt != null &&
|
||||||
(value.remoteParticipant.lastSpokeAt == null ||
|
(value?.remoteParticipant.lastSpokeAt == null ||
|
||||||
element.remoteParticipant.lastSpokeAt!
|
element.remoteParticipant.lastSpokeAt!
|
||||||
.compareTo(
|
.compareTo(
|
||||||
value
|
value!
|
||||||
.remoteParticipant
|
.remoteParticipant
|
||||||
.lastSpokeAt!,
|
.lastSpokeAt!,
|
||||||
) >
|
) >
|
||||||
@@ -315,11 +459,76 @@ class CallOverlayBar extends HookConsumerWidget {
|
|||||||
: value,
|
: value,
|
||||||
);
|
);
|
||||||
|
|
||||||
final actionButtonStyle = ButtonStyle(
|
if (lastSpeaker == null) {
|
||||||
minimumSize: const MaterialStatePropertyAll(Size(24, 24)),
|
return const SizedBox.shrink(key: ValueKey('active_waiting'));
|
||||||
);
|
}
|
||||||
|
|
||||||
|
final userInfo = ref.watch(userInfoProvider).value!;
|
||||||
|
|
||||||
|
// Preview Mode (Expanded)
|
||||||
|
if (isExpanded.value) {
|
||||||
|
return Card(
|
||||||
|
key: const ValueKey('active_expanded'),
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
// Header
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Gap(4),
|
||||||
|
Text(_getChatRoomName(callNotifier.chatRoom, userInfo)),
|
||||||
|
const Gap(4),
|
||||||
|
Text(formatDuration(callState.duration)).bold(),
|
||||||
|
const Spacer(),
|
||||||
|
OpenContainer(
|
||||||
|
closedElevation: 0,
|
||||||
|
closedColor: Colors.transparent,
|
||||||
|
openColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
middleColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
openBuilder: (context, action) => CallScreen(room: room),
|
||||||
|
closedBuilder:
|
||||||
|
(context, openContainer) => IconButton(
|
||||||
|
visualDensity: const VisualDensity(
|
||||||
|
horizontal: -4,
|
||||||
|
vertical: -4,
|
||||||
|
),
|
||||||
|
icon: const Icon(Icons.fullscreen),
|
||||||
|
onPressed: openContainer,
|
||||||
|
tooltip: 'Full Screen',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
visualDensity: const VisualDensity(
|
||||||
|
horizontal: -4,
|
||||||
|
vertical: -4,
|
||||||
|
),
|
||||||
|
icon: const Icon(Icons.expand_less),
|
||||||
|
onPressed: () => isExpanded.value = false,
|
||||||
|
tooltip: 'Collapse',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 12, vertical: 8),
|
||||||
|
// Video Preview
|
||||||
|
Container(
|
||||||
|
height: 200,
|
||||||
|
width: double.infinity,
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||||
|
child: const CallContent(),
|
||||||
|
),
|
||||||
|
const CallControlsBar(
|
||||||
|
isCompact: true,
|
||||||
|
).padding(vertical: 8, horizontal: 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compact Mode
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
|
key: const ValueKey('active_collapsed'),
|
||||||
|
onTap: () => isExpanded.value = true,
|
||||||
child: Card(
|
child: Card(
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -328,12 +537,7 @@ class CallOverlayBar extends HookConsumerWidget {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Builder(
|
SizedBox(
|
||||||
builder: (context) {
|
|
||||||
if (callNotifier.localParticipant == null) {
|
|
||||||
return CircularProgressIndicator().center();
|
|
||||||
}
|
|
||||||
return SizedBox(
|
|
||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
child:
|
child:
|
||||||
@@ -341,14 +545,19 @@ class CallOverlayBar extends HookConsumerWidget {
|
|||||||
live: lastSpeaker,
|
live: lastSpeaker,
|
||||||
size: 36,
|
size: 36,
|
||||||
).center(),
|
).center(),
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('@${lastSpeaker.participant.identity}').bold(),
|
Text('@${lastSpeaker.participant.identity}').bold(),
|
||||||
|
Row(
|
||||||
|
spacing: 4,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
_getChatRoomName(callNotifier.chatRoom, userInfo),
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
Text(
|
Text(
|
||||||
formatDuration(callState.duration),
|
formatDuration(callState.duration),
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
@@ -357,45 +566,26 @@ class CallOverlayBar extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
callState.isMicrophoneEnabled ? Icons.mic : Icons.mic_off,
|
callState.isMicrophoneEnabled ? Icons.mic : Icons.mic_off,
|
||||||
|
size: 20,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
callNotifier.toggleMicrophone();
|
callNotifier.toggleMicrophone();
|
||||||
},
|
},
|
||||||
style: actionButtonStyle,
|
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(
|
icon: const Icon(Icons.expand_more),
|
||||||
callState.isCameraEnabled ? Icons.videocam : Icons.videocam_off,
|
onPressed: () => isExpanded.value = true,
|
||||||
),
|
tooltip: 'Expand',
|
||||||
onPressed: () {
|
|
||||||
callNotifier.toggleCamera();
|
|
||||||
},
|
|
||||||
style: actionButtonStyle,
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(
|
|
||||||
callState.isScreenSharing
|
|
||||||
? Icons.stop_screen_share
|
|
||||||
: Icons.screen_share,
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
callNotifier.toggleScreenShare(context);
|
|
||||||
},
|
|
||||||
style: actionButtonStyle,
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
).padding(all: 16),
|
).padding(all: 12),
|
||||||
),
|
),
|
||||||
onTap: () {
|
|
||||||
context.pushNamed(
|
|
||||||
'chatCall',
|
|
||||||
pathParameters: {'id': callNotifier.roomId!},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,59 @@ import 'package:livekit_client/livekit_client.dart';
|
|||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
|
||||||
|
class SpeakingRipple extends StatelessWidget {
|
||||||
|
final double size;
|
||||||
|
final double audioLevel;
|
||||||
|
final bool isSpeaking;
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const SpeakingRipple({
|
||||||
|
super.key,
|
||||||
|
required this.size,
|
||||||
|
required this.audioLevel,
|
||||||
|
required this.isSpeaking,
|
||||||
|
required this.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final avatarRadius = size / 2;
|
||||||
|
final clampedLevel = audioLevel.clamp(0.0, 1.0);
|
||||||
|
final rippleRadius = avatarRadius + clampedLevel * (size * 0.333);
|
||||||
|
|
||||||
|
return SizedBox(
|
||||||
|
width: size + 8,
|
||||||
|
height: size + 8,
|
||||||
|
child: TweenAnimationBuilder<double>(
|
||||||
|
tween: Tween<double>(
|
||||||
|
begin: avatarRadius,
|
||||||
|
end: isSpeaking ? rippleRadius : avatarRadius,
|
||||||
|
),
|
||||||
|
duration: const Duration(milliseconds: 250),
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
builder: (context, animatedRadius, child) {
|
||||||
|
return Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
if (isSpeaking)
|
||||||
|
Container(
|
||||||
|
width: animatedRadius * 2,
|
||||||
|
height: animatedRadius * 2,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: Colors.green.withOpacity(0.75 + 0.25 * clampedLevel),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child!,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: SizedBox(width: size, height: size, child: child),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class SpeakingRippleAvatar extends HookConsumerWidget {
|
class SpeakingRippleAvatar extends HookConsumerWidget {
|
||||||
final CallParticipantLive live;
|
final CallParticipantLive live;
|
||||||
final double size;
|
final double size;
|
||||||
@@ -18,37 +71,17 @@ class SpeakingRippleAvatar extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final account = ref.watch(accountProvider(live.participant.identity));
|
final account = ref.watch(accountProvider(live.participant.identity));
|
||||||
|
|
||||||
final avatarRadius = size / 2;
|
return SpeakingRipple(
|
||||||
final clampedLevel = live.remoteParticipant.audioLevel.clamp(0.0, 1.0);
|
size: size,
|
||||||
final rippleRadius = avatarRadius + clampedLevel * (size * 0.333);
|
audioLevel: live.remoteParticipant.audioLevel,
|
||||||
return SizedBox(
|
isSpeaking: live.remoteParticipant.isSpeaking,
|
||||||
width: size + 8,
|
child: Stack(
|
||||||
height: size + 8,
|
|
||||||
child: TweenAnimationBuilder<double>(
|
|
||||||
tween: Tween<double>(
|
|
||||||
begin: avatarRadius,
|
|
||||||
end: live.remoteParticipant.isSpeaking ? rippleRadius : avatarRadius,
|
|
||||||
),
|
|
||||||
duration: const Duration(milliseconds: 250),
|
|
||||||
curve: Curves.easeOut,
|
|
||||||
builder: (context, animatedRadius, child) {
|
|
||||||
return Stack(
|
|
||||||
alignment: Alignment.center,
|
|
||||||
children: [
|
children: [
|
||||||
if (live.remoteParticipant.isSpeaking)
|
|
||||||
Container(
|
|
||||||
width: animatedRadius * 2,
|
|
||||||
height: animatedRadius * 2,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
color: Colors.green.withOpacity(0.75 + 0.25 * clampedLevel),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(
|
Container(
|
||||||
width: size,
|
width: size,
|
||||||
height: size,
|
height: size,
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
decoration: BoxDecoration(shape: BoxShape.circle),
|
decoration: const BoxDecoration(shape: BoxShape.circle),
|
||||||
child: account.when(
|
child: account.when(
|
||||||
data:
|
data:
|
||||||
(value) => CallParticipantGestureDetector(
|
(value) => CallParticipantGestureDetector(
|
||||||
@@ -72,25 +105,24 @@ class SpeakingRippleAvatar extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
if (live.remoteParticipant.isMuted)
|
if (live.remoteParticipant.isMuted)
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 4,
|
bottom: 0,
|
||||||
right: 4,
|
right: 0,
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 20,
|
width: 24,
|
||||||
height: 20,
|
height: 24,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.red,
|
color: Colors.red,
|
||||||
borderRadius: BorderRadius.all(Radius.circular(10)),
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: Colors.white, width: 2),
|
||||||
),
|
),
|
||||||
child: const Icon(
|
child: const Icon(
|
||||||
Symbols.mic_off,
|
Symbols.mic_off,
|
||||||
size: 14,
|
size: 14,
|
||||||
fill: 1,
|
color: Colors.white,
|
||||||
).padding(left: 1.5, top: 1.5),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -103,6 +135,8 @@ class CallParticipantTile extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final userInfo = ref.watch(accountProvider(live.participant.name));
|
||||||
|
|
||||||
final hasVideo =
|
final hasVideo =
|
||||||
live.hasVideo &&
|
live.hasVideo &&
|
||||||
live.remoteParticipant.trackPublications.values
|
live.remoteParticipant.trackPublications.values
|
||||||
@@ -110,12 +144,45 @@ class CallParticipantTile extends HookConsumerWidget {
|
|||||||
.isNotEmpty;
|
.isNotEmpty;
|
||||||
|
|
||||||
if (hasVideo) {
|
if (hasVideo) {
|
||||||
return Stack(
|
return Padding(
|
||||||
fit: StackFit.loose,
|
padding: const EdgeInsets.all(8),
|
||||||
children: [
|
child: LayoutBuilder(
|
||||||
AspectRatio(
|
builder: (context, constraints) {
|
||||||
|
// Use the smaller dimension to determine the "size" for the ripple calculation
|
||||||
|
// effectively making the ripple relative to the tile size.
|
||||||
|
// However, for a rectangular video, we might want a different approach.
|
||||||
|
// The user asked for "speaking ripple to the video as well".
|
||||||
|
// If we use the extracted SpeakingRipple, it expects a size and assumes a circle.
|
||||||
|
// We need to adapt it or create a rectangular version.
|
||||||
|
// Given the "image" likely shows a rectangular video with rounded corners,
|
||||||
|
// let's create a specific wrapper for the video tile that adds a border/glow when speaking.
|
||||||
|
|
||||||
|
final isSpeaking = live.remoteParticipant.isSpeaking;
|
||||||
|
final audioLevel = live.remoteParticipant.audioLevel;
|
||||||
|
|
||||||
|
return AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
border: Border.all(
|
||||||
|
color:
|
||||||
|
isSpeaking
|
||||||
|
? Colors.green.withOpacity(
|
||||||
|
0.5 + 0.5 * audioLevel.clamp(0.0, 1.0),
|
||||||
|
)
|
||||||
|
: Theme.of(context).colorScheme.outlineVariant,
|
||||||
|
width: isSpeaking ? 4 : 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: AspectRatio(
|
||||||
aspectRatio: 16 / 9,
|
aspectRatio: 16 / 9,
|
||||||
child: VideoTrackRenderer(
|
child: Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
VideoTrackRenderer(
|
||||||
live.remoteParticipant.trackPublications.values
|
live.remoteParticipant.trackPublications.values
|
||||||
.where((track) => track.kind == TrackType.VIDEO)
|
.where((track) => track.kind == TrackType.VIDEO)
|
||||||
.first
|
.first
|
||||||
@@ -123,29 +190,46 @@ class CallParticipantTile extends HookConsumerWidget {
|
|||||||
as VideoTrack,
|
as VideoTrack,
|
||||||
renderMode: VideoRenderMode.platformView,
|
renderMode: VideoRenderMode.platformView,
|
||||||
),
|
),
|
||||||
),
|
|
||||||
Positioned(
|
Positioned(
|
||||||
left: 8,
|
left: 8,
|
||||||
right: 8,
|
|
||||||
bottom: 8,
|
bottom: 8,
|
||||||
child: Text(
|
child: Container(
|
||||||
'@${live.participant.name}',
|
padding: const EdgeInsets.symmetric(
|
||||||
textAlign: TextAlign.center,
|
horizontal: 8,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black.withOpacity(0.6),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (live.remoteParticipant.isMuted)
|
||||||
|
const Icon(
|
||||||
|
Symbols.mic_off,
|
||||||
|
size: 14,
|
||||||
|
color: Colors.redAccent,
|
||||||
|
).padding(right: 4),
|
||||||
|
Text(
|
||||||
|
userInfo.value?.nick ?? live.participant.name,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 12,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
shadows: [
|
fontWeight: FontWeight.w500,
|
||||||
BoxShadow(
|
),
|
||||||
color: Colors.black54,
|
|
||||||
offset: Offset(1, 1),
|
|
||||||
spreadRadius: 8,
|
|
||||||
blurRadius: 8,
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return SpeakingRippleAvatar(size: 84, live: live);
|
return SpeakingRippleAvatar(size: 84, live: live);
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ void _insertPlaceholder(TextEditingController controller, String placeholder) {
|
|||||||
|
|
||||||
const kInputDrawerExpandedHeight = 180.0;
|
const kInputDrawerExpandedHeight = 180.0;
|
||||||
|
|
||||||
|
const kExpandedSectionTabHeight = 32.0;
|
||||||
|
|
||||||
class _ExpandedSection extends StatelessWidget {
|
class _ExpandedSection extends StatelessWidget {
|
||||||
final TextEditingController messageController;
|
final TextEditingController messageController;
|
||||||
final SnPoll? selectedPoll;
|
final SnPoll? selectedPoll;
|
||||||
@@ -75,9 +77,23 @@ class _ExpandedSection extends StatelessWidget {
|
|||||||
length: 2,
|
length: 2,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
TabBar(
|
PreferredSize(
|
||||||
splashBorderRadius: const BorderRadius.all(Radius.circular(40)),
|
preferredSize: const Size.fromHeight(kExpandedSectionTabHeight),
|
||||||
tabs: [Tab(text: 'Features'), Tab(text: 'Stickers')],
|
child: TabBar(
|
||||||
|
splashBorderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(40),
|
||||||
|
),
|
||||||
|
tabs: [
|
||||||
|
Tab(
|
||||||
|
text: 'features'.tr(),
|
||||||
|
height: kExpandedSectionTabHeight,
|
||||||
|
),
|
||||||
|
Tab(
|
||||||
|
text: 'stickers'.tr(),
|
||||||
|
height: kExpandedSectionTabHeight,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: kInputDrawerExpandedHeight,
|
height: kInputDrawerExpandedHeight,
|
||||||
@@ -248,6 +264,7 @@ class ChatInput extends HookConsumerWidget {
|
|||||||
|
|
||||||
void send() {
|
void send() {
|
||||||
inputFocusNode.requestFocus();
|
inputFocusNode.requestFocus();
|
||||||
|
if (isExpanded.value) isExpanded.value = false;
|
||||||
onSend.call();
|
onSend.call();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
import 'package:dio/dio.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flutter_platform_alert/flutter_platform_alert.dart';
|
|
||||||
import 'package:island/talker.dart';
|
|
||||||
|
|
||||||
String _parseRemoteError(DioException err) {
|
|
||||||
String? message;
|
|
||||||
if (err.response?.data is String) {
|
|
||||||
message = err.response?.data;
|
|
||||||
} else if (err.response?.data?['message'] != null) {
|
|
||||||
message = <String?>[
|
|
||||||
err.response?.data?['message']?.toString(),
|
|
||||||
err.response?.data?['detail']?.toString(),
|
|
||||||
].where((e) => e != null).cast<String>().map((e) => e.trim()).join('\n');
|
|
||||||
} else if (err.response?.data?['errors'] != null) {
|
|
||||||
final errors = err.response?.data['errors'] as Map<String, dynamic>;
|
|
||||||
message = errors.values
|
|
||||||
.map(
|
|
||||||
(ele) =>
|
|
||||||
(ele as List<dynamic>).map((ele) => ele.toString()).join('\n'),
|
|
||||||
)
|
|
||||||
.join('\n');
|
|
||||||
}
|
|
||||||
if (message == null || message.isEmpty) message = err.response?.statusMessage;
|
|
||||||
message ??= err.message;
|
|
||||||
return message ?? err.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
void showErrorAlert(dynamic err) async {
|
|
||||||
if (err is Error) {
|
|
||||||
talker.error('Something went wrong...', err, err.stackTrace);
|
|
||||||
}
|
|
||||||
final text = switch (err) {
|
|
||||||
String _ => err,
|
|
||||||
DioException _ => _parseRemoteError(err),
|
|
||||||
Exception _ => err.toString(),
|
|
||||||
_ => err.toString(),
|
|
||||||
};
|
|
||||||
FlutterPlatformAlert.showAlert(
|
|
||||||
windowTitle: 'somethingWentWrong'.tr(),
|
|
||||||
text: text,
|
|
||||||
alertStyle: AlertButtonStyle.ok,
|
|
||||||
iconStyle: IconStyle.error,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void showInfoAlert(String message, String title) async {
|
|
||||||
FlutterPlatformAlert.showAlert(
|
|
||||||
windowTitle: title,
|
|
||||||
text: message,
|
|
||||||
alertStyle: AlertButtonStyle.ok,
|
|
||||||
iconStyle: IconStyle.information,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> showConfirmAlert(String message, String title) async {
|
|
||||||
final result = await FlutterPlatformAlert.showAlert(
|
|
||||||
windowTitle: title,
|
|
||||||
text: message,
|
|
||||||
alertStyle: AlertButtonStyle.okCancel,
|
|
||||||
iconStyle: IconStyle.question,
|
|
||||||
);
|
|
||||||
return result == AlertButton.okButton;
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
// ignore_for_file: avoid_web_libraries_in_flutter
|
|
||||||
|
|
||||||
import 'dart:js' as js;
|
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
|
|
||||||
String _parseRemoteError(DioException err) {
|
|
||||||
String? message;
|
|
||||||
if (err.response?.data is String) {
|
|
||||||
message = err.response?.data;
|
|
||||||
} else if (err.response?.data?['message'] != null) {
|
|
||||||
message = <String?>[
|
|
||||||
err.response?.data?['message']?.toString(),
|
|
||||||
err.response?.data?['detail']?.toString(),
|
|
||||||
].where((e) => e != null).cast<String>().map((e) => e.trim()).join('\n');
|
|
||||||
} else if (err.response?.data?['errors'] != null) {
|
|
||||||
final errors = err.response?.data['errors'] as Map<String, dynamic>;
|
|
||||||
message = errors.values
|
|
||||||
.map(
|
|
||||||
(ele) =>
|
|
||||||
(ele as List<dynamic>).map((ele) => ele.toString()).join('\n'),
|
|
||||||
)
|
|
||||||
.join('\n');
|
|
||||||
}
|
|
||||||
if (message == null || message.isEmpty) message = err.response?.statusMessage;
|
|
||||||
message ??= err.message;
|
|
||||||
return message ?? err.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
void showErrorAlert(dynamic err) async {
|
|
||||||
final text = switch (err) {
|
|
||||||
String _ => err,
|
|
||||||
DioException _ => _parseRemoteError(err),
|
|
||||||
Exception _ => err.toString(),
|
|
||||||
_ => err.toString(),
|
|
||||||
};
|
|
||||||
js.context.callMethod('swal', ['somethingWentWrong'.tr(), text, 'error']);
|
|
||||||
}
|
|
||||||
|
|
||||||
void showInfoAlert(String message, String title) async {
|
|
||||||
js.context.callMethod('swal', [title, message, 'info']);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> showConfirmAlert(String message, String title) async {
|
|
||||||
final result = await js.context.callMethod('swal', [
|
|
||||||
title,
|
|
||||||
message,
|
|
||||||
'question',
|
|
||||||
{'buttons': true},
|
|
||||||
]);
|
|
||||||
return result == true;
|
|
||||||
}
|
|
||||||
@@ -215,6 +215,8 @@ class CloudFileList extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
if (files.length == 1) {
|
if (files.length == 1) {
|
||||||
final isImage = files.first.mimeType?.startsWith('image') ?? false;
|
final isImage = files.first.mimeType?.startsWith('image') ?? false;
|
||||||
|
final isAudio = files.first.mimeType?.startsWith('audio') ?? false;
|
||||||
|
final ratio = files.first.fileMeta?['ratio'] as num?;
|
||||||
final widgetItem = ClipRRect(
|
final widgetItem = ClipRRect(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
child: _CloudFileListEntry(
|
child: _CloudFileListEntry(
|
||||||
@@ -242,7 +244,15 @@ class CloudFileList extends HookConsumerWidget {
|
|||||||
minWidth: minWidth ?? 0,
|
minWidth: minWidth ?? 0,
|
||||||
maxWidth: files.length == 1 ? maxWidth : double.infinity,
|
maxWidth: files.length == 1 ? maxWidth : double.infinity,
|
||||||
),
|
),
|
||||||
child: IntrinsicWidth(child: IntrinsicHeight(child: widgetItem)),
|
child:
|
||||||
|
(ratio == null && isImage)
|
||||||
|
? IntrinsicWidth(child: IntrinsicHeight(child: widgetItem))
|
||||||
|
: (ratio == null && isAudio)
|
||||||
|
? IntrinsicHeight(child: widgetItem)
|
||||||
|
: AspectRatio(
|
||||||
|
aspectRatio: ratio?.toDouble() ?? 1,
|
||||||
|
child: widgetItem,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -408,8 +418,6 @@ class _CloudFileListEntry extends HookConsumerWidget {
|
|||||||
final lockedByMature = file.sensitiveMarks.isNotEmpty && !showMature.value;
|
final lockedByMature = file.sensitiveMarks.isNotEmpty && !showMature.value;
|
||||||
final meta = file.fileMeta is Map ? file.fileMeta as Map : const {};
|
final meta = file.fileMeta is Map ? file.fileMeta as Map : const {};
|
||||||
|
|
||||||
final ratio = meta['ratio'] as num?;
|
|
||||||
|
|
||||||
final fit = BoxFit.cover;
|
final fit = BoxFit.cover;
|
||||||
|
|
||||||
Widget bg = const SizedBox.shrink();
|
Widget bg = const SizedBox.shrink();
|
||||||
@@ -448,9 +456,7 @@ class _CloudFileListEntry extends HookConsumerWidget {
|
|||||||
fit: fit,
|
fit: fit,
|
||||||
useInternalGate: false,
|
useInternalGate: false,
|
||||||
))
|
))
|
||||||
: IntrinsicWidth(
|
: const SizedBox.shrink();
|
||||||
child: IntrinsicHeight(child: const SizedBox.shrink()),
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget overlays;
|
Widget overlays;
|
||||||
if (lockedByDS) {
|
if (lockedByDS) {
|
||||||
@@ -481,7 +487,7 @@ class _CloudFileListEntry extends HookConsumerWidget {
|
|||||||
onTap?.call();
|
onTap?.call();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: AspectRatio(aspectRatio: ratio?.toDouble() ?? 1, child: content),
|
child: content,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:gal/gal.dart';
|
import 'package:gal/gal.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/file.dart';
|
import 'package:island/models/file.dart';
|
||||||
import 'package:island/pods/config.dart';
|
import 'package:island/pods/config.dart';
|
||||||
@@ -171,6 +172,24 @@ class CloudFileLightbox extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
onPressed: showInfoSheet,
|
onPressed: showInfoSheet,
|
||||||
),
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
final router = GoRouter.of(context);
|
||||||
|
Navigator.of(context).pop(context);
|
||||||
|
Future(() {
|
||||||
|
router.pushNamed(
|
||||||
|
'fileDetail',
|
||||||
|
pathParameters: {'id': item.id},
|
||||||
|
extra: item,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: Icon(
|
||||||
|
Icons.more_horiz,
|
||||||
|
color: Colors.white,
|
||||||
|
shadows: shadow,
|
||||||
|
),
|
||||||
|
),
|
||||||
Spacer(),
|
Spacer(),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
|
|||||||
@@ -1,24 +1,17 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:file_saver/file_saver.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/file.dart';
|
import 'package:island/models/file.dart';
|
||||||
import 'package:island/pods/config.dart';
|
import 'package:island/pods/config.dart';
|
||||||
import 'package:island/pods/network.dart';
|
|
||||||
import 'package:island/services/time.dart';
|
import 'package:island/services/time.dart';
|
||||||
import 'package:island/utils/format.dart';
|
import 'package:island/utils/format.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:path/path.dart' show extension;
|
|
||||||
import 'package:path_provider/path_provider.dart';
|
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:island/widgets/data_saving_gate.dart';
|
import 'package:island/widgets/data_saving_gate.dart';
|
||||||
import 'package:island/widgets/content/file_info_sheet.dart';
|
|
||||||
|
|
||||||
import 'file_viewer_contents.dart';
|
import 'file_viewer_contents.dart';
|
||||||
import 'image.dart';
|
import 'image.dart';
|
||||||
@@ -66,34 +59,6 @@ class CloudFileWidget extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (item.mimeType == 'application/pdf') {
|
if (item.mimeType == 'application/pdf') {
|
||||||
Future<void> downloadFile() async {
|
|
||||||
try {
|
|
||||||
showSnackBar('Downloading file...');
|
|
||||||
|
|
||||||
final client = ref.read(apiClientProvider);
|
|
||||||
final tempDir = await getTemporaryDirectory();
|
|
||||||
var extName = extension(item.name).trim();
|
|
||||||
if (extName.isEmpty) {
|
|
||||||
extName = item.mimeType?.split('/').lastOrNull ?? 'pdf';
|
|
||||||
}
|
|
||||||
final filePath = '${tempDir.path}/${item.id}.$extName';
|
|
||||||
|
|
||||||
await client.download(
|
|
||||||
'/drive/files/${item.id}',
|
|
||||||
filePath,
|
|
||||||
queryParameters: {'original': true},
|
|
||||||
);
|
|
||||||
|
|
||||||
await FileSaver.instance.saveFile(
|
|
||||||
name: item.name.isEmpty ? '${item.id}.$extName' : item.name,
|
|
||||||
file: File(filePath),
|
|
||||||
);
|
|
||||||
showSnackBar('File saved to downloads');
|
|
||||||
} catch (e) {
|
|
||||||
showErrorAlert(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
height: 400,
|
height: 400,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -166,30 +131,20 @@ class CloudFileWidget extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Symbols.download,
|
Symbols.more_horiz,
|
||||||
color: Colors.white,
|
|
||||||
size: 16,
|
|
||||||
),
|
|
||||||
onPressed: downloadFile,
|
|
||||||
padding: EdgeInsets.all(4),
|
|
||||||
constraints: const BoxConstraints(),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(
|
|
||||||
Symbols.info,
|
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
size: 16,
|
size: 16,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
showModalBottomSheet(
|
context.pushNamed(
|
||||||
useRootNavigator: true,
|
'fileDetail',
|
||||||
context: context,
|
pathParameters: {'id': item.id},
|
||||||
isScrollControlled: true,
|
extra: item,
|
||||||
builder: (context) => FileInfoSheet(item: item),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
padding: EdgeInsets.all(4),
|
padding: EdgeInsets.all(4),
|
||||||
constraints: const BoxConstraints(),
|
constraints: const BoxConstraints(),
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -201,34 +156,6 @@ class CloudFileWidget extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (item.mimeType?.startsWith('text/') == true) {
|
if (item.mimeType?.startsWith('text/') == true) {
|
||||||
Future<void> downloadFile() async {
|
|
||||||
try {
|
|
||||||
showSnackBar('Downloading file...');
|
|
||||||
|
|
||||||
final client = ref.read(apiClientProvider);
|
|
||||||
final tempDir = await getTemporaryDirectory();
|
|
||||||
var extName = extension(item.name).trim();
|
|
||||||
if (extName.isEmpty) {
|
|
||||||
extName = item.mimeType?.split('/').lastOrNull ?? 'txt';
|
|
||||||
}
|
|
||||||
final filePath = '${tempDir.path}/${item.id}.$extName';
|
|
||||||
|
|
||||||
await client.download(
|
|
||||||
'/drive/files/${item.id}',
|
|
||||||
filePath,
|
|
||||||
queryParameters: {'original': true},
|
|
||||||
);
|
|
||||||
|
|
||||||
await FileSaver.instance.saveFile(
|
|
||||||
name: item.name.isEmpty ? '${item.id}.$extName' : item.name,
|
|
||||||
file: File(filePath),
|
|
||||||
);
|
|
||||||
showSnackBar('File saved to downloads');
|
|
||||||
} catch (e) {
|
|
||||||
showErrorAlert(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
height: 400,
|
height: 400,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -304,30 +231,20 @@ class CloudFileWidget extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Symbols.download,
|
Symbols.more_horiz,
|
||||||
color: Colors.white,
|
|
||||||
size: 16,
|
|
||||||
),
|
|
||||||
onPressed: downloadFile,
|
|
||||||
padding: EdgeInsets.all(4),
|
|
||||||
constraints: const BoxConstraints(),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(
|
|
||||||
Symbols.info,
|
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
size: 16,
|
size: 16,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
showModalBottomSheet(
|
context.pushNamed(
|
||||||
useRootNavigator: true,
|
'fileDetail',
|
||||||
context: context,
|
pathParameters: {'id': item.id},
|
||||||
isScrollControlled: true,
|
extra: item,
|
||||||
builder: (context) => FileInfoSheet(item: item),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
padding: EdgeInsets.all(4),
|
padding: EdgeInsets.all(4),
|
||||||
constraints: const BoxConstraints(),
|
constraints: const BoxConstraints(),
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -356,41 +273,13 @@ class CloudFileWidget extends HookConsumerWidget {
|
|||||||
'audio' => AudioFileContent(item: item, uri: uri),
|
'audio' => AudioFileContent(item: item, uri: uri),
|
||||||
_ => Builder(
|
_ => Builder(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
Future<void> downloadFile() async {
|
|
||||||
try {
|
|
||||||
showSnackBar('Downloading file...');
|
|
||||||
|
|
||||||
final client = ref.read(apiClientProvider);
|
|
||||||
final tempDir = await getTemporaryDirectory();
|
|
||||||
var extName = extension(item.name).trim();
|
|
||||||
if (extName.isEmpty) {
|
|
||||||
extName = item.mimeType?.split('/').lastOrNull ?? 'bin';
|
|
||||||
}
|
|
||||||
final filePath = '${tempDir.path}/${item.id}.$extName';
|
|
||||||
|
|
||||||
await client.download(
|
|
||||||
'/drive/files/${item.id}',
|
|
||||||
filePath,
|
|
||||||
queryParameters: {'original': true},
|
|
||||||
);
|
|
||||||
|
|
||||||
await FileSaver.instance.saveFile(
|
|
||||||
name: item.name.isEmpty ? '${item.id}.$extName' : item.name,
|
|
||||||
file: File(filePath),
|
|
||||||
);
|
|
||||||
showSnackBar('File saved to downloads');
|
|
||||||
} catch (e) {
|
|
||||||
showErrorAlert(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: Theme.of(context).colorScheme.outline,
|
color: Theme.of(context).colorScheme.outline,
|
||||||
width: 1,
|
width: 1,
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(16),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -422,19 +311,12 @@ class CloudFileWidget extends HookConsumerWidget {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
TextButton.icon(
|
|
||||||
onPressed: downloadFile,
|
|
||||||
icon: const Icon(Symbols.download),
|
|
||||||
label: Text('download').tr(),
|
|
||||||
),
|
|
||||||
const Gap(8),
|
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
showModalBottomSheet(
|
context.pushNamed(
|
||||||
useRootNavigator: true,
|
'fileDetail',
|
||||||
context: context,
|
pathParameters: {'id': item.id},
|
||||||
isScrollControlled: true,
|
extra: item,
|
||||||
builder: (context) => FileInfoSheet(item: item),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: const Icon(Symbols.info),
|
icon: const Icon(Symbols.info),
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:file_saver/file_saver.dart';
|
import 'package:file_saver/file_saver.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
@@ -28,8 +30,64 @@ class PdfFileContent extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final pdfViewer = useMemoized(() => SfPdfViewer.network(uri), [uri]);
|
final fileFuture = useMemoized(
|
||||||
return pdfViewer;
|
() => DefaultCacheManager().getSingleFile(uri),
|
||||||
|
[uri],
|
||||||
|
);
|
||||||
|
|
||||||
|
final pdfController = useMemoized(() => PdfViewerController(), []);
|
||||||
|
|
||||||
|
final shadow = [
|
||||||
|
Shadow(color: Colors.black54, blurRadius: 5.0, offset: Offset(1.0, 1.0)),
|
||||||
|
];
|
||||||
|
|
||||||
|
return FutureBuilder<File>(
|
||||||
|
future: fileFuture,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
} else if (snapshot.hasError) {
|
||||||
|
return Center(child: Text('Error loading PDF: ${snapshot.error}'));
|
||||||
|
} else if (snapshot.hasData) {
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
SfPdfViewer.file(snapshot.data!, controller: pdfController),
|
||||||
|
// Controls overlay
|
||||||
|
Positioned(
|
||||||
|
bottom: MediaQuery.of(context).padding.bottom + 16,
|
||||||
|
left: 16,
|
||||||
|
right: 16,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
Icons.remove,
|
||||||
|
color: Colors.white,
|
||||||
|
shadows: shadow,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
pdfController.zoomLevel = pdfController.zoomLevel * 0.9;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
Icons.add,
|
||||||
|
color: Colors.white,
|
||||||
|
shadows: shadow,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
pdfController.zoomLevel = pdfController.zoomLevel * 1.1;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return const Center(child: Text('No PDF data'));
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,6 +147,25 @@ class ImageFileContent extends HookConsumerWidget {
|
|||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
|
child: Listener(
|
||||||
|
onPointerSignal: (pointerSignal) {
|
||||||
|
try {
|
||||||
|
// Handle mouse wheel zoom - cast to dynamic to access scrollDelta
|
||||||
|
final delta =
|
||||||
|
(pointerSignal as dynamic).scrollDelta.dy as double?;
|
||||||
|
if (delta != null && delta != 0) {
|
||||||
|
final currentScale = photoViewController.scale ?? 1.0;
|
||||||
|
// Adjust scale based on scroll direction (invert for natural zoom)
|
||||||
|
final newScale =
|
||||||
|
delta > 0 ? currentScale * 0.9 : currentScale * 1.1;
|
||||||
|
// Clamp scale to reasonable bounds
|
||||||
|
final clampedScale = newScale.clamp(0.1, 10.0);
|
||||||
|
photoViewController.scale = clampedScale;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore non-scroll events
|
||||||
|
}
|
||||||
|
},
|
||||||
child: PhotoView(
|
child: PhotoView(
|
||||||
backgroundDecoration: BoxDecoration(
|
backgroundDecoration: BoxDecoration(
|
||||||
color: Colors.black.withOpacity(0.9),
|
color: Colors.black.withOpacity(0.9),
|
||||||
@@ -104,6 +181,7 @@ class ImageFileContent extends HookConsumerWidget {
|
|||||||
filterQuality: FilterQuality.high,
|
filterQuality: FilterQuality.high,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
// Controls overlay
|
// Controls overlay
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: MediaQuery.of(context).padding.bottom + 16,
|
bottom: MediaQuery.of(context).padding.bottom + 16,
|
||||||
@@ -245,16 +323,6 @@ class GenericFileContent extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Center(
|
return Center(
|
||||||
child: Container(
|
|
||||||
margin: const EdgeInsets.all(32),
|
|
||||||
padding: const EdgeInsets.all(32),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(
|
|
||||||
color: Theme.of(context).colorScheme.outline,
|
|
||||||
width: 1,
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
@@ -288,7 +356,7 @@ class GenericFileContent extends HookConsumerWidget {
|
|||||||
FilledButton.icon(
|
FilledButton.icon(
|
||||||
onPressed: downloadFile,
|
onPressed: downloadFile,
|
||||||
icon: const Icon(Symbols.download),
|
icon: const Icon(Symbols.download),
|
||||||
label: Text('download'),
|
label: Text('download').tr(),
|
||||||
),
|
),
|
||||||
const Gap(16),
|
const Gap(16),
|
||||||
OutlinedButton.icon(
|
OutlinedButton.icon(
|
||||||
@@ -301,13 +369,12 @@ class GenericFileContent extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: const Icon(Symbols.info),
|
icon: const Icon(Symbols.info),
|
||||||
label: Text('info'),
|
label: Text('info').tr(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -166,7 +166,6 @@ class MarkdownTextContent extends HookConsumerWidget {
|
|||||||
label: 'copyToClipboard'.tr(),
|
label: 'copyToClipboard'.tr(),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Clipboard.setData(ClipboardData(text: href));
|
Clipboard.setData(ClipboardData(text: href));
|
||||||
clearSnackBar(context);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -51,7 +51,10 @@ class SheetScaffold extends StatelessWidget {
|
|||||||
const Spacer(),
|
const Spacer(),
|
||||||
...actions,
|
...actions,
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Symbols.close),
|
icon: Icon(
|
||||||
|
Symbols.close,
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
),
|
||||||
onPressed:
|
onPressed:
|
||||||
() =>
|
() =>
|
||||||
onClose != null
|
onClose != null
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:island/pods/network.dart';
|
|
||||||
import 'package:island/talker.dart';
|
|
||||||
import 'package:media_kit/media_kit.dart';
|
import 'package:media_kit/media_kit.dart';
|
||||||
import 'package:media_kit_video/media_kit_video.dart';
|
import 'package:media_kit_video/media_kit_video.dart';
|
||||||
|
|
||||||
@@ -28,28 +25,12 @@ class _UniversalVideoState extends ConsumerState<UniversalVideo> {
|
|||||||
VideoController? _videoController;
|
VideoController? _videoController;
|
||||||
|
|
||||||
void _openVideo() async {
|
void _openVideo() async {
|
||||||
final url = widget.uri;
|
|
||||||
MediaKit.ensureInitialized();
|
MediaKit.ensureInitialized();
|
||||||
|
|
||||||
_player = Player();
|
_player = Player();
|
||||||
_videoController = VideoController(_player!);
|
_videoController = VideoController(_player!);
|
||||||
|
|
||||||
String? uri;
|
_player!.open(Media(widget.uri), play: widget.autoplay);
|
||||||
final inCacheInfo = await DefaultCacheManager().getFileFromCache(url);
|
|
||||||
if (inCacheInfo == null) {
|
|
||||||
talker.info('[MediaPlayer] Miss cache: $url');
|
|
||||||
final token = ref.watch(tokenProvider)?.token;
|
|
||||||
DefaultCacheManager().downloadFile(
|
|
||||||
url,
|
|
||||||
authHeaders: {'Authorization': 'AtField $token'},
|
|
||||||
);
|
|
||||||
uri = url;
|
|
||||||
} else {
|
|
||||||
uri = inCacheInfo.file.path;
|
|
||||||
talker.info('[MediaPlayer] Hit cache: $url');
|
|
||||||
}
|
|
||||||
|
|
||||||
_player!.open(Media(uri), play: widget.autoplay);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ Future<void> _showSetTokenDialog(BuildContext context, WidgetRef ref) async {
|
|||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
hintText: 'Enter access token',
|
hintText: 'Enter access token',
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -457,7 +457,7 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
maxLines: 6,
|
maxLines: 6,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ 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/post.dart';
|
import 'package:island/models/post.dart';
|
||||||
|
import 'package:island/widgets/alert.dart';
|
||||||
import 'package:island/widgets/content/sheet.dart';
|
import 'package:island/widgets/content/sheet.dart';
|
||||||
import 'package:island/widgets/post/compose_shared.dart';
|
import 'package:island/widgets/post/compose_shared.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
@@ -56,9 +57,7 @@ class ComposeEmbedSheet extends HookConsumerWidget {
|
|||||||
void saveEmbedView() {
|
void saveEmbedView() {
|
||||||
final uri = uriController.text.trim();
|
final uri = uriController.text.trim();
|
||||||
if (uri.isEmpty) {
|
if (uri.isEmpty) {
|
||||||
ScaffoldMessenger.of(
|
showSnackBar('embedUriRequired'.tr());
|
||||||
context,
|
|
||||||
).showSnackBar(SnackBar(content: Text('embedUriRequired'.tr())));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -281,8 +281,8 @@ class ComposeFundSheet extends HookConsumerWidget {
|
|||||||
// Return the fund that was just created (but not yet paid)
|
// Return the fund that was just created (but not yet paid)
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
hideLoadingModal(context);
|
hideLoadingModal(context);
|
||||||
}
|
|
||||||
Navigator.of(context).pop(fund);
|
Navigator.of(context).pop(fund);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,10 +327,10 @@ class ComposeFundSheet extends HookConsumerWidget {
|
|||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
hideLoadingModal(context);
|
hideLoadingModal(context);
|
||||||
}
|
|
||||||
Navigator.of(
|
Navigator.of(
|
||||||
context,
|
context,
|
||||||
).pop(updatedFund);
|
).pop(updatedFund);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
isPushing.value = false;
|
isPushing.value = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -751,12 +751,7 @@ class ComposeLogic {
|
|||||||
|
|
||||||
return post;
|
return post;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Show error message if context is mounted
|
showErrorAlert(err);
|
||||||
if (context.mounted) {
|
|
||||||
ScaffoldMessenger.of(
|
|
||||||
context,
|
|
||||||
).showSnackBar(SnackBar(content: Text('Error: $err')));
|
|
||||||
}
|
|
||||||
rethrow;
|
rethrow;
|
||||||
} finally {
|
} finally {
|
||||||
state.submitting.value = false;
|
state.submitting.value = false;
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ class PostItemScreenshot extends ConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
Gap(renderingPadding.vertical),
|
Gap(renderingPadding.vertical),
|
||||||
PostHeader(
|
PostHeader(
|
||||||
|
hideOverlay: true,
|
||||||
item: item,
|
item: item,
|
||||||
isFullPost: isFullPost,
|
isFullPost: isFullPost,
|
||||||
isInteractive: false,
|
isInteractive: false,
|
||||||
@@ -73,6 +74,7 @@ class PostItemScreenshot extends ConsumerWidget {
|
|||||||
isFullPost: isFullPost,
|
isFullPost: isFullPost,
|
||||||
isTextSelectable: false,
|
isTextSelectable: false,
|
||||||
isInteractive: false,
|
isInteractive: false,
|
||||||
|
hideOverlay: true,
|
||||||
),
|
),
|
||||||
if (isShowReference)
|
if (isShowReference)
|
||||||
ReferencedPostWidget(
|
ReferencedPostWidget(
|
||||||
|
|||||||
@@ -631,12 +631,33 @@ class CustomReactionForm extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
suffixIcon: InkWell(
|
suffixIcon: InkWell(
|
||||||
onTapDown: (details) async {
|
onTapDown: (details) async {
|
||||||
|
final screenSize = MediaQuery.sizeOf(context);
|
||||||
|
const popoverWidth = 500.0;
|
||||||
|
const popoverHeight = 500.0;
|
||||||
|
const padding = 20.0;
|
||||||
|
|
||||||
|
// Calculate safe horizontal position (centered, but within bounds)
|
||||||
|
final maxHorizontalOffset = math.max(
|
||||||
|
padding,
|
||||||
|
screenSize.width - popoverWidth - padding,
|
||||||
|
);
|
||||||
|
final horizontalOffset = ((screenSize.width - popoverWidth) /
|
||||||
|
2)
|
||||||
|
.clamp(padding, maxHorizontalOffset);
|
||||||
|
|
||||||
|
// Calculate safe vertical position (bottom-aligned, but within bounds)
|
||||||
|
final maxVerticalOffset = math.max(
|
||||||
|
padding,
|
||||||
|
screenSize.height - popoverHeight - padding,
|
||||||
|
);
|
||||||
|
final verticalOffset = (screenSize.height -
|
||||||
|
popoverHeight -
|
||||||
|
padding)
|
||||||
|
.clamp(padding, maxVerticalOffset);
|
||||||
|
|
||||||
await showStickerPickerPopover(
|
await showStickerPickerPopover(
|
||||||
context,
|
context,
|
||||||
Offset(
|
Offset(horizontalOffset, verticalOffset),
|
||||||
(MediaQuery.sizeOf(context).width - 500) / 2,
|
|
||||||
MediaQuery.sizeOf(context).height - 500,
|
|
||||||
),
|
|
||||||
alignment: Alignment.topLeft,
|
alignment: Alignment.topLeft,
|
||||||
onPick: (placeholder) {
|
onPick: (placeholder) {
|
||||||
// Remove the surrounding : from the placeholder
|
// Remove the surrounding : from the placeholder
|
||||||
|
|||||||
@@ -554,6 +554,7 @@ class PostHeader extends StatelessWidget {
|
|||||||
final EdgeInsets renderingPadding;
|
final EdgeInsets renderingPadding;
|
||||||
final bool isRelativeTime;
|
final bool isRelativeTime;
|
||||||
final bool isCompact;
|
final bool isCompact;
|
||||||
|
final bool hideOverlay;
|
||||||
|
|
||||||
const PostHeader({
|
const PostHeader({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -564,6 +565,7 @@ class PostHeader extends StatelessWidget {
|
|||||||
this.renderingPadding = EdgeInsets.zero,
|
this.renderingPadding = EdgeInsets.zero,
|
||||||
this.isRelativeTime = true,
|
this.isRelativeTime = true,
|
||||||
this.isCompact = false,
|
this.isCompact = false,
|
||||||
|
this.hideOverlay = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -606,6 +608,7 @@ class PostHeader extends StatelessWidget {
|
|||||||
(item.publisher.account != null &&
|
(item.publisher.account != null &&
|
||||||
item.publisher.type == 0)
|
item.publisher.type == 0)
|
||||||
? AccountName(
|
? AccountName(
|
||||||
|
hideOverlay: hideOverlay,
|
||||||
account: item.publisher.account!,
|
account: item.publisher.account!,
|
||||||
textOverride: item.publisher.nick,
|
textOverride: item.publisher.nick,
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
@@ -618,7 +621,10 @@ class PostHeader extends StatelessWidget {
|
|||||||
).bold(),
|
).bold(),
|
||||||
),
|
),
|
||||||
if (item.publisher.verification != null)
|
if (item.publisher.verification != null)
|
||||||
VerificationMark(mark: item.publisher.verification!),
|
VerificationMark(
|
||||||
|
mark: item.publisher.verification!,
|
||||||
|
hideOverlay: hideOverlay,
|
||||||
|
),
|
||||||
if (item.realm == null)
|
if (item.realm == null)
|
||||||
Flexible(
|
Flexible(
|
||||||
child:
|
child:
|
||||||
@@ -690,6 +696,7 @@ class PostBody extends ConsumerWidget {
|
|||||||
final bool isInteractive;
|
final bool isInteractive;
|
||||||
final EdgeInsets renderingPadding;
|
final EdgeInsets renderingPadding;
|
||||||
final bool isRelativeTime;
|
final bool isRelativeTime;
|
||||||
|
final bool hideOverlay;
|
||||||
|
|
||||||
const PostBody({
|
const PostBody({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -700,6 +707,7 @@ class PostBody extends ConsumerWidget {
|
|||||||
this.isInteractive = true,
|
this.isInteractive = true,
|
||||||
this.renderingPadding = EdgeInsets.zero,
|
this.renderingPadding = EdgeInsets.zero,
|
||||||
this.isRelativeTime = true,
|
this.isRelativeTime = true,
|
||||||
|
this.hideOverlay = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -771,18 +779,7 @@ class PostBody extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (item.editedAt != null) {
|
if (item.editedAt != null) {
|
||||||
metadataChildren.add(
|
final text = Text(
|
||||||
Row(
|
|
||||||
spacing: 8,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
const Icon(Symbols.edit, size: 16),
|
|
||||||
Tooltip(
|
|
||||||
message:
|
|
||||||
!isFullPost && isRelativeTime
|
|
||||||
? item.editedAt!.formatSystem()
|
|
||||||
: item.editedAt!.formatRelative(context),
|
|
||||||
child: Text(
|
|
||||||
'editedAt'.tr(
|
'editedAt'.tr(
|
||||||
args: [
|
args: [
|
||||||
!isFullPost && isRelativeTime
|
!isFullPost && isRelativeTime
|
||||||
@@ -790,7 +787,22 @@ class PostBody extends ConsumerWidget {
|
|||||||
: item.editedAt!.formatSystem(),
|
: item.editedAt!.formatSystem(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
).fontSize(13),
|
).fontSize(13);
|
||||||
|
|
||||||
|
metadataChildren.add(
|
||||||
|
Row(
|
||||||
|
spacing: 8,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.edit, size: 16),
|
||||||
|
hideOverlay
|
||||||
|
? text
|
||||||
|
: Tooltip(
|
||||||
|
message:
|
||||||
|
!isFullPost && isRelativeTime
|
||||||
|
? item.editedAt!.formatSystem()
|
||||||
|
: item.editedAt!.formatRelative(context),
|
||||||
|
child: text,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
289
lib/widgets/sites/file_item.dart
Normal file
289
lib/widgets/sites/file_item.dart
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_code_editor/flutter_code_editor.dart';
|
||||||
|
import 'package:flutter_highlight/themes/monokai-sublime.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:island/models/site_file.dart';
|
||||||
|
import 'package:island/models/publication_site.dart';
|
||||||
|
import 'package:island/pods/site_files.dart';
|
||||||
|
import 'package:island/pods/network.dart';
|
||||||
|
import 'package:island/widgets/alert.dart';
|
||||||
|
import 'package:island/widgets/content/sheet.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
|
||||||
|
class FileItem extends HookConsumerWidget {
|
||||||
|
final SnSiteFileEntry file;
|
||||||
|
final SnPublicationSite site;
|
||||||
|
final void Function(String path)? onNavigateDirectory;
|
||||||
|
|
||||||
|
const FileItem({
|
||||||
|
super.key,
|
||||||
|
required this.file,
|
||||||
|
required this.site,
|
||||||
|
this.onNavigateDirectory,
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<void> _downloadFile(BuildContext context, WidgetRef ref) async {
|
||||||
|
try {
|
||||||
|
final apiClient = ref.read(apiClientProvider);
|
||||||
|
|
||||||
|
// Get downloads directory
|
||||||
|
Directory? directory;
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
directory = await getExternalStorageDirectory();
|
||||||
|
if (directory != null) {
|
||||||
|
directory = Directory('${directory.path}/Download');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
directory = await getDownloadsDirectory();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (directory == null) {
|
||||||
|
throw Exception('Unable to access downloads directory');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create directory if it doesn't exist
|
||||||
|
await directory.create(recursive: true);
|
||||||
|
|
||||||
|
// Generate file path
|
||||||
|
final fileName = file.relativePath.split('/').last;
|
||||||
|
final filePath = '${directory.path}/$fileName';
|
||||||
|
|
||||||
|
// Use Dio's download method to directly stream from server to file
|
||||||
|
await apiClient.download(
|
||||||
|
'/zone/sites/${site.id}/files/content/${file.relativePath}',
|
||||||
|
filePath,
|
||||||
|
);
|
||||||
|
|
||||||
|
showSnackBar('Downloaded to $filePath');
|
||||||
|
} catch (e) {
|
||||||
|
showErrorAlert(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _showEditSheet(BuildContext context, WidgetRef ref) async {
|
||||||
|
try {
|
||||||
|
final fileContent = await ref.read(
|
||||||
|
siteFileContentProvider(
|
||||||
|
siteId: site.id,
|
||||||
|
relativePath: file.relativePath,
|
||||||
|
).future,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
await showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
useSafeArea: false,
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxHeight: MediaQuery.of(context).size.height,
|
||||||
|
),
|
||||||
|
barrierColor: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return FileEditorSheet(
|
||||||
|
file: file,
|
||||||
|
site: site,
|
||||||
|
initialContent: fileContent.content,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showErrorAlert(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
|
elevation: 0,
|
||||||
|
child: ListTile(
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||||
|
),
|
||||||
|
leading: Icon(
|
||||||
|
file.isDirectory ? Symbols.folder : Symbols.description,
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
title: Text(file.relativePath),
|
||||||
|
subtitle: Text(
|
||||||
|
file.isDirectory
|
||||||
|
? 'Directory'
|
||||||
|
: '${(file.size / 1024).toStringAsFixed(1)} KB',
|
||||||
|
),
|
||||||
|
trailing: PopupMenuButton<String>(
|
||||||
|
itemBuilder:
|
||||||
|
(context) => [
|
||||||
|
PopupMenuItem(
|
||||||
|
value: 'download',
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.download),
|
||||||
|
const Gap(16),
|
||||||
|
Text('Download'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (!file.isDirectory) ...[
|
||||||
|
PopupMenuItem(
|
||||||
|
value: 'edit',
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.edit),
|
||||||
|
const Gap(16),
|
||||||
|
Text('Edit Content'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
PopupMenuItem(
|
||||||
|
value: 'delete',
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.delete, color: Colors.red),
|
||||||
|
const Gap(16),
|
||||||
|
Text('Delete').textColor(Colors.red),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onSelected: (value) async {
|
||||||
|
switch (value) {
|
||||||
|
case 'download':
|
||||||
|
await _downloadFile(context, ref);
|
||||||
|
break;
|
||||||
|
case 'edit':
|
||||||
|
await _showEditSheet(context, ref);
|
||||||
|
break;
|
||||||
|
case 'delete':
|
||||||
|
final confirmed = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder:
|
||||||
|
(context) => AlertDialog(
|
||||||
|
title: const Text('Delete File'),
|
||||||
|
content: Text(
|
||||||
|
'Are you sure you want to delete "${file.relativePath}"?',
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(false),
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(true),
|
||||||
|
child: const Text('Delete'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (confirmed == true) {
|
||||||
|
try {
|
||||||
|
await ref
|
||||||
|
.read(
|
||||||
|
siteFilesNotifierProvider((
|
||||||
|
siteId: site.id,
|
||||||
|
path: null,
|
||||||
|
)).notifier,
|
||||||
|
)
|
||||||
|
.deleteFile(file.relativePath);
|
||||||
|
showSnackBar('File deleted successfully');
|
||||||
|
} catch (e) {
|
||||||
|
showErrorAlert(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
if (file.isDirectory) {
|
||||||
|
onNavigateDirectory?.call(file.relativePath);
|
||||||
|
} else {
|
||||||
|
_showEditSheet(context, ref);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FileEditorSheet extends HookConsumerWidget {
|
||||||
|
final SnSiteFileEntry file;
|
||||||
|
final SnPublicationSite site;
|
||||||
|
final String initialContent;
|
||||||
|
|
||||||
|
const FileEditorSheet({
|
||||||
|
super.key,
|
||||||
|
required this.file,
|
||||||
|
required this.site,
|
||||||
|
required this.initialContent,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final codeController = useMemoized(
|
||||||
|
() => CodeController(
|
||||||
|
text: initialContent,
|
||||||
|
language: null, // Let the editor auto-detect or use plain text
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final isSaving = useState(false);
|
||||||
|
|
||||||
|
final saveFile = useCallback(() async {
|
||||||
|
if (codeController.text.trim().isEmpty) {
|
||||||
|
showSnackBar('contentCantEmpty'.tr());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isSaving.value = true;
|
||||||
|
try {
|
||||||
|
await ref
|
||||||
|
.read(
|
||||||
|
siteFilesNotifierProvider((siteId: site.id, path: null)).notifier,
|
||||||
|
)
|
||||||
|
.updateFileContent(file.relativePath, codeController.text);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
showSnackBar('File saved successfully');
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showErrorAlert(e);
|
||||||
|
} finally {
|
||||||
|
isSaving.value = false;
|
||||||
|
}
|
||||||
|
}, [codeController, ref, site.id, file.relativePath, context, isSaving]);
|
||||||
|
|
||||||
|
return SheetScaffold(
|
||||||
|
heightFactor: 1,
|
||||||
|
titleText: 'Edit ${file.relativePath}',
|
||||||
|
actions: [
|
||||||
|
FilledButton(
|
||||||
|
onPressed: isSaving.value ? null : saveFile,
|
||||||
|
child: Text(isSaving.value ? 'Saving...' : 'Save'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
child: CodeTheme(
|
||||||
|
data: CodeThemeData(styles: monokaiSublimeTheme),
|
||||||
|
child: CodeField(
|
||||||
|
controller: codeController,
|
||||||
|
minLines: 20,
|
||||||
|
maxLines: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
155
lib/widgets/sites/file_management_action_section.dart
Normal file
155
lib/widgets/sites/file_management_action_section.dart
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:http_parser/http_parser.dart';
|
||||||
|
import 'package:island/models/publication_site.dart';
|
||||||
|
import 'package:island/pods/network.dart';
|
||||||
|
import 'package:island/widgets/alert.dart';
|
||||||
|
import 'package:island/pods/site_files.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
|
||||||
|
class FileManagementActionSection extends HookConsumerWidget {
|
||||||
|
final SnPublicationSite site;
|
||||||
|
final String pubName;
|
||||||
|
|
||||||
|
const FileManagementActionSection({
|
||||||
|
super.key,
|
||||||
|
required this.site,
|
||||||
|
required this.pubName,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'fileActions'.tr(),
|
||||||
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
).padding(horizontal: 16, top: 16),
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
leading: Icon(
|
||||||
|
Symbols.delete_forever,
|
||||||
|
color: theme.colorScheme.error,
|
||||||
|
),
|
||||||
|
title: Text('purgeFiles'.tr()),
|
||||||
|
subtitle: Text('purgeFilesDescription'.tr()),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
onTap: () => _purgeFiles(context, ref),
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
ListTile(
|
||||||
|
leading: Icon(
|
||||||
|
Symbols.upload,
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
title: Text('deploySite'.tr()),
|
||||||
|
subtitle: Text('deploySiteDescription'.tr()),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
onTap: () => _deploySite(context, ref),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(vertical: 8),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _purgeFiles(BuildContext context, WidgetRef ref) async {
|
||||||
|
final confirmed = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder:
|
||||||
|
(context) => AlertDialog(
|
||||||
|
title: Text('confirmPurge'.tr()),
|
||||||
|
content: Text('purgeFilesConfirm'.tr()),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context, false),
|
||||||
|
child: Text('cancel'.tr()),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () => Navigator.pop(context, true),
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.error,
|
||||||
|
),
|
||||||
|
child: Text('purgeAllFiles'.tr()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (confirmed != true) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final apiClient = ref.read(apiClientProvider);
|
||||||
|
await apiClient.delete('/zone/sites/${site.id}/files/purge');
|
||||||
|
if (context.mounted) {
|
||||||
|
showSnackBar('allFilesPurgedSuccess'.tr());
|
||||||
|
// Refresh the file management section
|
||||||
|
ref.invalidate(siteFilesProvider(siteId: site.id));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (context.mounted) {
|
||||||
|
showSnackBar('failedToPurgeFiles'.tr(args: [e.toString()]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _deploySite(BuildContext context, WidgetRef ref) async {
|
||||||
|
final result = await FilePicker.platform.pickFiles(
|
||||||
|
type: FileType.custom,
|
||||||
|
allowedExtensions: ['zip'],
|
||||||
|
allowMultiple: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result == null || result.files.isEmpty) {
|
||||||
|
return; // User canceled
|
||||||
|
}
|
||||||
|
|
||||||
|
final file = File(result.files.first.path!);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final apiClient = ref.read(apiClientProvider);
|
||||||
|
|
||||||
|
// Create multipart form data
|
||||||
|
final formData = FormData.fromMap({
|
||||||
|
'file': await MultipartFile.fromFile(
|
||||||
|
file.path,
|
||||||
|
filename: result.files.first.name,
|
||||||
|
contentType: MediaType('application', 'zip'),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
await apiClient.post(
|
||||||
|
'/zone/sites/${site.id}/files/deploy',
|
||||||
|
data: formData,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
showSnackBar('siteDeployedSuccess'.tr());
|
||||||
|
// Refresh the file management section
|
||||||
|
ref.invalidate(siteFilesProvider(siteId: site.id));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (context.mounted) {
|
||||||
|
showSnackBar('failedToDeploySite'.tr(args: [e.toString()]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
312
lib/widgets/sites/file_management_section.dart
Normal file
312
lib/widgets/sites/file_management_section.dart
Normal file
@@ -0,0 +1,312 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:island/models/publication_site.dart';
|
||||||
|
import 'package:island/pods/site_files.dart';
|
||||||
|
import 'package:island/widgets/alert.dart';
|
||||||
|
import 'package:island/widgets/sites/file_upload_dialog.dart';
|
||||||
|
import 'package:island/widgets/sites/file_item.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
|
||||||
|
class FileManagementSection extends HookConsumerWidget {
|
||||||
|
final SnPublicationSite site;
|
||||||
|
final String pubName;
|
||||||
|
|
||||||
|
const FileManagementSection({
|
||||||
|
super.key,
|
||||||
|
required this.site,
|
||||||
|
required this.pubName,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final currentPath = useState<String?>(null);
|
||||||
|
final filesAsync = ref.watch(
|
||||||
|
siteFilesProvider(siteId: site.id, path: currentPath.value),
|
||||||
|
);
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(Symbols.folder, size: 20),
|
||||||
|
const Gap(8),
|
||||||
|
Text(
|
||||||
|
'fileManagement'.tr(),
|
||||||
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
PopupMenuButton<String>(
|
||||||
|
icon: const Icon(Symbols.upload),
|
||||||
|
onSelected: (String choice) async {
|
||||||
|
List<File> files = [];
|
||||||
|
List<Map<String, dynamic>>? results;
|
||||||
|
if (choice == 'files') {
|
||||||
|
final selectedFiles = await FilePicker.platform
|
||||||
|
.pickFiles(
|
||||||
|
allowMultiple: true,
|
||||||
|
type: FileType.any,
|
||||||
|
);
|
||||||
|
if (selectedFiles == null ||
|
||||||
|
selectedFiles.files.isEmpty) {
|
||||||
|
return; // User canceled
|
||||||
|
}
|
||||||
|
files =
|
||||||
|
selectedFiles.files
|
||||||
|
.map((f) => File(f.path!))
|
||||||
|
.toList();
|
||||||
|
} else if (choice == 'folder') {
|
||||||
|
final dirPath =
|
||||||
|
await FilePicker.platform.getDirectoryPath();
|
||||||
|
if (dirPath == null) return;
|
||||||
|
results = await _getFilesRecursive(dirPath);
|
||||||
|
files =
|
||||||
|
results.map((m) => m['file'] as File).toList();
|
||||||
|
if (files.isEmpty) {
|
||||||
|
showSnackBar('noFilesFoundInFolder'.tr());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!context.mounted) return;
|
||||||
|
|
||||||
|
// Show upload dialog for path specification
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
builder:
|
||||||
|
(context) => FileUploadDialog(
|
||||||
|
selectedFiles: files,
|
||||||
|
site: site,
|
||||||
|
relativePaths:
|
||||||
|
results
|
||||||
|
?.map(
|
||||||
|
(m) => m['relativePath'] as String,
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
onUploadComplete: () {
|
||||||
|
// Refresh file list
|
||||||
|
ref.invalidate(
|
||||||
|
siteFilesProvider(
|
||||||
|
siteId: site.id,
|
||||||
|
path: currentPath.value,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
itemBuilder:
|
||||||
|
(BuildContext context) => [
|
||||||
|
PopupMenuItem<String>(
|
||||||
|
value: 'files',
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Symbols.file_copy),
|
||||||
|
Gap(12),
|
||||||
|
Text('siteFiles'.tr()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
PopupMenuItem<String>(
|
||||||
|
value: 'folder',
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Symbols.folder),
|
||||||
|
Gap(12),
|
||||||
|
Text('siteFolder'.tr()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
style: ButtonStyle(
|
||||||
|
visualDensity: const VisualDensity(
|
||||||
|
horizontal: -4,
|
||||||
|
vertical: -4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
if (currentPath.value != null && currentPath.value!.isNotEmpty)
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.only(top: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.surfaceContainerHigh,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Symbols.arrow_back),
|
||||||
|
onPressed: () {
|
||||||
|
final pathParts =
|
||||||
|
currentPath.value!
|
||||||
|
.split('/')
|
||||||
|
.where((part) => part.isNotEmpty)
|
||||||
|
.toList();
|
||||||
|
if (pathParts.isEmpty) {
|
||||||
|
currentPath.value = null;
|
||||||
|
} else {
|
||||||
|
pathParts.removeLast();
|
||||||
|
currentPath.value =
|
||||||
|
pathParts.isEmpty
|
||||||
|
? null
|
||||||
|
: pathParts.join('/');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
visualDensity: const VisualDensity(
|
||||||
|
horizontal: -4,
|
||||||
|
vertical: -4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Wrap(
|
||||||
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
|
children: [
|
||||||
|
InkWell(
|
||||||
|
onTap: () => currentPath.value = null,
|
||||||
|
child: Text('siteRoot'.tr()),
|
||||||
|
),
|
||||||
|
...() {
|
||||||
|
final parts =
|
||||||
|
currentPath.value!
|
||||||
|
.split('/')
|
||||||
|
.where((part) => part.isNotEmpty)
|
||||||
|
.toList();
|
||||||
|
final widgets = <Widget>[];
|
||||||
|
String currentBuilder = '';
|
||||||
|
for (final part in parts) {
|
||||||
|
currentBuilder +=
|
||||||
|
(currentBuilder.isEmpty ? '' : '/') +
|
||||||
|
part;
|
||||||
|
final pathToSet = currentBuilder;
|
||||||
|
widgets.addAll([
|
||||||
|
const Text(' / '),
|
||||||
|
InkWell(
|
||||||
|
onTap:
|
||||||
|
() => currentPath.value = pathToSet,
|
||||||
|
child: Text(part),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return widgets;
|
||||||
|
}(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
filesAsync.when(
|
||||||
|
data: (files) {
|
||||||
|
if (files.isEmpty) {
|
||||||
|
return Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(32),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Symbols.folder,
|
||||||
|
size: 48,
|
||||||
|
color: theme.colorScheme.outline,
|
||||||
|
),
|
||||||
|
const Gap(16),
|
||||||
|
Text(
|
||||||
|
'noFilesUploadedYet'.tr(),
|
||||||
|
style: theme.textTheme.bodyLarge,
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
Text(
|
||||||
|
'uploadFirstFile'.tr(),
|
||||||
|
style: theme.textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
itemCount: files.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final file = files[index];
|
||||||
|
return FileItem(
|
||||||
|
file: file,
|
||||||
|
site: site,
|
||||||
|
onNavigateDirectory:
|
||||||
|
(path) => currentPath.value = path,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
loading:
|
||||||
|
() => const Center(child: CircularProgressIndicator()),
|
||||||
|
error:
|
||||||
|
(error, stack) => Center(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text('failedToLoadFiles'.tr()),
|
||||||
|
const Gap(8),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed:
|
||||||
|
() => ref.invalidate(
|
||||||
|
siteFilesProvider(
|
||||||
|
siteId: site.id,
|
||||||
|
path: currentPath.value,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text('retry'.tr()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Map<String, dynamic>>> _getFilesRecursive(String dirPath) async {
|
||||||
|
final List<Map<String, dynamic>> results = [];
|
||||||
|
try {
|
||||||
|
await for (final entity in Directory(dirPath).list(recursive: true)) {
|
||||||
|
if (entity is File) {
|
||||||
|
String relativePath = entity.path.substring(dirPath.length);
|
||||||
|
if (relativePath.startsWith('/')) {
|
||||||
|
relativePath = relativePath.substring(1);
|
||||||
|
}
|
||||||
|
if (relativePath.isEmpty) continue;
|
||||||
|
results.add({
|
||||||
|
'file': File(entity.path),
|
||||||
|
'relativePath': relativePath,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Handle error if needed
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
288
lib/widgets/sites/file_upload_dialog.dart
Normal file
288
lib/widgets/sites/file_upload_dialog.dart
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:island/models/publication_site.dart';
|
||||||
|
import 'package:island/pods/site_files.dart';
|
||||||
|
import 'package:island/widgets/alert.dart';
|
||||||
|
import 'package:island/widgets/content/sheet.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
|
||||||
|
class FileUploadDialog extends HookConsumerWidget {
|
||||||
|
final List<File> selectedFiles;
|
||||||
|
final SnPublicationSite site;
|
||||||
|
final VoidCallback onUploadComplete;
|
||||||
|
final List<String>? relativePaths;
|
||||||
|
|
||||||
|
const FileUploadDialog({
|
||||||
|
super.key,
|
||||||
|
required this.selectedFiles,
|
||||||
|
required this.site,
|
||||||
|
required this.onUploadComplete,
|
||||||
|
this.relativePaths,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final formKey = useMemoized(() => GlobalKey<FormState>());
|
||||||
|
final pathController = useTextEditingController(text: '/');
|
||||||
|
final isUploading = useState(false);
|
||||||
|
final progressStates = useState<List<Map<String, dynamic>>>(
|
||||||
|
selectedFiles
|
||||||
|
.map(
|
||||||
|
(file) => {
|
||||||
|
'fileName':
|
||||||
|
relativePaths?[selectedFiles.indexOf(file)] ??
|
||||||
|
file.path.split('/').last,
|
||||||
|
'progress': 0.0,
|
||||||
|
'status':
|
||||||
|
'pending', // 'pending', 'uploading', 'completed', 'error'
|
||||||
|
'error': null,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Calculate overall progress
|
||||||
|
final overallProgress =
|
||||||
|
progressStates.value.isNotEmpty
|
||||||
|
? progressStates.value
|
||||||
|
.map((e) => e['progress'] as double)
|
||||||
|
.reduce((a, b) => a + b) /
|
||||||
|
progressStates.value.length
|
||||||
|
: 0.0;
|
||||||
|
|
||||||
|
final overallStatus =
|
||||||
|
progressStates.value.isEmpty
|
||||||
|
? 'pending'
|
||||||
|
: progressStates.value.every((e) => e['status'] == 'completed')
|
||||||
|
? 'completed'
|
||||||
|
: progressStates.value.any((e) => e['status'] == 'error')
|
||||||
|
? 'error'
|
||||||
|
: progressStates.value.any((e) => e['status'] == 'uploading')
|
||||||
|
? 'uploading'
|
||||||
|
: 'pending';
|
||||||
|
|
||||||
|
final uploadFile = useCallback((
|
||||||
|
String basePath,
|
||||||
|
File file,
|
||||||
|
int index,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
progressStates.value[index]['status'] = 'uploading';
|
||||||
|
progressStates.value = [...progressStates.value];
|
||||||
|
|
||||||
|
final siteFilesNotifier = ref.read(
|
||||||
|
siteFilesNotifierProvider((siteId: site.id, path: null)).notifier,
|
||||||
|
);
|
||||||
|
|
||||||
|
final fileName = relativePaths?[index] ?? file.path.split('/').last;
|
||||||
|
final uploadPath =
|
||||||
|
basePath.endsWith('/')
|
||||||
|
? '$basePath$fileName'
|
||||||
|
: '$basePath/$fileName';
|
||||||
|
|
||||||
|
await siteFilesNotifier.uploadFile(file, uploadPath);
|
||||||
|
|
||||||
|
progressStates.value[index]['status'] = 'completed';
|
||||||
|
progressStates.value[index]['progress'] = 1.0;
|
||||||
|
progressStates.value = [...progressStates.value];
|
||||||
|
} catch (e) {
|
||||||
|
progressStates.value[index]['status'] = 'error';
|
||||||
|
progressStates.value[index]['error'] = e.toString();
|
||||||
|
progressStates.value = [...progressStates.value];
|
||||||
|
}
|
||||||
|
}, [ref, site.id, progressStates]);
|
||||||
|
|
||||||
|
final uploadAllFiles = useCallback(
|
||||||
|
() async {
|
||||||
|
if (!formKey.currentState!.validate()) return;
|
||||||
|
|
||||||
|
isUploading.value = true;
|
||||||
|
|
||||||
|
// Reset all progress states
|
||||||
|
for (int i = 0; i < progressStates.value.length; i++) {
|
||||||
|
progressStates.value[i]['status'] = 'pending';
|
||||||
|
progressStates.value[i]['progress'] = 0.0;
|
||||||
|
progressStates.value[i]['error'] = null;
|
||||||
|
}
|
||||||
|
progressStates.value = [...progressStates.value];
|
||||||
|
|
||||||
|
// Upload files sequentially (could be made parallel if needed)
|
||||||
|
for (int i = 0; i < selectedFiles.length; i++) {
|
||||||
|
final file = selectedFiles[i];
|
||||||
|
await uploadFile(pathController.text, file, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
isUploading.value = false;
|
||||||
|
|
||||||
|
// Close dialog if all uploads completed successfully
|
||||||
|
if (progressStates.value.every(
|
||||||
|
(state) => state['status'] == 'completed',
|
||||||
|
)) {
|
||||||
|
if (context.mounted) {
|
||||||
|
showSnackBar('All files uploaded successfully');
|
||||||
|
onUploadComplete();
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
uploadFile,
|
||||||
|
isUploading,
|
||||||
|
progressStates,
|
||||||
|
selectedFiles,
|
||||||
|
onUploadComplete,
|
||||||
|
context,
|
||||||
|
formKey,
|
||||||
|
pathController,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return SheetScaffold(
|
||||||
|
titleText: 'Upload Files',
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
child: Form(
|
||||||
|
key: formKey,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Upload path field
|
||||||
|
TextFormField(
|
||||||
|
controller: pathController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Upload Path',
|
||||||
|
hintText: '/ (root) or /assets/images/',
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Please enter an upload path';
|
||||||
|
}
|
||||||
|
if (!value.startsWith('/') && value != '/') {
|
||||||
|
return 'Path must start with /';
|
||||||
|
}
|
||||||
|
if (value.contains(' ')) {
|
||||||
|
return 'Path cannot contain spaces';
|
||||||
|
}
|
||||||
|
if (value.contains('//')) {
|
||||||
|
return 'Path cannot have consecutive slashes';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
onTapOutside:
|
||||||
|
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
),
|
||||||
|
const Gap(16),
|
||||||
|
Card(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// Overall progress
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'${(overallProgress * 100).toStringAsFixed(0)}% completed',
|
||||||
|
style: Theme.of(context).textTheme.titleSmall,
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
LinearProgressIndicator(value: overallProgress),
|
||||||
|
const Gap(8),
|
||||||
|
Text(
|
||||||
|
_getOverallStatusText(overallStatus),
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Divider
|
||||||
|
const Divider(height: 0),
|
||||||
|
// File list in expansion
|
||||||
|
ExpansionTile(
|
||||||
|
title: Text('${selectedFiles.length} files to upload'),
|
||||||
|
initiallyExpanded: selectedFiles.length <= 10,
|
||||||
|
children:
|
||||||
|
selectedFiles.map((file) {
|
||||||
|
final index = selectedFiles.indexOf(file);
|
||||||
|
final progressState = progressStates.value[index];
|
||||||
|
final displayName =
|
||||||
|
progressState['fileName'] as String;
|
||||||
|
return ListTile(
|
||||||
|
leading: _getStatusIcon(
|
||||||
|
progressState['status'] as String,
|
||||||
|
),
|
||||||
|
title: Text(displayName),
|
||||||
|
subtitle: Text(
|
||||||
|
'Size: ${(file.lengthSync() / 1024).toStringAsFixed(1)} KB',
|
||||||
|
),
|
||||||
|
dense: true,
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(24),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: OutlinedButton(
|
||||||
|
onPressed:
|
||||||
|
isUploading.value
|
||||||
|
? null
|
||||||
|
: () => Navigator.of(context).pop(),
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(12),
|
||||||
|
Expanded(
|
||||||
|
child: FilledButton(
|
||||||
|
onPressed: isUploading.value ? null : uploadAllFiles,
|
||||||
|
child: Text(
|
||||||
|
isUploading.value
|
||||||
|
? 'Uploading...'
|
||||||
|
: 'Upload ${selectedFiles.length} File${selectedFiles.length == 1 ? '' : 's'}',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Icon _getStatusIcon(String status) {
|
||||||
|
switch (status) {
|
||||||
|
case 'completed':
|
||||||
|
return const Icon(Symbols.check_circle, color: Colors.green);
|
||||||
|
case 'uploading':
|
||||||
|
return const Icon(Symbols.sync);
|
||||||
|
case 'error':
|
||||||
|
return const Icon(Symbols.error, color: Colors.red);
|
||||||
|
default:
|
||||||
|
return const Icon(Symbols.pending);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getOverallStatusText(String status) {
|
||||||
|
switch (status) {
|
||||||
|
case 'completed':
|
||||||
|
return 'All uploads completed';
|
||||||
|
case 'error':
|
||||||
|
return 'Some uploads failed';
|
||||||
|
case 'uploading':
|
||||||
|
return 'Uploading in progress';
|
||||||
|
default:
|
||||||
|
return 'Ready to upload';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
53
lib/widgets/sites/info_row.dart
Normal file
53
lib/widgets/sites/info_row.dart
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
|
||||||
|
class InfoRow extends StatelessWidget {
|
||||||
|
final String label;
|
||||||
|
final String value;
|
||||||
|
final IconData icon;
|
||||||
|
final bool monospace;
|
||||||
|
final VoidCallback? onTap;
|
||||||
|
|
||||||
|
const InfoRow({
|
||||||
|
super.key,
|
||||||
|
required this.label,
|
||||||
|
required this.value,
|
||||||
|
required this.icon,
|
||||||
|
this.monospace = false,
|
||||||
|
this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
Widget valueWidget = Text(
|
||||||
|
value,
|
||||||
|
style:
|
||||||
|
monospace
|
||||||
|
? GoogleFonts.robotoMono(fontSize: 14)
|
||||||
|
: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
textAlign: TextAlign.end,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (onTap != null) valueWidget = InkWell(onTap: onTap, child: valueWidget);
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 20, color: Theme.of(context).colorScheme.primary),
|
||||||
|
const Gap(12),
|
||||||
|
Expanded(
|
||||||
|
flex: 2,
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(12),
|
||||||
|
Expanded(flex: 3, child: valueWidget),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
397
lib/widgets/sites/page_form.dart
Normal file
397
lib/widgets/sites/page_form.dart
Normal file
@@ -0,0 +1,397 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:island/models/publication_site.dart';
|
||||||
|
import 'package:island/pods/site_pages.dart';
|
||||||
|
import 'package:island/widgets/alert.dart';
|
||||||
|
import 'package:island/widgets/content/sheet.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
|
||||||
|
class PageForm extends HookConsumerWidget {
|
||||||
|
final SnPublicationSite site;
|
||||||
|
final String pubName;
|
||||||
|
final SnPublicationPage? page; // null for create, non-null for edit
|
||||||
|
|
||||||
|
const PageForm({
|
||||||
|
super.key,
|
||||||
|
required this.site,
|
||||||
|
required this.pubName,
|
||||||
|
this.page,
|
||||||
|
});
|
||||||
|
|
||||||
|
int _getPageType(SnPublicationPage? page) {
|
||||||
|
if (page == null) return 0; // Default to HTML
|
||||||
|
// Check config structure to determine type
|
||||||
|
return page.config?.containsKey('target') == true ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final formKey = useMemoized(() => GlobalKey<FormState>());
|
||||||
|
final pathController = useTextEditingController(text: page?.path ?? '/');
|
||||||
|
|
||||||
|
// Determine initial type and create appropriate controllers
|
||||||
|
final initialType = _getPageType(page);
|
||||||
|
final pageType = useState(initialType);
|
||||||
|
|
||||||
|
final htmlController = useTextEditingController(
|
||||||
|
text:
|
||||||
|
pageType.value == 0
|
||||||
|
? (page?.config?['html'] ?? page?.config?['content'] ?? '')
|
||||||
|
: '',
|
||||||
|
);
|
||||||
|
final titleController = useTextEditingController(
|
||||||
|
text: pageType.value == 0 ? (page?.config?['title'] ?? '') : '',
|
||||||
|
);
|
||||||
|
final targetController = useTextEditingController(
|
||||||
|
text: pageType.value == 1 ? (page?.config?['target'] ?? '') : '',
|
||||||
|
);
|
||||||
|
|
||||||
|
final isLoading = useState(false);
|
||||||
|
|
||||||
|
// Update controllers when page type changes
|
||||||
|
useEffect(() {
|
||||||
|
pageType.addListener(() {
|
||||||
|
if (pageType.value == 0) {
|
||||||
|
// HTML mode
|
||||||
|
htmlController.text =
|
||||||
|
page?.config?['html'] ?? page?.config?['content'] ?? '';
|
||||||
|
titleController.text = page?.config?['title'] ?? '';
|
||||||
|
targetController.clear();
|
||||||
|
} else {
|
||||||
|
// Redirect mode
|
||||||
|
htmlController.clear();
|
||||||
|
titleController.clear();
|
||||||
|
targetController.text = page?.config?['target'] ?? '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}, [pageType]);
|
||||||
|
|
||||||
|
// Initialize form fields when page data is loaded
|
||||||
|
useEffect(() {
|
||||||
|
if (page?.path != null && pathController.text == '/') {
|
||||||
|
pathController.text = page!.path!;
|
||||||
|
if (pageType.value == 0) {
|
||||||
|
htmlController.text =
|
||||||
|
page!.config?['html'] ?? page!.config?['content'] ?? '';
|
||||||
|
titleController.text = page!.config?['title'] ?? '';
|
||||||
|
} else {
|
||||||
|
targetController.text = page!.config?['target'] ?? '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [page]);
|
||||||
|
|
||||||
|
final savePage = useCallback(() async {
|
||||||
|
if (!formKey.currentState!.validate()) return;
|
||||||
|
|
||||||
|
isLoading.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final pagesNotifier = ref.read(
|
||||||
|
sitePagesNotifierProvider((
|
||||||
|
pubName: pubName,
|
||||||
|
siteSlug: site.slug,
|
||||||
|
)).notifier,
|
||||||
|
);
|
||||||
|
|
||||||
|
late final Map<String, dynamic> pageData;
|
||||||
|
|
||||||
|
if (pageType.value == 0) {
|
||||||
|
// HTML page
|
||||||
|
pageData = {
|
||||||
|
'type': 0,
|
||||||
|
'path': pathController.text,
|
||||||
|
'config': {
|
||||||
|
'title': titleController.text,
|
||||||
|
'html': htmlController.text,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Redirect page
|
||||||
|
pageData = {
|
||||||
|
'type': 1,
|
||||||
|
'path': pathController.text,
|
||||||
|
'config': {'target': targetController.text},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (page == null) {
|
||||||
|
// Create new page
|
||||||
|
await pagesNotifier.createPage(pageData);
|
||||||
|
} else {
|
||||||
|
// Update existing page
|
||||||
|
await pagesNotifier.updatePage(page!.id, pageData);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
showSnackBar(
|
||||||
|
page == null
|
||||||
|
? 'Page created successfully'
|
||||||
|
: 'Page updated successfully',
|
||||||
|
);
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showErrorAlert(e);
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}, [pageType, pubName, site.slug, page]);
|
||||||
|
|
||||||
|
final deletePage = useCallback(() async {
|
||||||
|
if (page == null) return; // Shouldn't happen for editing
|
||||||
|
|
||||||
|
final confirmed = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder:
|
||||||
|
(context) => AlertDialog(
|
||||||
|
title: const Text('Delete Page'),
|
||||||
|
content: const Text('Are you sure you want to delete this page?'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(false),
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(true),
|
||||||
|
child: const Text('Delete'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (confirmed != true) return;
|
||||||
|
|
||||||
|
isLoading.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final pagesNotifier = ref.read(
|
||||||
|
sitePagesNotifierProvider((
|
||||||
|
pubName: pubName,
|
||||||
|
siteSlug: site.slug,
|
||||||
|
)).notifier,
|
||||||
|
);
|
||||||
|
|
||||||
|
await pagesNotifier.deletePage(page!.id);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
showSnackBar('Page deleted successfully');
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showErrorAlert(e);
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}, [pubName, site.slug, page, context]);
|
||||||
|
|
||||||
|
return SheetScaffold(
|
||||||
|
titleText: page == null ? 'Create Page' : 'Edit Page',
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Form(
|
||||||
|
key: formKey,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// Page type selector
|
||||||
|
DropdownButtonFormField<int>(
|
||||||
|
value: pageType.value,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Page Type',
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
items: const [
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: 0,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Symbols.code, size: 20),
|
||||||
|
Gap(8),
|
||||||
|
Text('HTML Page'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: 1,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Symbols.link, size: 20),
|
||||||
|
Gap(8),
|
||||||
|
Text('Redirect Page'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value != null) {
|
||||||
|
pageType.value = value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null) {
|
||||||
|
return 'Please select a page type';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
).padding(all: 20),
|
||||||
|
// Conditional form fields based on page type
|
||||||
|
if (pageType.value == 0) ...[
|
||||||
|
// HTML Page fields
|
||||||
|
TextFormField(
|
||||||
|
controller: pathController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Page Path',
|
||||||
|
hintText: '/about, /contact, etc.',
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Please enter a page path';
|
||||||
|
}
|
||||||
|
if (!RegExp(r'^[a-zA-Z0-9\-/_]+$').hasMatch(value)) {
|
||||||
|
return 'Page path can only contain letters, numbers, hyphens, underscores, and slashes';
|
||||||
|
}
|
||||||
|
if (!value.startsWith('/')) {
|
||||||
|
return 'Page path must start with /';
|
||||||
|
}
|
||||||
|
if (value.contains('//')) {
|
||||||
|
return 'Page path cannot have consecutive slashes';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
onTapOutside:
|
||||||
|
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
).padding(horizontal: 20),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
controller: titleController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Page Title',
|
||||||
|
hintText: 'About Us, Contact, etc.',
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Please enter a page title';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
onTapOutside:
|
||||||
|
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
).padding(horizontal: 20),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
controller: htmlController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Page Content (HTML)',
|
||||||
|
hintText:
|
||||||
|
'<h1>Hello World</h1><p>This is my page content...</p>',
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
|
),
|
||||||
|
alignLabelWithHint: true,
|
||||||
|
),
|
||||||
|
maxLines: 10,
|
||||||
|
onTapOutside:
|
||||||
|
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Please enter HTML content for the page';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
).padding(horizontal: 20),
|
||||||
|
] else ...[
|
||||||
|
// Redirect Page fields
|
||||||
|
TextFormField(
|
||||||
|
controller: pathController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Page Path',
|
||||||
|
hintText: '/old-page, /redirect, etc.',
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Please enter a page path';
|
||||||
|
}
|
||||||
|
if (!RegExp(r'^[a-zA-Z0-9\-/_]+$').hasMatch(value)) {
|
||||||
|
return 'Page path can only contain letters, numbers, hyphens, underscores, and slashes';
|
||||||
|
}
|
||||||
|
if (!value.startsWith('/')) {
|
||||||
|
return 'Page path must start with /';
|
||||||
|
}
|
||||||
|
if (value.contains('//')) {
|
||||||
|
return 'Page path cannot have consecutive slashes';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
onTapOutside:
|
||||||
|
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
).padding(horizontal: 20),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
controller: targetController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Redirect Target',
|
||||||
|
hintText: '/new-page, https://example.com, etc.',
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Please enter a redirect target';
|
||||||
|
}
|
||||||
|
if (!value.startsWith('/') &&
|
||||||
|
!value.startsWith('http://') &&
|
||||||
|
!value.startsWith('https://')) {
|
||||||
|
return 'Target must be a relative path (/) or absolute URL (http/https)';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
onTapOutside:
|
||||||
|
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
).padding(horizontal: 20),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
if (page != null) ...[
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: deletePage,
|
||||||
|
icon: const Icon(Symbols.delete_forever),
|
||||||
|
label: const Text('Delete Page'),
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
foregroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
).alignment(Alignment.centerRight),
|
||||||
|
const Spacer(),
|
||||||
|
] else
|
||||||
|
const Spacer(),
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: savePage,
|
||||||
|
icon: const Icon(Symbols.save),
|
||||||
|
label: const Text('Save Page'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 20, vertical: 16),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
125
lib/widgets/sites/page_item.dart
Normal file
125
lib/widgets/sites/page_item.dart
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
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/site_pages.dart';
|
||||||
|
import 'package:island/widgets/alert.dart';
|
||||||
|
import 'package:island/widgets/sites/page_form.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
|
class PageItem extends HookConsumerWidget {
|
||||||
|
final SnPublicationPage page;
|
||||||
|
final SnPublicationSite site;
|
||||||
|
final String pubName;
|
||||||
|
|
||||||
|
const PageItem({
|
||||||
|
super.key,
|
||||||
|
required this.page,
|
||||||
|
required this.site,
|
||||||
|
required this.pubName,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
|
elevation: 0,
|
||||||
|
child: ListTile(
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||||
|
),
|
||||||
|
leading: Icon(Symbols.article, color: theme.colorScheme.primary),
|
||||||
|
title: Text(page.path ?? '/'),
|
||||||
|
subtitle: Text(page.config?['title'] ?? 'Untitled'),
|
||||||
|
trailing: PopupMenuButton<String>(
|
||||||
|
itemBuilder:
|
||||||
|
(context) => [
|
||||||
|
PopupMenuItem(
|
||||||
|
value: 'edit',
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.edit),
|
||||||
|
const Gap(16),
|
||||||
|
Text('edit'.tr()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
value: 'delete',
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.delete, color: Colors.red),
|
||||||
|
const Gap(16),
|
||||||
|
Text('delete'.tr()).textColor(Colors.red),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onSelected: (value) async {
|
||||||
|
switch (value) {
|
||||||
|
case 'edit':
|
||||||
|
// Open page edit dialog
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
builder:
|
||||||
|
(context) =>
|
||||||
|
PageForm(site: site, pubName: pubName, page: page),
|
||||||
|
).then((_) {
|
||||||
|
// Refresh pages after editing
|
||||||
|
ref.invalidate(sitePagesProvider(pubName, site.slug));
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'delete':
|
||||||
|
final confirmed = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder:
|
||||||
|
(context) => AlertDialog(
|
||||||
|
title: const Text('Delete Page'),
|
||||||
|
content: const Text(
|
||||||
|
'Are you sure you want to delete this page?',
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(false),
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(true),
|
||||||
|
child: const Text('Delete'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (confirmed == true) {
|
||||||
|
try {
|
||||||
|
await ref
|
||||||
|
.read(
|
||||||
|
sitePagesNotifierProvider((
|
||||||
|
pubName: pubName,
|
||||||
|
siteSlug: site.slug,
|
||||||
|
)).notifier,
|
||||||
|
)
|
||||||
|
.deletePage(page.id);
|
||||||
|
showSnackBar('Page deleted successfully');
|
||||||
|
} catch (e) {
|
||||||
|
showErrorAlert(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
launchUrlString('https://${site.slug}.solian.page${page.path ?? ''}');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
123
lib/widgets/sites/pages_section.dart
Normal file
123
lib/widgets/sites/pages_section.dart
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
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/site_pages.dart';
|
||||||
|
import 'package:island/widgets/sites/page_form.dart';
|
||||||
|
import 'package:island/widgets/sites/page_item.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
|
||||||
|
class PagesSection extends HookConsumerWidget {
|
||||||
|
final SnPublicationSite site;
|
||||||
|
final String pubName;
|
||||||
|
|
||||||
|
const PagesSection({super.key, required this.site, required this.pubName});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final pagesAsync = ref.watch(sitePagesProvider(pubName, site.slug));
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.article, size: 20),
|
||||||
|
const Gap(8),
|
||||||
|
Text(
|
||||||
|
'sitePages'.tr(),
|
||||||
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
// Open page creation dialog
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
builder:
|
||||||
|
(context) => PageForm(site: site, pubName: pubName),
|
||||||
|
).then((_) {
|
||||||
|
// Refresh pages after creation
|
||||||
|
ref.invalidate(sitePagesProvider(pubName, site.slug));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: const Icon(Symbols.add),
|
||||||
|
visualDensity: const VisualDensity(
|
||||||
|
horizontal: -4,
|
||||||
|
vertical: -4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Gap(16),
|
||||||
|
pagesAsync.when(
|
||||||
|
data: (pages) {
|
||||||
|
if (pages.isEmpty) {
|
||||||
|
return Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(32),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Symbols.article,
|
||||||
|
size: 48,
|
||||||
|
color: theme.colorScheme.outline,
|
||||||
|
),
|
||||||
|
const Gap(16),
|
||||||
|
Text(
|
||||||
|
'noPagesYet'.tr(),
|
||||||
|
style: theme.textTheme.bodyLarge,
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
Text(
|
||||||
|
'createFirstPage'.tr(),
|
||||||
|
style: theme.textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
itemCount: pages.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final page = pages[index];
|
||||||
|
return PageItem(page: page, site: site, pubName: pubName);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
|
error:
|
||||||
|
(error, stack) => Center(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text('failedToLoadPages'.tr()),
|
||||||
|
const Gap(8),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed:
|
||||||
|
() => ref.invalidate(
|
||||||
|
sitePagesProvider(pubName, site.slug),
|
||||||
|
),
|
||||||
|
child: Text('retry'.tr()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
99
lib/widgets/sites/site_action_menu.dart
Normal file
99
lib/widgets/sites/site_action_menu.dart
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
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/screens/creators/sites/site_detail.dart';
|
||||||
|
import 'package:island/screens/creators/sites/site_edit.dart';
|
||||||
|
import 'package:island/widgets/alert.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
|
||||||
|
class SiteActionMenu extends HookConsumerWidget {
|
||||||
|
final SnPublicationSite site;
|
||||||
|
final String pubName;
|
||||||
|
|
||||||
|
const SiteActionMenu({super.key, required this.site, required this.pubName});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return PopupMenuButton<String>(
|
||||||
|
itemBuilder:
|
||||||
|
(context) => [
|
||||||
|
PopupMenuItem(
|
||||||
|
value: 'edit',
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Symbols.edit,
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
const Gap(16),
|
||||||
|
Text('edit'.tr()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const PopupMenuDivider(),
|
||||||
|
PopupMenuItem(
|
||||||
|
value: 'delete',
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.delete, color: Colors.red),
|
||||||
|
const Gap(16),
|
||||||
|
Text('delete'.tr()).textColor(Colors.red),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onSelected: (value) async {
|
||||||
|
switch (value) {
|
||||||
|
case 'edit':
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
builder:
|
||||||
|
(context) => SiteForm(pubName: pubName, siteSlug: site.slug),
|
||||||
|
).then((_) {
|
||||||
|
// Refresh site data after potential edit
|
||||||
|
ref.invalidate(publicationSiteDetailProvider(pubName, site.slug));
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'delete':
|
||||||
|
final confirmed = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder:
|
||||||
|
(context) => AlertDialog(
|
||||||
|
title: Text('deleteSite'.tr()),
|
||||||
|
content: Text('publicationSiteDeleteConfirm'.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}');
|
||||||
|
if (context.mounted) {
|
||||||
|
showSnackBar('siteDeletedSuccess'.tr());
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showErrorAlert(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
109
lib/widgets/sites/site_detail_content.dart
Normal file
109
lib/widgets/sites/site_detail_content.dart
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
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/widgets/sites/file_management_section.dart';
|
||||||
|
import 'package:island/widgets/sites/file_management_action_section.dart';
|
||||||
|
import 'package:island/widgets/sites/info_row.dart';
|
||||||
|
import 'package:island/widgets/sites/pages_section.dart';
|
||||||
|
import 'package:island/services/time.dart';
|
||||||
|
import 'package:island/widgets/extended_refresh_indicator.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
import 'package:island/screens/creators/sites/site_detail.dart';
|
||||||
|
|
||||||
|
class SiteDetailContent extends HookConsumerWidget {
|
||||||
|
final SnPublicationSite site;
|
||||||
|
final String pubName;
|
||||||
|
|
||||||
|
const SiteDetailContent({
|
||||||
|
super.key,
|
||||||
|
required this.site,
|
||||||
|
required this.pubName,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return ExtendedRefreshIndicator(
|
||||||
|
onRefresh:
|
||||||
|
() async =>
|
||||||
|
ref.invalidate(publicationSiteDetailProvider(pubName, site.slug)),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Site Info Card
|
||||||
|
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: 'Mode',
|
||||||
|
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),
|
||||||
|
// Pages Section
|
||||||
|
PagesSection(site: site, pubName: pubName),
|
||||||
|
FileManagementSection(site: site, pubName: pubName),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -381,6 +381,10 @@ class _EmbeddedPackSwitcherState extends State<_EmbeddedPackSwitcher> {
|
|||||||
return Tooltip(
|
return Tooltip(
|
||||||
message: packs[i].name,
|
message: packs[i].name,
|
||||||
child: FilterChip(
|
child: FilterChip(
|
||||||
|
visualDensity: const VisualDensity(
|
||||||
|
horizontal: 0,
|
||||||
|
vertical: -4,
|
||||||
|
),
|
||||||
label: Text(packs[i].name, overflow: TextOverflow.ellipsis),
|
label: Text(packs[i].name, overflow: TextOverflow.ellipsis),
|
||||||
selected: selected,
|
selected: selected,
|
||||||
onSelected: (_) {
|
onSelected: (_) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
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';
|
||||||
@@ -29,7 +29,49 @@ class UploadOverlay extends HookConsumerWidget {
|
|||||||
.toList()
|
.toList()
|
||||||
..sort((a, b) => b.createdAt.compareTo(a.createdAt)); // Newest first
|
..sort((a, b) => b.createdAt.compareTo(a.createdAt)); // Newest first
|
||||||
|
|
||||||
final isVisible = activeTasks.isNotEmpty;
|
final isVisibleOverride = useState<bool?>(null);
|
||||||
|
final pendingHide = useState(false);
|
||||||
|
final isExpandedLocal = useState(false);
|
||||||
|
final autoHideTimer = useState<Timer?>(null);
|
||||||
|
|
||||||
|
final allFinished = activeTasks.every(
|
||||||
|
(task) =>
|
||||||
|
task.status == DriveTaskStatus.completed ||
|
||||||
|
task.status == DriveTaskStatus.failed ||
|
||||||
|
task.status == DriveTaskStatus.cancelled ||
|
||||||
|
task.status == DriveTaskStatus.expired,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Auto-hide timer effect
|
||||||
|
useEffect(() {
|
||||||
|
// Reset pendingHide if there are unfinished tasks
|
||||||
|
final hasUnfinishedTasks = activeTasks.any(
|
||||||
|
(task) =>
|
||||||
|
task.status == DriveTaskStatus.pending ||
|
||||||
|
task.status == DriveTaskStatus.inProgress ||
|
||||||
|
task.status == DriveTaskStatus.paused,
|
||||||
|
);
|
||||||
|
if (hasUnfinishedTasks && pendingHide.value) {
|
||||||
|
pendingHide.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
autoHideTimer.value?.cancel();
|
||||||
|
if (allFinished &&
|
||||||
|
activeTasks.isNotEmpty &&
|
||||||
|
!isExpandedLocal.value &&
|
||||||
|
!pendingHide.value) {
|
||||||
|
autoHideTimer.value = Timer(const Duration(seconds: 3), () {
|
||||||
|
pendingHide.value = true;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
autoHideTimer.value?.cancel();
|
||||||
|
autoHideTimer.value = null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [allFinished, activeTasks, isExpandedLocal.value, pendingHide.value]);
|
||||||
|
final isVisible =
|
||||||
|
(isVisibleOverride.value ?? activeTasks.isNotEmpty) &&
|
||||||
|
!pendingHide.value;
|
||||||
final slideController = useAnimationController(
|
final slideController = useAnimationController(
|
||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
);
|
);
|
||||||
@@ -63,6 +105,8 @@ class UploadOverlay extends HookConsumerWidget {
|
|||||||
position: slideAnimation,
|
position: slideAnimation,
|
||||||
child: _UploadOverlayContent(
|
child: _UploadOverlayContent(
|
||||||
activeTasks: activeTasks,
|
activeTasks: activeTasks,
|
||||||
|
isExpanded: isExpandedLocal.value,
|
||||||
|
onExpansionChanged: (expanded) => isExpandedLocal.value = expanded,
|
||||||
).padding(bottom: 16 + MediaQuery.of(context).padding.bottom),
|
).padding(bottom: 16 + MediaQuery.of(context).padding.bottom),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -71,12 +115,17 @@ class UploadOverlay extends HookConsumerWidget {
|
|||||||
|
|
||||||
class _UploadOverlayContent extends HookConsumerWidget {
|
class _UploadOverlayContent extends HookConsumerWidget {
|
||||||
final List<DriveTask> activeTasks;
|
final List<DriveTask> activeTasks;
|
||||||
|
final bool isExpanded;
|
||||||
|
final Function(bool)? onExpansionChanged;
|
||||||
|
|
||||||
const _UploadOverlayContent({required this.activeTasks});
|
const _UploadOverlayContent({
|
||||||
|
required this.activeTasks,
|
||||||
|
required this.isExpanded,
|
||||||
|
this.onExpansionChanged,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final isExpanded = useState(false);
|
|
||||||
final animationController = useAnimationController(
|
final animationController = useAnimationController(
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
initialValue: 0.0,
|
initialValue: 0.0,
|
||||||
@@ -91,15 +140,17 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
if (isExpanded.value) {
|
if (isExpanded) {
|
||||||
animationController.forward();
|
animationController.forward();
|
||||||
} else {
|
} else {
|
||||||
animationController.reverse();
|
animationController.reverse();
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, [isExpanded.value]);
|
}, [isExpanded]);
|
||||||
|
|
||||||
final isMobile = MediaQuery.of(context).size.width < 600;
|
final isMobile = !isWideScreen(context);
|
||||||
|
|
||||||
|
final taskNotifier = ref.read(uploadTasksProvider.notifier);
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
@@ -108,7 +159,7 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
|||||||
right: isMobile ? 16 : 24,
|
right: isMobile ? 16 : 24,
|
||||||
),
|
),
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () => isExpanded.value = !isExpanded.value,
|
onTap: () => onExpansionChanged?.call(!isExpanded),
|
||||||
child: AnimatedBuilder(
|
child: AnimatedBuilder(
|
||||||
animation: animationController,
|
animation: animationController,
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
@@ -142,8 +193,8 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: Icon(
|
child: Icon(
|
||||||
key: ValueKey(isExpanded.value),
|
key: ValueKey(isExpanded),
|
||||||
isExpanded.value
|
isExpanded
|
||||||
? Symbols.list_rounded
|
? Symbols.list_rounded
|
||||||
: _getOverallStatusIcon(activeTasks),
|
: _getOverallStatusIcon(activeTasks),
|
||||||
size: 24,
|
size: 24,
|
||||||
@@ -159,7 +210,7 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
isExpanded.value
|
isExpanded
|
||||||
? 'uploadTasks'.tr()
|
? 'uploadTasks'.tr()
|
||||||
: _getOverallStatusText(activeTasks),
|
: _getOverallStatusText(activeTasks),
|
||||||
style: Theme.of(context)
|
style: Theme.of(context)
|
||||||
@@ -169,8 +220,7 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
|||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
if (!isExpanded.value &&
|
if (!isExpanded && activeTasks.isNotEmpty)
|
||||||
activeTasks.isNotEmpty)
|
|
||||||
Text(
|
Text(
|
||||||
_getOverallProgressText(activeTasks),
|
_getOverallProgressText(activeTasks),
|
||||||
style: Theme.of(
|
style: Theme.of(
|
||||||
@@ -187,7 +237,7 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
|
|
||||||
// Progress indicator (collapsed)
|
// Progress indicator (collapsed)
|
||||||
if (!isExpanded.value)
|
if (!isExpanded)
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 32,
|
width: 32,
|
||||||
height: 32,
|
height: 32,
|
||||||
@@ -207,14 +257,14 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
|||||||
turns: opacityAnimation * 0.5,
|
turns: opacityAnimation * 0.5,
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
isExpanded.value
|
isExpanded
|
||||||
? Symbols.expand_more
|
? Symbols.expand_more
|
||||||
: Symbols.chevron_right,
|
: Symbols.chevron_right,
|
||||||
size: 20,
|
size: 20,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onPressed:
|
onPressed:
|
||||||
() => isExpanded.value = !isExpanded.value,
|
() => onExpansionChanged?.call(!isExpanded),
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
constraints: const BoxConstraints(),
|
constraints: const BoxConstraints(),
|
||||||
),
|
),
|
||||||
@@ -223,7 +273,7 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
|
|
||||||
// Expanded content
|
// Expanded content
|
||||||
if (isExpanded.value)
|
if (isExpanded)
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -243,7 +293,7 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
|||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
dense: true,
|
dense: true,
|
||||||
title: const Text('Clear Completed'),
|
title: const Text('clearCompleted').tr(),
|
||||||
leading: Icon(
|
leading: Icon(
|
||||||
Symbols.clear_all,
|
Symbols.clear_all,
|
||||||
size: 18,
|
size: 18,
|
||||||
@@ -253,9 +303,35 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
|||||||
).colorScheme.onSurfaceVariant,
|
).colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
ref
|
taskNotifier.clearCompletedTasks();
|
||||||
.read(uploadTasksProvider.notifier)
|
onExpansionChanged?.call(false);
|
||||||
.clearCompletedTasks();
|
},
|
||||||
|
|
||||||
|
tileColor:
|
||||||
|
Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.surfaceContainerHighest,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Clear all tasks button
|
||||||
|
if (activeTasks.any(
|
||||||
|
(task) =>
|
||||||
|
task.status != DriveTaskStatus.completed,
|
||||||
|
))
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: ListTile(
|
||||||
|
dense: true,
|
||||||
|
title: const Text('Clear All'),
|
||||||
|
leading: Icon(
|
||||||
|
Symbols.clear_all,
|
||||||
|
size: 18,
|
||||||
|
color:
|
||||||
|
Theme.of(context).colorScheme.error,
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
taskNotifier.clearAllTasks();
|
||||||
|
onExpansionChanged?.call(false);
|
||||||
},
|
},
|
||||||
tileColor:
|
tileColor:
|
||||||
Theme.of(
|
Theme.of(
|
||||||
@@ -318,6 +394,7 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
|||||||
IconData _getOverallStatusIcon(List<DriveTask> tasks) {
|
IconData _getOverallStatusIcon(List<DriveTask> tasks) {
|
||||||
if (tasks.isEmpty) return Symbols.upload;
|
if (tasks.isEmpty) return Symbols.upload;
|
||||||
|
|
||||||
|
final hasDownload = tasks.any((task) => task.type == 'FileDownload');
|
||||||
final hasInProgress = tasks.any(
|
final hasInProgress = tasks.any(
|
||||||
(task) => task.status == DriveTaskStatus.inProgress,
|
(task) => task.status == DriveTaskStatus.inProgress,
|
||||||
);
|
);
|
||||||
@@ -339,6 +416,9 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
|||||||
|
|
||||||
// Priority order: in progress > pending > paused > failed > completed
|
// Priority order: in progress > pending > paused > failed > completed
|
||||||
if (hasInProgress) {
|
if (hasInProgress) {
|
||||||
|
if (hasDownload) {
|
||||||
|
return Symbols.download;
|
||||||
|
}
|
||||||
return Symbols.upload;
|
return Symbols.upload;
|
||||||
} else if (hasPending) {
|
} else if (hasPending) {
|
||||||
return Symbols.schedule;
|
return Symbols.schedule;
|
||||||
@@ -356,6 +436,7 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
|||||||
String _getOverallStatusText(List<DriveTask> tasks) {
|
String _getOverallStatusText(List<DriveTask> tasks) {
|
||||||
if (tasks.isEmpty) return '0 tasks';
|
if (tasks.isEmpty) return '0 tasks';
|
||||||
|
|
||||||
|
final hasDownload = tasks.any((task) => task.type == 'FileDownload');
|
||||||
final hasInProgress = tasks.any(
|
final hasInProgress = tasks.any(
|
||||||
(task) => task.status == DriveTaskStatus.inProgress,
|
(task) => task.status == DriveTaskStatus.inProgress,
|
||||||
);
|
);
|
||||||
@@ -377,7 +458,11 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
|||||||
|
|
||||||
// Priority order: in progress > pending > paused > failed > completed
|
// Priority order: in progress > pending > paused > failed > completed
|
||||||
if (hasInProgress) {
|
if (hasInProgress) {
|
||||||
|
if (hasDownload) {
|
||||||
|
return '${tasks.length} ${'downloading'.tr()}';
|
||||||
|
} else {
|
||||||
return '${tasks.length} ${'uploading'.tr()}';
|
return '${tasks.length} ${'uploading'.tr()}';
|
||||||
|
}
|
||||||
} else if (hasPending) {
|
} else if (hasPending) {
|
||||||
return '${tasks.length} ${'pending'.tr()}';
|
return '${tasks.length} ${'pending'.tr()}';
|
||||||
} else if (hasPaused) {
|
} else if (hasPaused) {
|
||||||
@@ -519,7 +604,10 @@ class _UploadTaskTileState extends State<UploadTaskTile>
|
|||||||
color = Theme.of(context).colorScheme.secondary;
|
color = Theme.of(context).colorScheme.secondary;
|
||||||
break;
|
break;
|
||||||
case DriveTaskStatus.inProgress:
|
case DriveTaskStatus.inProgress:
|
||||||
icon = Symbols.upload;
|
icon =
|
||||||
|
widget.task.type == 'FileDownload'
|
||||||
|
? Symbols.download
|
||||||
|
: Symbols.upload;
|
||||||
color = Theme.of(context).colorScheme.primary;
|
color = Theme.of(context).colorScheme.primary;
|
||||||
break;
|
break;
|
||||||
case DriveTaskStatus.paused:
|
case DriveTaskStatus.paused:
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:island/models/wallet.dart';
|
import 'package:island/models/wallet.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
import 'package:island/pods/userinfo.dart';
|
import 'package:island/pods/userinfo.dart';
|
||||||
|
import 'package:island/widgets/alert.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
|
||||||
@@ -56,7 +57,7 @@ class FundEnvelopeWidget extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
'Failed to load fund envelope',
|
'fundEnvelopeLoadFailed'.tr(),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Theme.of(context).colorScheme.error,
|
color: Theme.of(context).colorScheme.error,
|
||||||
),
|
),
|
||||||
@@ -87,7 +88,7 @@ class FundEnvelopeWidget extends HookConsumerWidget {
|
|||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
'Fund Envelope',
|
'fundEnvelope'.tr(),
|
||||||
style: Theme.of(context).textTheme.titleMedium
|
style: Theme.of(context).textTheme.titleMedium
|
||||||
?.copyWith(fontWeight: FontWeight.w600),
|
?.copyWith(fontWeight: FontWeight.w600),
|
||||||
),
|
),
|
||||||
@@ -115,7 +116,12 @@ class FundEnvelopeWidget extends HookConsumerWidget {
|
|||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
if (fund.remainingAmount != fund.totalAmount)
|
if (fund.remainingAmount != fund.totalAmount)
|
||||||
Text(
|
Text(
|
||||||
'Remaining: ${fund.remainingAmount.toStringAsFixed(2)} ${fund.currency}',
|
'fundEnvelopeRemaining'.tr(
|
||||||
|
args: [
|
||||||
|
fund.remainingAmount.toStringAsFixed(2),
|
||||||
|
fund.currency,
|
||||||
|
],
|
||||||
|
),
|
||||||
style: Theme.of(
|
style: Theme.of(
|
||||||
context,
|
context,
|
||||||
).textTheme.bodySmall?.copyWith(
|
).textTheme.bodySmall?.copyWith(
|
||||||
@@ -125,7 +131,13 @@ class FundEnvelopeWidget extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'Split: ${fund.splitType == 0 ? 'Evenly' : 'Randomly'}',
|
'fundEnvelopeSplit'.tr(
|
||||||
|
args: [
|
||||||
|
fund.splitType == 0
|
||||||
|
? 'fundEnvelopeSplitEvenly'.tr()
|
||||||
|
: 'fundEnvelopeSplitRandomly'.tr(),
|
||||||
|
],
|
||||||
|
),
|
||||||
style: Theme.of(
|
style: Theme.of(
|
||||||
context,
|
context,
|
||||||
).textTheme.bodySmall?.copyWith(
|
).textTheme.bodySmall?.copyWith(
|
||||||
@@ -244,20 +256,10 @@ class FundEnvelopeWidget extends HookConsumerWidget {
|
|||||||
|
|
||||||
if (dialogContext.mounted) {
|
if (dialogContext.mounted) {
|
||||||
Navigator.of(dialogContext).pop();
|
Navigator.of(dialogContext).pop();
|
||||||
ScaffoldMessenger.of(dialogContext).showSnackBar(
|
showSnackBar('fundEnvelopeClaimSuccess'.tr());
|
||||||
SnackBar(content: Text('Fund claimed successfully!'.tr())),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (dialogContext.mounted) {
|
showErrorAlert(e);
|
||||||
ScaffoldMessenger.of(dialogContext).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text('Failed to claim fund: $e'),
|
|
||||||
backgroundColor:
|
|
||||||
Theme.of(dialogContext).colorScheme.error,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -270,23 +272,23 @@ class FundEnvelopeWidget extends HookConsumerWidget {
|
|||||||
|
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 0:
|
case 0:
|
||||||
text = 'Created';
|
text = 'fundEnvelopeStatusCreated'.tr();
|
||||||
color = Colors.blue;
|
color = Colors.blue;
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
text = 'Partially Claimed';
|
text = 'fundEnvelopeStatusPartial'.tr();
|
||||||
color = Colors.orange;
|
color = Colors.orange;
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
text = 'Fully Claimed';
|
text = 'fundEnvelopeStatusCompleted'.tr();
|
||||||
color = Colors.green;
|
color = Colors.green;
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
text = 'Expired';
|
text = 'fundEnvelopeStatusExpired'.tr();
|
||||||
color = Colors.red;
|
color = Colors.red;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
text = 'Unknown';
|
text = 'fundEnvelopeStatusUnknown'.tr();
|
||||||
color = Colors.grey;
|
color = Colors.grey;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -348,7 +350,9 @@ class FundEnvelopeWidget extends HookConsumerWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Recipients ($claimedCount/$totalCount claimed)',
|
'fundEnvelopeRecipients'.tr(
|
||||||
|
args: [claimedCount.toString(), totalCount.toString()],
|
||||||
|
),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
@@ -371,15 +375,26 @@ class FundEnvelopeWidget extends HookConsumerWidget {
|
|||||||
final difference = date.difference(now);
|
final difference = date.difference(now);
|
||||||
|
|
||||||
if (difference.isNegative) {
|
if (difference.isNegative) {
|
||||||
return 'Expired ${difference.inDays.abs()} days ago';
|
final days = difference.inDays.abs();
|
||||||
|
return 'fundEnvelopeExpiredDaysAgo'.plural(
|
||||||
|
days,
|
||||||
|
args: [days.toString()],
|
||||||
|
);
|
||||||
} else if (difference.inDays == 0) {
|
} else if (difference.inDays == 0) {
|
||||||
final hours = difference.inHours;
|
final hours = difference.inHours;
|
||||||
if (hours == 0) {
|
if (hours == 0) {
|
||||||
return 'Expires soon';
|
return 'fundEnvelopeExpiresSoon'.tr();
|
||||||
}
|
}
|
||||||
return 'Expires in $hours hour${hours == 1 ? '' : 's'}';
|
return 'fundEnvelopeExpiresInHours'.plural(
|
||||||
|
hours,
|
||||||
|
args: [hours.toString()],
|
||||||
|
);
|
||||||
} else if (difference.inDays < 7) {
|
} else if (difference.inDays < 7) {
|
||||||
return 'Expires in ${difference.inDays} day${difference.inDays == 1 ? '' : 's'}';
|
final days = difference.inDays;
|
||||||
|
return 'fundEnvelopeExpiresInDays'.plural(
|
||||||
|
days,
|
||||||
|
args: [days.toString()],
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return '${date.day}/${date.month}/${date.year}';
|
return '${date.day}/${date.month}/${date.year}';
|
||||||
}
|
}
|
||||||
@@ -458,7 +473,13 @@ class FundClaimDialog extends HookConsumerWidget {
|
|||||||
|
|
||||||
// Remaining amount
|
// Remaining amount
|
||||||
Text(
|
Text(
|
||||||
'${fund.remainingAmount.toStringAsFixed(2)} ${fund.currency} / ${remainingSplits} splits',
|
'fundEnvelopeRemainingWithSplits'.tr(
|
||||||
|
args: [
|
||||||
|
fund.remainingAmount.toStringAsFixed(2),
|
||||||
|
fund.currency,
|
||||||
|
remainingSplits.toString(),
|
||||||
|
],
|
||||||
|
),
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
color: Theme.of(context).colorScheme.secondary,
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
@@ -512,7 +533,8 @@ class FundClaimDialog extends HookConsumerWidget {
|
|||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
recipient.recipientAccount?.nick ?? 'Unknown User',
|
recipient.recipientAccount?.nick ??
|
||||||
|
'fundEnvelopeUnknownUser'.tr(),
|
||||||
style: Theme.of(
|
style: Theme.of(
|
||||||
context,
|
context,
|
||||||
).textTheme.bodySmall?.copyWith(
|
).textTheme.bodySmall?.copyWith(
|
||||||
@@ -560,7 +582,8 @@ class FundClaimDialog extends HookConsumerWidget {
|
|||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
recipient.recipientAccount?.nick ?? 'Unknown User',
|
recipient.recipientAccount?.nick ??
|
||||||
|
'fundEnvelopeUnknownUser'.tr(),
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
#include <desktop_drop/desktop_drop_plugin.h>
|
#include <desktop_drop/desktop_drop_plugin.h>
|
||||||
#include <file_saver/file_saver_plugin.h>
|
#include <file_saver/file_saver_plugin.h>
|
||||||
#include <file_selector_linux/file_selector_plugin.h>
|
#include <file_selector_linux/file_selector_plugin.h>
|
||||||
#include <flutter_platform_alert/flutter_platform_alert_plugin.h>
|
|
||||||
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
|
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
|
||||||
#include <flutter_timezone/flutter_timezone_plugin.h>
|
#include <flutter_timezone/flutter_timezone_plugin.h>
|
||||||
#include <flutter_udid/flutter_udid_plugin.h>
|
#include <flutter_udid/flutter_udid_plugin.h>
|
||||||
@@ -26,7 +25,6 @@
|
|||||||
#include <syncfusion_pdfviewer_linux/syncfusion_pdfviewer_linux_plugin.h>
|
#include <syncfusion_pdfviewer_linux/syncfusion_pdfviewer_linux_plugin.h>
|
||||||
#include <tray_manager/tray_manager_plugin.h>
|
#include <tray_manager/tray_manager_plugin.h>
|
||||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||||
#include <volume_controller/volume_controller_plugin.h>
|
|
||||||
#include <window_manager/window_manager_plugin.h>
|
#include <window_manager/window_manager_plugin.h>
|
||||||
|
|
||||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||||
@@ -39,9 +37,6 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
|||||||
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
|
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
|
||||||
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
|
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
|
||||||
g_autoptr(FlPluginRegistrar) flutter_platform_alert_registrar =
|
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterPlatformAlertPlugin");
|
|
||||||
flutter_platform_alert_plugin_register_with_registrar(flutter_platform_alert_registrar);
|
|
||||||
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
|
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
|
||||||
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
|
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
|
||||||
@@ -90,9 +85,6 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
|||||||
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||||
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||||
g_autoptr(FlPluginRegistrar) volume_controller_registrar =
|
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "VolumeControllerPlugin");
|
|
||||||
volume_controller_plugin_register_with_registrar(volume_controller_registrar);
|
|
||||||
g_autoptr(FlPluginRegistrar) window_manager_registrar =
|
g_autoptr(FlPluginRegistrar) window_manager_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin");
|
||||||
window_manager_plugin_register_with_registrar(window_manager_registrar);
|
window_manager_plugin_register_with_registrar(window_manager_registrar);
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||||||
desktop_drop
|
desktop_drop
|
||||||
file_saver
|
file_saver
|
||||||
file_selector_linux
|
file_selector_linux
|
||||||
flutter_platform_alert
|
|
||||||
flutter_secure_storage_linux
|
flutter_secure_storage_linux
|
||||||
flutter_timezone
|
flutter_timezone
|
||||||
flutter_udid
|
flutter_udid
|
||||||
@@ -23,7 +22,6 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||||||
syncfusion_pdfviewer_linux
|
syncfusion_pdfviewer_linux
|
||||||
tray_manager
|
tray_manager
|
||||||
url_launcher_linux
|
url_launcher_linux
|
||||||
volume_controller
|
|
||||||
window_manager
|
window_manager
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import firebase_crashlytics
|
|||||||
import firebase_messaging
|
import firebase_messaging
|
||||||
import flutter_inappwebview_macos
|
import flutter_inappwebview_macos
|
||||||
import flutter_local_notifications
|
import flutter_local_notifications
|
||||||
import flutter_platform_alert
|
|
||||||
import flutter_secure_storage_macos
|
import flutter_secure_storage_macos
|
||||||
import flutter_timezone
|
import flutter_timezone
|
||||||
import flutter_udid
|
import flutter_udid
|
||||||
@@ -30,7 +29,6 @@ import media_kit_libs_macos_video
|
|||||||
import media_kit_video
|
import media_kit_video
|
||||||
import package_info_plus
|
import package_info_plus
|
||||||
import pasteboard
|
import pasteboard
|
||||||
import path_provider_foundation
|
|
||||||
import protocol_handler_macos
|
import protocol_handler_macos
|
||||||
import record_macos
|
import record_macos
|
||||||
import screen_retriever_macos
|
import screen_retriever_macos
|
||||||
@@ -43,7 +41,6 @@ import super_native_extensions
|
|||||||
import syncfusion_pdfviewer_macos
|
import syncfusion_pdfviewer_macos
|
||||||
import tray_manager
|
import tray_manager
|
||||||
import url_launcher_macos
|
import url_launcher_macos
|
||||||
import volume_controller
|
|
||||||
import wakelock_plus
|
import wakelock_plus
|
||||||
import window_manager
|
import window_manager
|
||||||
|
|
||||||
@@ -60,7 +57,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
|
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
|
||||||
InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
|
InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
|
||||||
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
||||||
FlutterPlatformAlertPlugin.register(with: registry.registrar(forPlugin: "FlutterPlatformAlertPlugin"))
|
|
||||||
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
||||||
FlutterTimezonePlugin.register(with: registry.registrar(forPlugin: "FlutterTimezonePlugin"))
|
FlutterTimezonePlugin.register(with: registry.registrar(forPlugin: "FlutterTimezonePlugin"))
|
||||||
FlutterUdidPlugin.register(with: registry.registrar(forPlugin: "FlutterUdidPlugin"))
|
FlutterUdidPlugin.register(with: registry.registrar(forPlugin: "FlutterUdidPlugin"))
|
||||||
@@ -73,7 +69,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin"))
|
MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin"))
|
||||||
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||||
PasteboardPlugin.register(with: registry.registrar(forPlugin: "PasteboardPlugin"))
|
PasteboardPlugin.register(with: registry.registrar(forPlugin: "PasteboardPlugin"))
|
||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
|
||||||
ProtocolHandlerMacosPlugin.register(with: registry.registrar(forPlugin: "ProtocolHandlerMacosPlugin"))
|
ProtocolHandlerMacosPlugin.register(with: registry.registrar(forPlugin: "ProtocolHandlerMacosPlugin"))
|
||||||
RecordMacOsPlugin.register(with: registry.registrar(forPlugin: "RecordMacOsPlugin"))
|
RecordMacOsPlugin.register(with: registry.registrar(forPlugin: "RecordMacOsPlugin"))
|
||||||
ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin"))
|
ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin"))
|
||||||
@@ -86,7 +81,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
SyncfusionFlutterPdfViewerPlugin.register(with: registry.registrar(forPlugin: "SyncfusionFlutterPdfViewerPlugin"))
|
SyncfusionFlutterPdfViewerPlugin.register(with: registry.registrar(forPlugin: "SyncfusionFlutterPdfViewerPlugin"))
|
||||||
TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin"))
|
TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin"))
|
||||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||||
VolumeControllerPlugin.register(with: registry.registrar(forPlugin: "VolumeControllerPlugin"))
|
|
||||||
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
|
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
|
||||||
WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
|
WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ PODS:
|
|||||||
- firebase_core (4.2.1):
|
- firebase_core (4.2.1):
|
||||||
- Firebase/CoreOnly (~> 12.4.0)
|
- Firebase/CoreOnly (~> 12.4.0)
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- firebase_crashlytics (5.0.4):
|
- firebase_crashlytics (5.0.5):
|
||||||
- Firebase/CoreOnly (~> 12.4.0)
|
- Firebase/CoreOnly (~> 12.4.0)
|
||||||
- Firebase/Crashlytics (~> 12.4.0)
|
- Firebase/Crashlytics (~> 12.4.0)
|
||||||
- firebase_core
|
- firebase_core
|
||||||
@@ -102,15 +102,13 @@ PODS:
|
|||||||
- OrderedSet (~> 6.0.3)
|
- OrderedSet (~> 6.0.3)
|
||||||
- flutter_local_notifications (0.0.1):
|
- flutter_local_notifications (0.0.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- flutter_platform_alert (0.0.1):
|
|
||||||
- FlutterMacOS
|
|
||||||
- flutter_secure_storage_macos (6.1.3):
|
- flutter_secure_storage_macos (6.1.3):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- flutter_timezone (0.1.0):
|
- flutter_timezone (0.1.0):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- flutter_udid (0.0.1):
|
- flutter_udid (0.0.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- SAMKeychain
|
- KeychainAccess
|
||||||
- flutter_webrtc (1.2.0):
|
- flutter_webrtc (1.2.0):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- WebRTC-SDK (= 137.7151.04)
|
- WebRTC-SDK (= 137.7151.04)
|
||||||
@@ -172,6 +170,7 @@ PODS:
|
|||||||
- GoogleUtilities/Privacy
|
- GoogleUtilities/Privacy
|
||||||
- irondash_engine_context (0.0.1):
|
- irondash_engine_context (0.0.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
|
- KeychainAccess (4.2.2)
|
||||||
- livekit_client (2.5.3):
|
- livekit_client (2.5.3):
|
||||||
- flutter_webrtc
|
- flutter_webrtc
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
@@ -188,14 +187,13 @@ PODS:
|
|||||||
- nanopb/encode (= 3.30910.0)
|
- nanopb/encode (= 3.30910.0)
|
||||||
- nanopb/decode (3.30910.0)
|
- nanopb/decode (3.30910.0)
|
||||||
- nanopb/encode (3.30910.0)
|
- nanopb/encode (3.30910.0)
|
||||||
|
- objective_c (0.0.1):
|
||||||
|
- FlutterMacOS
|
||||||
- OrderedSet (6.0.3)
|
- OrderedSet (6.0.3)
|
||||||
- package_info_plus (0.0.1):
|
- package_info_plus (0.0.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- pasteboard (0.0.1):
|
- pasteboard (0.0.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- path_provider_foundation (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- FlutterMacOS
|
|
||||||
- PromisesObjC (2.4.0)
|
- PromisesObjC (2.4.0)
|
||||||
- PromisesSwift (2.4.0):
|
- PromisesSwift (2.4.0):
|
||||||
- PromisesObjC (= 2.4.0)
|
- PromisesObjC (= 2.4.0)
|
||||||
@@ -203,7 +201,6 @@ PODS:
|
|||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- record_macos (1.1.0):
|
- record_macos (1.1.0):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- SAMKeychain (1.5.3)
|
|
||||||
- screen_retriever_macos (0.0.1):
|
- screen_retriever_macos (0.0.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- share_plus (0.0.1):
|
- share_plus (0.0.1):
|
||||||
@@ -249,8 +246,6 @@ PODS:
|
|||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- url_launcher_macos (0.0.1):
|
- url_launcher_macos (0.0.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- volume_controller (0.0.1):
|
|
||||||
- FlutterMacOS
|
|
||||||
- wakelock_plus (0.0.1):
|
- wakelock_plus (0.0.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- WebRTC-SDK (137.7151.04)
|
- WebRTC-SDK (137.7151.04)
|
||||||
@@ -271,7 +266,6 @@ DEPENDENCIES:
|
|||||||
- firebase_messaging (from `Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos`)
|
- firebase_messaging (from `Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos`)
|
||||||
- flutter_inappwebview_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos`)
|
- flutter_inappwebview_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos`)
|
||||||
- flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`)
|
- flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`)
|
||||||
- flutter_platform_alert (from `Flutter/ephemeral/.symlinks/plugins/flutter_platform_alert/macos`)
|
|
||||||
- flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`)
|
- flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`)
|
||||||
- flutter_timezone (from `Flutter/ephemeral/.symlinks/plugins/flutter_timezone/macos`)
|
- flutter_timezone (from `Flutter/ephemeral/.symlinks/plugins/flutter_timezone/macos`)
|
||||||
- flutter_udid (from `Flutter/ephemeral/.symlinks/plugins/flutter_udid/macos`)
|
- flutter_udid (from `Flutter/ephemeral/.symlinks/plugins/flutter_udid/macos`)
|
||||||
@@ -283,9 +277,9 @@ DEPENDENCIES:
|
|||||||
- local_auth_darwin (from `Flutter/ephemeral/.symlinks/plugins/local_auth_darwin/darwin`)
|
- local_auth_darwin (from `Flutter/ephemeral/.symlinks/plugins/local_auth_darwin/darwin`)
|
||||||
- media_kit_libs_macos_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos`)
|
- media_kit_libs_macos_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos`)
|
||||||
- media_kit_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos`)
|
- media_kit_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos`)
|
||||||
|
- objective_c (from `Flutter/ephemeral/.symlinks/plugins/objective_c/macos`)
|
||||||
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
|
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
|
||||||
- pasteboard (from `Flutter/ephemeral/.symlinks/plugins/pasteboard/macos`)
|
- pasteboard (from `Flutter/ephemeral/.symlinks/plugins/pasteboard/macos`)
|
||||||
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
|
|
||||||
- protocol_handler_macos (from `Flutter/ephemeral/.symlinks/plugins/protocol_handler_macos/macos`)
|
- protocol_handler_macos (from `Flutter/ephemeral/.symlinks/plugins/protocol_handler_macos/macos`)
|
||||||
- record_macos (from `Flutter/ephemeral/.symlinks/plugins/record_macos/macos`)
|
- record_macos (from `Flutter/ephemeral/.symlinks/plugins/record_macos/macos`)
|
||||||
- screen_retriever_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos`)
|
- screen_retriever_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos`)
|
||||||
@@ -298,7 +292,6 @@ DEPENDENCIES:
|
|||||||
- syncfusion_pdfviewer_macos (from `Flutter/ephemeral/.symlinks/plugins/syncfusion_pdfviewer_macos/macos`)
|
- syncfusion_pdfviewer_macos (from `Flutter/ephemeral/.symlinks/plugins/syncfusion_pdfviewer_macos/macos`)
|
||||||
- tray_manager (from `Flutter/ephemeral/.symlinks/plugins/tray_manager/macos`)
|
- tray_manager (from `Flutter/ephemeral/.symlinks/plugins/tray_manager/macos`)
|
||||||
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
||||||
- volume_controller (from `Flutter/ephemeral/.symlinks/plugins/volume_controller/macos`)
|
|
||||||
- wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`)
|
- wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`)
|
||||||
- window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`)
|
- window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`)
|
||||||
|
|
||||||
@@ -317,11 +310,11 @@ SPEC REPOS:
|
|||||||
- GoogleAppMeasurement
|
- GoogleAppMeasurement
|
||||||
- GoogleDataTransport
|
- GoogleDataTransport
|
||||||
- GoogleUtilities
|
- GoogleUtilities
|
||||||
|
- KeychainAccess
|
||||||
- nanopb
|
- nanopb
|
||||||
- OrderedSet
|
- OrderedSet
|
||||||
- PromisesObjC
|
- PromisesObjC
|
||||||
- PromisesSwift
|
- PromisesSwift
|
||||||
- SAMKeychain
|
|
||||||
- sqlite3
|
- sqlite3
|
||||||
- WebRTC-SDK
|
- WebRTC-SDK
|
||||||
|
|
||||||
@@ -352,8 +345,6 @@ EXTERNAL SOURCES:
|
|||||||
:path: Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos
|
||||||
flutter_local_notifications:
|
flutter_local_notifications:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos
|
||||||
flutter_platform_alert:
|
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/flutter_platform_alert/macos
|
|
||||||
flutter_secure_storage_macos:
|
flutter_secure_storage_macos:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos
|
||||||
flutter_timezone:
|
flutter_timezone:
|
||||||
@@ -376,12 +367,12 @@ EXTERNAL SOURCES:
|
|||||||
:path: Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos
|
||||||
media_kit_video:
|
media_kit_video:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos
|
||||||
|
objective_c:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/objective_c/macos
|
||||||
package_info_plus:
|
package_info_plus:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos
|
||||||
pasteboard:
|
pasteboard:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/pasteboard/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/pasteboard/macos
|
||||||
path_provider_foundation:
|
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
|
|
||||||
protocol_handler_macos:
|
protocol_handler_macos:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/protocol_handler_macos/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/protocol_handler_macos/macos
|
||||||
record_macos:
|
record_macos:
|
||||||
@@ -406,8 +397,6 @@ EXTERNAL SOURCES:
|
|||||||
:path: Flutter/ephemeral/.symlinks/plugins/tray_manager/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/tray_manager/macos
|
||||||
url_launcher_macos:
|
url_launcher_macos:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
|
||||||
volume_controller:
|
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/volume_controller/macos
|
|
||||||
wakelock_plus:
|
wakelock_plus:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos
|
||||||
window_manager:
|
window_manager:
|
||||||
@@ -424,7 +413,7 @@ SPEC CHECKSUMS:
|
|||||||
Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e
|
Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e
|
||||||
firebase_analytics: 09241c4796c1c42a02349ef8bf30025f5b640f0e
|
firebase_analytics: 09241c4796c1c42a02349ef8bf30025f5b640f0e
|
||||||
firebase_core: e054894ab56033ef9bcbe2d9eac9395e5306e2fc
|
firebase_core: e054894ab56033ef9bcbe2d9eac9395e5306e2fc
|
||||||
firebase_crashlytics: c2438b5f5bdcacf59d0eaee5852c6b0ab09dab77
|
firebase_crashlytics: add6934cf4413d6c1307ca5251dbdbfe580e4caf
|
||||||
firebase_messaging: 373ac3a56e5aa37bb9aff4127f700aa5973c1168
|
firebase_messaging: 373ac3a56e5aa37bb9aff4127f700aa5973c1168
|
||||||
FirebaseAnalytics: 0fc2b20091f0ddd21bf73397cf8f0eb5346dc24f
|
FirebaseAnalytics: 0fc2b20091f0ddd21bf73397cf8f0eb5346dc24f
|
||||||
FirebaseCore: bb595f3114953664e3c1dc032f008a244147cfd3
|
FirebaseCore: bb595f3114953664e3c1dc032f008a244147cfd3
|
||||||
@@ -437,10 +426,9 @@ SPEC CHECKSUMS:
|
|||||||
FirebaseSessions: ba7c7a7ca8696a8d540eb3fe3800fbe98c79786d
|
FirebaseSessions: ba7c7a7ca8696a8d540eb3fe3800fbe98c79786d
|
||||||
flutter_inappwebview_macos: c2d68649f9f8f1831bfcd98d73fd6256366d9d1d
|
flutter_inappwebview_macos: c2d68649f9f8f1831bfcd98d73fd6256366d9d1d
|
||||||
flutter_local_notifications: 4bf37a31afde695b56091b4ae3e4d9c7a7e6cda0
|
flutter_local_notifications: 4bf37a31afde695b56091b4ae3e4d9c7a7e6cda0
|
||||||
flutter_platform_alert: 8fa7a7c21f95b26d08b4a3891936ca27e375f284
|
|
||||||
flutter_secure_storage_macos: 7f45e30f838cf2659862a4e4e3ee1c347c2b3b54
|
flutter_secure_storage_macos: 7f45e30f838cf2659862a4e4e3ee1c347c2b3b54
|
||||||
flutter_timezone: d272288c69082ad571630e0d17140b3d6b93dc0c
|
flutter_timezone: d272288c69082ad571630e0d17140b3d6b93dc0c
|
||||||
flutter_udid: d26e455e8c06174e6aff476e147defc6cae38495
|
flutter_udid: 00c09e022fd527fd39fef97670b220f2ae8190e7
|
||||||
flutter_webrtc: 718eae22a371cd94e5d56aa4f301443ebc5bb737
|
flutter_webrtc: 718eae22a371cd94e5d56aa4f301443ebc5bb737
|
||||||
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
|
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
|
||||||
gal: baecd024ebfd13c441269ca7404792a7152fde89
|
gal: baecd024ebfd13c441269ca7404792a7152fde89
|
||||||
@@ -448,20 +436,20 @@ SPEC CHECKSUMS:
|
|||||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||||
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
||||||
irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba
|
irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba
|
||||||
|
KeychainAccess: c0c4f7f38f6fc7bbe58f5702e25f7bd2f65abf51
|
||||||
livekit_client: a6d5ae8aaeebf3e52235da866fea00f43156c72b
|
livekit_client: a6d5ae8aaeebf3e52235da866fea00f43156c72b
|
||||||
local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb
|
local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb
|
||||||
media_kit_libs_macos_video: 85a23e549b5f480e72cae3e5634b5514bc692f65
|
media_kit_libs_macos_video: 85a23e549b5f480e72cae3e5634b5514bc692f65
|
||||||
media_kit_video: fa6564e3799a0a28bff39442334817088b7ca758
|
media_kit_video: fa6564e3799a0a28bff39442334817088b7ca758
|
||||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||||
|
objective_c: ec13431e45ba099cb734eb2829a5c1cd37986cba
|
||||||
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
||||||
package_info_plus: f0052d280d17aa382b932f399edf32507174e870
|
package_info_plus: f0052d280d17aa382b932f399edf32507174e870
|
||||||
pasteboard: 278d8100149f940fb795d6b3a74f0720c890ecb7
|
pasteboard: 278d8100149f940fb795d6b3a74f0720c890ecb7
|
||||||
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
|
|
||||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||||
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
|
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
|
||||||
protocol_handler_macos: f9cd7b13bcaf6b0425f7410cbe52376cb843a936
|
protocol_handler_macos: f9cd7b13bcaf6b0425f7410cbe52376cb843a936
|
||||||
record_macos: 43194b6c06ca6f8fa132e2acea72b202b92a0f5b
|
record_macos: 43194b6c06ca6f8fa132e2acea72b202b92a0f5b
|
||||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
|
||||||
screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f
|
screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f
|
||||||
share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc
|
share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc
|
||||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||||
@@ -473,7 +461,6 @@ SPEC CHECKSUMS:
|
|||||||
syncfusion_pdfviewer_macos: b3b110c68039178ca4105dd03ef38761eca3b36b
|
syncfusion_pdfviewer_macos: b3b110c68039178ca4105dd03ef38761eca3b36b
|
||||||
tray_manager: a104b5c81b578d83f3c3d0f40a997c8b10810166
|
tray_manager: a104b5c81b578d83f3c3d0f40a997c8b10810166
|
||||||
url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd
|
url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd
|
||||||
volume_controller: 5c068e6d085c80dadd33fc2c918d2114b775b3dd
|
|
||||||
wakelock_plus: 917609be14d812ddd9e9528876538b2263aaa03b
|
wakelock_plus: 917609be14d812ddd9e9528876538b2263aaa03b
|
||||||
WebRTC-SDK: 40d4f5ba05cadff14e4db5614aec402a633f007e
|
WebRTC-SDK: 40d4f5ba05cadff14e4db5614aec402a633f007e
|
||||||
window_manager: b729e31d38fb04905235df9ea896128991cad99e
|
window_manager: b729e31d38fb04905235df9ea896128991cad99e
|
||||||
|
|||||||
258
pubspec.lock
258
pubspec.lock
@@ -73,6 +73,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.13.0"
|
version: "2.13.0"
|
||||||
|
autotrie:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: autotrie
|
||||||
|
sha256: "55da6faefb53cfcb0abb2f2ca8636123fb40e35286bb57440d2cf467568188f8"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.0"
|
||||||
avatar_stack:
|
avatar_stack:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -149,10 +157,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: built_value
|
name: built_value
|
||||||
sha256: a30f0a0e38671e89a492c44d005b5545b830a961575bbd8336d42869ff71066d
|
sha256: "426cf75afdb23aa74bd4e471704de3f9393f3c7b04c1e2d9c6f1073ae0b8b139"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.12.0"
|
version: "8.12.1"
|
||||||
cached_network_image:
|
cached_network_image:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -301,10 +309,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: cross_file
|
name: cross_file
|
||||||
sha256: "942a4791cd385a68ccb3b32c71c427aba508a1bb949b86dff2adbe4049f16239"
|
sha256: "701dcfc06da0882883a2657c445103380e53e647060ad8d9dfb710c100996608"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.5"
|
version: "0.3.5+1"
|
||||||
crypto:
|
crypto:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -565,10 +573,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: file_picker
|
name: file_picker
|
||||||
sha256: f8f4ea435f791ab1f817b4e338ed958cb3d04ba43d6736ffc39958d950754967
|
sha256: "7872545770c277236fd32b022767576c562ba28366204ff1a5628853cf8f2200"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.3.6"
|
version: "10.3.7"
|
||||||
file_saver:
|
file_saver:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -581,18 +589,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: file_selector_linux
|
name: file_selector_linux
|
||||||
sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33"
|
sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.9.3+2"
|
version: "0.9.4"
|
||||||
file_selector_macos:
|
file_selector_macos:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: file_selector_macos
|
name: file_selector_macos
|
||||||
sha256: "88707a3bec4b988aaed3b4df5d7441ee4e987f20b286cddca5d6a8270cab23f2"
|
sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.9.4+5"
|
version: "0.9.5"
|
||||||
file_selector_platform_interface:
|
file_selector_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -605,10 +613,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: file_selector_windows
|
name: file_selector_windows
|
||||||
sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b"
|
sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.9.3+4"
|
version: "0.9.3+5"
|
||||||
firebase_analytics:
|
firebase_analytics:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -661,10 +669,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: firebase_crashlytics
|
name: firebase_crashlytics
|
||||||
sha256: c3ebe3ed9f3b1d36c0864a4a28b041fcc2686f11fb2a4f7891319ea8d1d161cc
|
sha256: bd4eadd620071ae48ef0e3617499be2a0ea4062d6d5c6fea6c1cec41ebbb2435
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.4"
|
version: "5.0.5"
|
||||||
firebase_crashlytics_platform_interface:
|
firebase_crashlytics_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -766,6 +774,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.2.0"
|
version: "7.2.0"
|
||||||
|
flutter_code_editor:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_code_editor
|
||||||
|
sha256: "9af48ba8e3558b6ea4bb98b84c5eb1649702acf53e61a84d88383eeb79b239b0"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.5"
|
||||||
flutter_colorpicker:
|
flutter_colorpicker:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -1003,22 +1019,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.5.1+1"
|
version: "1.5.1+1"
|
||||||
flutter_platform_alert:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: flutter_platform_alert
|
|
||||||
sha256: "70f4979a617388cd890ec32e9acc1a6a425bcdf3d8b444eb976be1834e79dc0c"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.8.0"
|
|
||||||
flutter_plugin_android_lifecycle:
|
flutter_plugin_android_lifecycle:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_plugin_android_lifecycle
|
name: flutter_plugin_android_lifecycle
|
||||||
sha256: "306f0596590e077338312f38837f595c04f28d6cdeeac392d3d74df2f0003687"
|
sha256: ee8068e0e1cd16c4a82714119918efdeed33b3ba7772c54b5d094ab53f9b7fd1
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.32"
|
version: "2.0.33"
|
||||||
flutter_popup_card:
|
flutter_popup_card:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -1103,10 +1111,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_svg
|
name: flutter_svg
|
||||||
sha256: "055de8921be7b8e8b98a233c7a5ef84b3a6fcc32f46f1ebf5b9bb3576d108355"
|
sha256: "87fbd7c534435b6c5d9d98b01e1fd527812b82e68ddd8bd35fc45ed0fa8f0a95"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.2"
|
version: "2.2.3"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -1132,10 +1140,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_udid
|
name: flutter_udid
|
||||||
sha256: "166bee5989a58c66b8b62000ea65edccc7c8167bbafdbb08022638db330dd030"
|
sha256: "31193dbfef74f697e9e5317e59f3c2ae6dc45ce4b9f5d39308a32446e8303acc"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.0"
|
version: "4.1.0"
|
||||||
flutter_web_plugins:
|
flutter_web_plugins:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -1253,6 +1261,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.0"
|
version: "0.7.0"
|
||||||
|
hive:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: hive
|
||||||
|
sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.3"
|
||||||
hooks_riverpod:
|
hooks_riverpod:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -1302,7 +1318,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.2"
|
version: "3.2.2"
|
||||||
http_parser:
|
http_parser:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: http_parser
|
name: http_parser
|
||||||
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
||||||
@@ -1329,26 +1345,26 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: image_picker_android
|
name: image_picker_android
|
||||||
sha256: ca2a3b04d34e76157e9ae680ef16014fb4c2d20484e78417eaed6139330056f6
|
sha256: "5e9bf126c37c117cf8094215373c6d561117a3cfb50ebc5add1a61dc6e224677"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.8.13+7"
|
version: "0.8.13+10"
|
||||||
image_picker_for_web:
|
image_picker_for_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: image_picker_for_web
|
name: image_picker_for_web
|
||||||
sha256: "40c2a6a0da15556dc0f8e38a3246064a971a9f512386c3339b89f76db87269b6"
|
sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.0"
|
version: "3.1.1"
|
||||||
image_picker_ios:
|
image_picker_ios:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: image_picker_ios
|
name: image_picker_ios
|
||||||
sha256: e675c22790bcc24e9abd455deead2b7a88de4b79f7327a281812f14de1a56f58
|
sha256: "997d100ce1dda5b1ba4085194c5e36c9f8a1fb7987f6a36ab677a344cd2dc986"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.8.13+1"
|
version: "0.8.13+2"
|
||||||
image_picker_linux:
|
image_picker_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1461,6 +1477,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.2"
|
version: "3.0.2"
|
||||||
|
linked_scroll_controller:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: linked_scroll_controller
|
||||||
|
sha256: e6020062bcf4ffc907ee7fd090fa971e65d8dfaac3c62baf601a3ced0b37986a
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.0"
|
||||||
lint:
|
lint:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1497,18 +1521,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: local_auth_android
|
name: local_auth_android
|
||||||
sha256: d836715ed95b16b2de3a8c47a88ba5e607976bb1e27c9446d193152ea1429fae
|
sha256: "04dd9050b59cb4bcaf051d44eec65865779a9b2f6daccc523f59f96b565a5d54"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0"
|
version: "2.0.3"
|
||||||
local_auth_darwin:
|
local_auth_darwin:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: local_auth_darwin
|
name: local_auth_darwin
|
||||||
sha256: "15d9db4ad4d58a11d7269e55d46ff8d49ed5e856226c8a5a91280f0d7c37b3a6"
|
sha256: "668ea65edaab17380956e9713f57e34f78ede505ca0cfd8d39db34e2f260bfee"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0"
|
version: "2.0.1"
|
||||||
local_auth_platform_interface:
|
local_auth_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1521,10 +1545,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: local_auth_windows
|
name: local_auth_windows
|
||||||
sha256: d95535a73eddf57ce5930d5e78a0fa4f294c31981fdeeee83325b797302be454
|
sha256: be12c5b8ba5e64896983123655c5f67d2484ecfcc95e367952ad6e3bff94cb16
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0"
|
version: "2.0.1"
|
||||||
logger:
|
logger:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1649,10 +1673,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: media_kit_video
|
name: media_kit_video
|
||||||
sha256: "813858c3fe84eb46679eb698695f60665e2bfbef757766fac4d2e683f926e15a"
|
sha256: eac9b5f27310afe6def49f9b785cef155f99f1db6079d2b2b127b8bddafb6472
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.1"
|
version: "2.0.0"
|
||||||
menu_base:
|
menu_base:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1685,6 +1709,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.1"
|
version: "1.0.1"
|
||||||
|
mocktail:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: mocktail
|
||||||
|
sha256: "890df3f9688106f25755f26b1c60589a92b3ab91a22b8b224947ad041bf172d8"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.4"
|
||||||
modal_bottom_sheet:
|
modal_bottom_sheet:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -1725,6 +1757,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.0"
|
version: "0.5.0"
|
||||||
|
objective_c:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: objective_c
|
||||||
|
sha256: "1f81ed9e41909d44162d7ec8663b2c647c202317cc0b56d3d56f6a13146a0b64"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "9.1.0"
|
||||||
octo_image:
|
octo_image:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1793,18 +1833,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_android
|
name: path_provider_android
|
||||||
sha256: e122c5ea805bb6773bb12ce667611265980940145be920cd09a4b0ec0285cb16
|
sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.20"
|
version: "2.2.22"
|
||||||
path_provider_foundation:
|
path_provider_foundation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_foundation
|
name: path_provider_foundation
|
||||||
sha256: efaec349ddfc181528345c56f8eda9d6cccd71c177511b132c6a0ddaefaa2738
|
sha256: "6192e477f34018ef1ea790c56fffc7302e3bc3efede9e798b934c252c8c105ba"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.3"
|
version: "2.5.0"
|
||||||
path_provider_linux:
|
path_provider_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -2066,18 +2106,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: record_android
|
name: record_android
|
||||||
sha256: fb54ee4e28f6829b8c580252a9ef49d9c549cfd263b0660ad7eeac0908658e9f
|
sha256: "9aaf3f151e61399b09bd7c31eb5f78253d2962b3f57af019ac5a2d1a3afdcf71"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.4"
|
version: "1.4.5"
|
||||||
record_ios:
|
record_ios:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: record_ios
|
name: record_ios
|
||||||
sha256: "765b42ac1be019b1674ddd809b811fc721fe5a93f7bb1da7803f0d16772fd6d7"
|
sha256: "69fcd37c6185834e90254573599a9165db18a2cbfa266b6d1e46ffffeb06a28c"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.4"
|
version: "1.1.5"
|
||||||
record_linux:
|
record_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -2106,10 +2146,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: record_web
|
name: record_web
|
||||||
sha256: "20ac10d56514cb9f8cecc8f3579383084fdfb43b0d04e05a95244d0d76091d90"
|
sha256: "3feeffbc0913af3021da9810bb8702a068db6bc9da52dde1d19b6ee7cb9edb51"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.1"
|
version: "1.2.2"
|
||||||
record_windows:
|
record_windows:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -2190,22 +2230,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.1"
|
version: "2.0.1"
|
||||||
screen_brightness_android:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: screen_brightness_android
|
|
||||||
sha256: d34f5321abd03bc3474f4c381f53d189117eba0b039eac1916aa92cca5fd0a96
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.1.3"
|
|
||||||
screen_brightness_platform_interface:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: screen_brightness_platform_interface
|
|
||||||
sha256: "737bd47b57746bc4291cab1b8a5843ee881af499514881b0247ec77447ee769c"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.1.0"
|
|
||||||
screen_retriever:
|
screen_retriever:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -2262,6 +2286,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.1"
|
version: "3.0.1"
|
||||||
|
scrollable_positioned_list:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: scrollable_positioned_list
|
||||||
|
sha256: "1b54d5f1329a1e263269abc9e2543d90806131aa14fe7c6062a8054d57249287"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.8"
|
||||||
sdp_transform:
|
sdp_transform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -2298,18 +2330,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: shared_preferences_android
|
name: shared_preferences_android
|
||||||
sha256: "34266009473bf71d748912da4bf62d439185226c03e01e2d9687bc65bbfcb713"
|
sha256: "46a46fd64659eff15f4638bbe19de43f9483f0e0bf024a9fb6b3582064bacc7b"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.15"
|
version: "2.4.17"
|
||||||
shared_preferences_foundation:
|
shared_preferences_foundation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: shared_preferences_foundation
|
name: shared_preferences_foundation
|
||||||
sha256: "1c33a907142607c40a7542768ec9badfd16293bac51da3a4482623d15845f88b"
|
sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.5.5"
|
version: "2.5.6"
|
||||||
shared_preferences_linux:
|
shared_preferences_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -2583,18 +2615,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: syncfusion_flutter_core
|
name: syncfusion_flutter_core
|
||||||
sha256: "8118f13264d1401a7085d12a0aaeac1ebd5cd939046b8c565d195879646daad6"
|
sha256: fea2b5f6c976455d20b19bf77d29bf96a740d14579127b4fc1cdafde42b48177
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "31.2.10"
|
version: "31.2.12"
|
||||||
syncfusion_flutter_pdf:
|
syncfusion_flutter_pdf:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: syncfusion_flutter_pdf
|
name: syncfusion_flutter_pdf
|
||||||
sha256: "34d8b658e9fa7b18c4c16b4a775dc3f634933c4367e5f1ef4854f80cdd22c3ff"
|
sha256: d7852ea65da3e5b64a06b38959cf0de1fb1eaafbfce7ac6950d4a3d59d3cfd57
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "31.2.10"
|
version: "31.2.12"
|
||||||
syncfusion_flutter_pdfviewer:
|
syncfusion_flutter_pdfviewer:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -2607,50 +2639,50 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: syncfusion_flutter_signaturepad
|
name: syncfusion_flutter_signaturepad
|
||||||
sha256: ef891418bee7c79470ff1b6290f7745df8c1b2adf4df6b81ab9cd69ef900c4e8
|
sha256: f74fca0463e0977b7fca6f37fc7dad3cab4b99648594dbe8b99f55f2f5c37ab0
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "31.2.10"
|
version: "31.2.12"
|
||||||
syncfusion_pdfviewer_linux:
|
syncfusion_pdfviewer_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: syncfusion_pdfviewer_linux
|
name: syncfusion_pdfviewer_linux
|
||||||
sha256: ec4efb4cdd34462f40b7dafcb5094780a15c988691f28bdec141ea2a01f145bb
|
sha256: "1ffb2e3656694342e29216d7df19961b7fde40d424024efda0610fe5661f1dc3"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "31.2.10"
|
version: "31.2.12"
|
||||||
syncfusion_pdfviewer_macos:
|
syncfusion_pdfviewer_macos:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: syncfusion_pdfviewer_macos
|
name: syncfusion_pdfviewer_macos
|
||||||
sha256: "15d4e7d5a5a705b7861bdf7e5758d371973a03fda33a21068dc934569b8fc363"
|
sha256: "166225a4db5c182cd6e18bba69685e15cfe7bd10232c1d5663842636de605437"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "31.2.10"
|
version: "31.2.12"
|
||||||
syncfusion_pdfviewer_platform_interface:
|
syncfusion_pdfviewer_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: syncfusion_pdfviewer_platform_interface
|
name: syncfusion_pdfviewer_platform_interface
|
||||||
sha256: d2b9d4631693503340d5eaef6f42446d6d74f290dca9764e65f5b55b0b4043cf
|
sha256: d630694835967ca78151b22ac88149325e933f1a5de29bf4e845753cc5123585
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "31.2.10"
|
version: "31.2.12"
|
||||||
syncfusion_pdfviewer_web:
|
syncfusion_pdfviewer_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: syncfusion_pdfviewer_web
|
name: syncfusion_pdfviewer_web
|
||||||
sha256: "073384338eb9a6370e3ec7b9fbad973b6d0312c027698392c09409e156644807"
|
sha256: "395d3c6f2afb816f9f883b7fe7281a16d3bc38a8b3799e54a1c8399ff91fc059"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "31.2.10"
|
version: "31.2.12"
|
||||||
syncfusion_pdfviewer_windows:
|
syncfusion_pdfviewer_windows:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: syncfusion_pdfviewer_windows
|
name: syncfusion_pdfviewer_windows
|
||||||
sha256: de6254b5b939c17b32498d895aaf272748035fd20a9790b4ee7e8afe915ef233
|
sha256: ac4a5fdf5eae92163933669d5ec80a3553b84421828d6c7beb167519084a42e4
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "31.2.10"
|
version: "31.2.12"
|
||||||
synchronized:
|
synchronized:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -2671,34 +2703,34 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: talker
|
name: talker
|
||||||
sha256: "82de443cadfb6c41d457e7774c7890a91c73af3c2f17f3f7c01670bb58d5f5a1"
|
sha256: f17bde91ef86a51803974804bc6c0e161d14b496522bbc646c531be2c702e390
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.2"
|
version: "5.1.0"
|
||||||
talker_dio_logger:
|
talker_dio_logger:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: talker_dio_logger
|
name: talker_dio_logger
|
||||||
sha256: "5bbecc237f3d2c4af9348da5a0086321ed6dd6bf9857d723b1f54f61c810cff2"
|
sha256: "322e8fee094d5fdce10f0439fec5625fb73fd282622b37c794bdaab4e1fc2b1b"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.2"
|
version: "5.1.0"
|
||||||
talker_flutter:
|
talker_flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: talker_flutter
|
name: talker_flutter
|
||||||
sha256: "4f7a8d739237a3a3c8ba4dddcdbc1f9d9dec143811641dbafebd6b70f947f8ca"
|
sha256: "5869c80c046a8e0478560fa2ebb40a3f0e025a9948bcdea50c12343cba681820"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.2"
|
version: "5.1.0"
|
||||||
talker_logger:
|
talker_logger:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: talker_logger
|
name: talker_logger
|
||||||
sha256: "8218836d871ea5ab1ec616cffe3cdae84e8fb44022d5cc04c95d7b220572b8fb"
|
sha256: "87c66fefded42446d8c6365643582f3d180c0ce09485c129e9a88fa14d0d34eb"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.2"
|
version: "5.1.0"
|
||||||
talker_riverpod_logger:
|
talker_riverpod_logger:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -2784,10 +2816,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: universal_io
|
name: universal_io
|
||||||
sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad"
|
sha256: f63cbc48103236abf48e345e07a03ce5757ea86285ed313a6a032596ed9301e2
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.2"
|
version: "2.3.1"
|
||||||
universal_platform:
|
universal_platform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -2816,34 +2848,34 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_android
|
name: url_launcher_android
|
||||||
sha256: "5c8b6c2d89a78f5a1cca70a73d9d5f86c701b36b42f9c9dac7bad592113c28e9"
|
sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.3.24"
|
version: "6.3.28"
|
||||||
url_launcher_ios:
|
url_launcher_ios:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_ios
|
name: url_launcher_ios
|
||||||
sha256: "6b63f1441e4f653ae799166a72b50b1767321ecc263a57aadf825a7a2a5477d9"
|
sha256: cfde38aa257dae62ffe79c87fab20165dfdf6988c1d31b58ebf59b9106062aad
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.3.5"
|
version: "6.3.6"
|
||||||
url_launcher_linux:
|
url_launcher_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_linux
|
name: url_launcher_linux
|
||||||
sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935"
|
sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.1"
|
version: "3.2.2"
|
||||||
url_launcher_macos:
|
url_launcher_macos:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_macos
|
name: url_launcher_macos
|
||||||
sha256: "8262208506252a3ed4ff5c0dc1e973d2c0e0ef337d0a074d35634da5d44397c9"
|
sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.4"
|
version: "3.2.5"
|
||||||
url_launcher_platform_interface:
|
url_launcher_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -2864,10 +2896,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_windows
|
name: url_launcher_windows
|
||||||
sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77"
|
sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.4"
|
version: "3.1.5"
|
||||||
uuid:
|
uuid:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -2932,14 +2964,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "15.0.2"
|
version: "15.0.2"
|
||||||
volume_controller:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: volume_controller
|
|
||||||
sha256: d75039e69c0d90e7810bfd47e3eedf29ff8543ea7a10392792e81f9bded7edf5
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "3.4.0"
|
|
||||||
wakelock_plus:
|
wakelock_plus:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|||||||
34
pubspec.yaml
34
pubspec.yaml
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 3.3.0+146
|
version: 3.3.0+147
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.7.2
|
sdk: ^3.7.2
|
||||||
@@ -57,30 +57,30 @@ dependencies:
|
|||||||
cached_network_image: ^3.4.1
|
cached_network_image: ^3.4.1
|
||||||
web: ^1.1.1
|
web: ^1.1.1
|
||||||
flutter_blurhash: ^0.9.1
|
flutter_blurhash: ^0.9.1
|
||||||
media_kit: ^1.2.1
|
media_kit: ^1.2.2
|
||||||
media_kit_video: ^1.3.1
|
media_kit_video: ^2.0.0
|
||||||
media_kit_libs_video: ^1.0.7
|
media_kit_libs_video: ^1.0.7
|
||||||
flutter_cache_manager: ^3.4.1
|
flutter_cache_manager: ^3.4.1
|
||||||
flutter_platform_alert: ^0.8.0
|
|
||||||
email_validator: ^3.0.0
|
email_validator: ^3.0.0
|
||||||
easy_localization: ^3.0.8
|
easy_localization: ^3.0.8
|
||||||
flutter_inappwebview: ^6.1.5
|
flutter_inappwebview: ^6.1.5
|
||||||
animations: ^2.1.0
|
animations: ^2.1.1
|
||||||
package_info_plus: ^9.0.0
|
package_info_plus: ^9.0.0
|
||||||
device_info_plus: ^11.3.0
|
device_info_plus: ^11.3.0
|
||||||
protocol_handler: ^0.2.0
|
protocol_handler: ^0.2.0
|
||||||
tus_client_dart:
|
tus_client_dart:
|
||||||
git: https://github.com/LittleSheep2Code/tus_client.git
|
git: https://github.com/LittleSheep2Code/tus_client.git
|
||||||
cross_file: ^0.3.5
|
cross_file: ^0.3.5+1
|
||||||
image_picker: ^1.2.1
|
image_picker: ^1.2.1
|
||||||
file_picker: ^10.3.6
|
file_picker: ^10.3.7
|
||||||
riverpod_annotation: ^2.6.1
|
riverpod_annotation: ^2.6.1
|
||||||
image_picker_platform_interface: ^2.11.1
|
image_picker_platform_interface: ^2.11.1
|
||||||
image_picker_android: ^0.8.13+7
|
image_picker_android: ^0.8.13+10
|
||||||
super_context_menu: ^0.9.1
|
super_context_menu: ^0.9.1
|
||||||
modal_bottom_sheet: ^3.0.0
|
modal_bottom_sheet: ^3.0.0
|
||||||
firebase_messaging: ^16.0.4
|
firebase_messaging: ^16.0.4
|
||||||
flutter_udid: ^4.0.0
|
flutter_udid: ^4.1.0
|
||||||
firebase_core: ^4.2.1
|
firebase_core: ^4.2.1
|
||||||
web_socket_channel: ^3.0.3
|
web_socket_channel: ^3.0.3
|
||||||
material_symbols_icons: ^4.2874.0
|
material_symbols_icons: ^4.2874.0
|
||||||
@@ -117,7 +117,7 @@ dependencies:
|
|||||||
flutter_timezone: ^5.0.1
|
flutter_timezone: ^5.0.1
|
||||||
fl_chart: ^1.1.1
|
fl_chart: ^1.1.1
|
||||||
sign_in_with_apple: ^7.0.1
|
sign_in_with_apple: ^7.0.1
|
||||||
flutter_svg: ^2.2.2
|
flutter_svg: ^2.2.3
|
||||||
native_exif: ^0.6.2
|
native_exif: ^0.6.2
|
||||||
local_auth: ^3.0.0
|
local_auth: ^3.0.0
|
||||||
flutter_secure_storage: ^9.2.4
|
flutter_secure_storage: ^9.2.4
|
||||||
@@ -136,7 +136,7 @@ dependencies:
|
|||||||
flutter_app_update: ^3.2.2
|
flutter_app_update: ^3.2.2
|
||||||
archive: ^4.0.7
|
archive: ^4.0.7
|
||||||
process_run: ^1.2.4
|
process_run: ^1.2.4
|
||||||
firebase_crashlytics: ^5.0.4
|
firebase_crashlytics: ^5.0.5
|
||||||
firebase_analytics: ^12.0.4
|
firebase_analytics: ^12.0.4
|
||||||
material_color_utilities: ^0.11.1
|
material_color_utilities: ^0.11.1
|
||||||
screenshot: ^3.0.0
|
screenshot: ^3.0.0
|
||||||
@@ -155,10 +155,10 @@ dependencies:
|
|||||||
dart_ipc: ^1.0.1
|
dart_ipc: ^1.0.1
|
||||||
pretty_diff_text: ^2.1.0
|
pretty_diff_text: ^2.1.0
|
||||||
window_manager: ^0.5.1
|
window_manager: ^0.5.1
|
||||||
talker: ^5.0.2
|
talker: ^5.1.0
|
||||||
talker_flutter: ^5.0.2
|
talker_flutter: ^5.1.0
|
||||||
talker_logger: ^5.0.2
|
talker_logger: ^5.1.0
|
||||||
talker_dio_logger: ^5.0.2
|
talker_dio_logger: ^5.1.0
|
||||||
talker_riverpod_logger: ^5.0.1
|
talker_riverpod_logger: ^5.0.1
|
||||||
syncfusion_flutter_pdfviewer: ^31.1.21
|
syncfusion_flutter_pdfviewer: ^31.1.21
|
||||||
swipe_to: ^1.0.6
|
swipe_to: ^1.0.6
|
||||||
@@ -168,7 +168,9 @@ dependencies:
|
|||||||
event_bus: ^2.0.1
|
event_bus: ^2.0.1
|
||||||
convert: ^3.1.2
|
convert: ^3.1.2
|
||||||
desktop_drop: ^0.7.0
|
desktop_drop: ^0.7.0
|
||||||
flutter_animate: ^4.5.0
|
flutter_animate: ^4.5.2
|
||||||
|
http_parser: ^4.1.2
|
||||||
|
flutter_code_editor: ^0.3.5
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
23
test/drift/app_database/generated/schema.dart
Normal file
23
test/drift/app_database/generated/schema.dart
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
// dart format width=80
|
||||||
|
// GENERATED CODE, DO NOT EDIT BY HAND.
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:drift/internal/migrations.dart';
|
||||||
|
import 'schema_v6.dart' as v6;
|
||||||
|
import 'schema_v7.dart' as v7;
|
||||||
|
|
||||||
|
class GeneratedHelper implements SchemaInstantiationHelper {
|
||||||
|
@override
|
||||||
|
GeneratedDatabase databaseForVersion(QueryExecutor db, int version) {
|
||||||
|
switch (version) {
|
||||||
|
case 6:
|
||||||
|
return v6.DatabaseAtV6(db);
|
||||||
|
case 7:
|
||||||
|
return v7.DatabaseAtV7(db);
|
||||||
|
default:
|
||||||
|
throw MissingSchemaException(version, versions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static const versions = const [6, 7];
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user